Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"build": "tsdx build",
"lint": "tsdx lint --fix",
"test": "tsdx test",
"test:coverage": "tsdx test --coverage",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why",
Expand Down
2 changes: 1 addition & 1 deletion src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class Graph {
private _pathSet: Set<string> = new Set();

constructor(point: string | number, slug: Array<string | number> = []) {
this.slug = slug || [];
this.slug = slug;
this._point = point;
}

Expand Down
1 change: 0 additions & 1 deletion src/Reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,6 @@ class Reaction {
const graphMap = new Map();

const keys = Object.keys(paths);

keys.forEach(graphMapKey => {
const graph = graphMap.has(graphMapKey)
? graphMap.get(graphMapKey)
Expand Down
4 changes: 2 additions & 2 deletions src/StateTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const createPlainTrackerObject = function({
lastUpdateAt,
}: StateTrackerConstructorProps) {
return {
_id: canIUseProxy()
_id: canIUseProxy
? `ProxyStateTracker_${count++}`
: `ES5StateTracker_${count++}`,
_useProxy: canIUseProxy(),
_useProxy: canIUseProxy,
_accessPath: accessPath,
_rootPath: rootPath,
_type: Array.isArray(base) ? Type.Array : Type.Object,
Expand Down
2 changes: 1 addition & 1 deletion src/StateTrackerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class StateTrackerNode {
// which need to be fixed for performance
isPropsShallowEqual(
nextProps: ObserverProps,
options: {
options?: {
changedValue?: ChangedValue;
omitKeys?: Array<string>;
}
Expand Down
63 changes: 27 additions & 36 deletions src/StateTrackerUtil.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import {
raw,
TRACKER,
pathEqual,
isTrackable,
shallowCopy,
isPlainObject,
generateRandomContextKey,
isProxy,
buildCachedProxyPath,
fastJoin,
IS_PROXY,
} from './commons';
import {
State,
Expand All @@ -26,34 +25,34 @@ import StateTrackerContext from './StateTrackerContext';
import StateTrackerNode from './StateTrackerNode';

const StateTrackerUtil = {
hasTracker: function (proxy: IStateTracker) {
hasTracker: function(proxy: IStateTracker) {
return proxy && isPlainObject(proxy) && !!proxy[TRACKER];
},

getTracker: function (proxy: IStateTracker) {
getTracker: function(proxy: IStateTracker) {
return proxy[TRACKER];
},

enter: function (proxy: IStateTracker, mark?: string, props?: ObserverProps) {
enter: function(proxy: IStateTracker, mark?: string, props?: ObserverProps) {
const tracker = proxy[TRACKER];
const name = mark || generateRandomContextKey();
const trackerContext = tracker._stateTrackerContext;
trackerContext.enter(name, props);
},

enterNode: function (proxy: IStateTracker, node: StateTrackerNode) {
enterNode: function(proxy: IStateTracker, node: StateTrackerNode) {
const tracker = proxy[TRACKER];
const trackerContext = tracker._stateTrackerContext;
trackerContext.enterNode(node);
},

leave: function (proxy: IStateTracker) {
leave: function(proxy: IStateTracker) {
const tracker = proxy[TRACKER];
const trackerContext = tracker._stateTrackerContext;
trackerContext.leave();
},

peek: function (
peek: function(
proxyState: IStateTracker | NextState,
accessPath: Array<string>
) {
Expand All @@ -73,7 +72,7 @@ const StateTrackerUtil = {
}, proxyState);
},

peekValue: function (
peekValue: function(
proxyState: IStateTracker | NextState,
key: string | number
) {
Expand Down Expand Up @@ -283,7 +282,7 @@ const StateTrackerUtil = {
return token;
},

getContext: function (proxy: IStateTracker) {
getContext: function(proxy: IStateTracker) {
const tracker = proxy[TRACKER];
return tracker._stateTrackerContext;
},
Expand All @@ -303,46 +302,38 @@ const StateTrackerUtil = {
stateTrackerContext: StateTrackerContext;
createProxy: (state: State, options: ProduceProxyOptions) => IStateTracker;
}) {
if (!isTrackable(value)) {
const trackableFlag = isTrackable(value);
if (!trackableFlag) {
return value;
}
const path = buildCachedProxyPath(nextAccessPath.slice(0, -1));
const isProxyValue = isProxy(value);

const isProxyValue = trackableFlag && value[IS_PROXY];
if (isProxyValue) {
const tracker = StateTrackerUtil.getTracker(value);
if (
pathEqual(nextAccessPath.slice(0, 1), tracker._accessPath.slice(0, 1))
) {
return value;
}
if (nextAccessPath[0] === tracker._accessPath[0]) return value;
}

const rawValue = raw(value);

let rawValue = value;
if (isProxyValue) rawValue = StateTrackerUtil.getTracker(value)._base;
const path = buildCachedProxyPath(nextAccessPath.slice(0, -1));
const cachedProxy = stateTrackerContext.getCachedProxy(path, rawValue);

if (cachedProxy) return cachedProxy;
if (!isProxy(value)) {
const next = createProxy(value, {
accessPath: nextAccessPath.slice() as Array<string>,

const createNextProxy = () => {
const next = createProxy(isProxyValue ? shallowCopy(value) : value, {
accessPath: nextAccessPath.slice() as string[],
parentProxy: proxy as IStateTracker,
rootPath,
stateTrackerContext,
});
stateTrackerContext.setCachedProxy(path, value, next);
stateTrackerContext.setCachedProxy(
path,
isProxyValue ? rawValue : value,
next
);
return next;
}

const next = createProxy(isProxyValue ? shallowCopy(value) : value, {
accessPath: nextAccessPath.slice() as Array<string>,
parentProxy: proxy as IStateTracker,
rootPath,
stateTrackerContext,
});
};

stateTrackerContext.setCachedProxy(path, rawValue, next);
return next;
return createNextProxy();
},
};

Expand Down
24 changes: 11 additions & 13 deletions src/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,8 @@ const ownKeys = (o: any) =>
)
: Object.getOwnPropertyNames(o);

export const arrayProtoOwnKeys = Object.freeze(
ownKeys(Object.getPrototypeOf([]))
);
export const objectProtoOwnKeys = Object.freeze(
ownKeys(Object.getPrototypeOf({}))
);
export const arrayProtoOwnKeys = new Set(ownKeys(Object.getPrototypeOf([])));
export const objectProtoOwnKeys = new Set(ownKeys(Object.getPrototypeOf({})));

export const emptyFunction = () => {};

Expand All @@ -46,22 +42,22 @@ export const IS_PROXY: unique symbol = hasSymbol
? Symbol.for('is_proxy')
: ('__is_proxy__' as any);

export const canIUseProxy = () => {
export const canIUseProxy = (() => {
try {
new Proxy({}, {}); // eslint-disable-line
} catch (err) {
return false;
}

return true;
};
})();

export const hasOwnProperty = (o: object, prop: PropertyKey) =>
o.hasOwnProperty(prop); // eslint-disable-line

export const isTrackable = (o: any) => {
// eslint-disable-line
return ['[object Object]', '[object Array]'].indexOf(toString(o)) !== -1;
const type = toString(o);
return type === '[object Object]' || type === '[object Array]';
};

export const isNumber = (obj: any) => toString(obj) === '[object Number]';
Expand Down Expand Up @@ -91,7 +87,7 @@ export function each<T>(obj: T, iter: Iter<T>) {
);
} else if (isObject(obj)) {
// @ts-ignore
ownKeys(obj).forEach((key) => (iter as EachObject<T>)(key, obj[key], obj));
ownKeys(obj).forEach(key => (iter as EachObject<T>)(key, obj[key], obj));
}
}

Expand All @@ -106,7 +102,7 @@ export function shallowCopy(o: any) {
tracker._isPeeking = true;
if (Array.isArray(o)) return o.slice();
const value = Object.create(Object.getPrototypeOf(o));
ownKeys(o).forEach((key) => {
ownKeys(o).forEach(key => {
value[key] = o[key];
});
tracker._isPeeking = false;
Expand Down Expand Up @@ -255,7 +251,7 @@ export function peek(
if (index === -1) return null;

const left = accessPathString.slice(index + rootPathString.length);
const parts = left.split('_').filter((v) => v);
const parts = left.split('_').filter(v => v);
return parts.reduce((n: { [key: string]: any }, c: string) => {
return n[c];
}, obj);
Expand All @@ -279,3 +275,5 @@ export const noop = () => {};
export const DEFAULT_CACHED_PROXY_PATH = '__$__';
export const buildCachedProxyPath = (paths: Array<string | number>) =>
fastJoin(paths, '_');

export const internalKeys = new Set([TRACKER, 'unlink']);
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import StateTrackerUtil from './StateTrackerUtil';

// produce = ES6Produce;

// if (canIUseProxy()) {
// if (canIUseProxy) {
// produce = ES6Produce;
// } else {
// produce = ES5Produce;
Expand Down
2 changes: 1 addition & 1 deletion src/produce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { produceImpl as produceImplES5 } from './es5';
import { State, ProxyCache } from './types';

const produce = (state: State, proxyCache?: ProxyCache) => {
if (canIUseProxy()) return produceImpl(state, proxyCache);
if (canIUseProxy) return produceImpl(state, proxyCache);
return produceImplES5(state, proxyCache);
};

Expand Down
35 changes: 16 additions & 19 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
arrayProtoOwnKeys,
objectProtoOwnKeys,
createHiddenProperty,
internalKeys,
} from './commons';
import { createPlainTrackerObject } from './StateTracker';
import {
Expand All @@ -20,7 +21,6 @@ import {
import StateTrackerContext from './StateTrackerContext';
import StateTrackerUtil from './StateTrackerUtil';
import Container from './Container';

export function produceImpl(
state: State,
// affected?: WeakMap<object, IStateTracker>,
Expand Down Expand Up @@ -54,28 +54,27 @@ export function createProxy(
rootPath = [],
stateTrackerContext,
} = options || {};
const internalKeys = [TRACKER, 'unlink'];

const handler = {
get: (target: IStateTracker, prop: PropertyKey, receiver: any) => {
try {
// https://stackoverflow.com/questions/36372611/how-to-test-if-an-object-is-a-proxy
if (prop === IS_PROXY) return true;
if (internalKeys.indexOf(prop as string | symbol) !== -1)
return Reflect.get(target, prop, receiver);
if (typeof prop === 'symbol')
if (
internalKeys.has(prop as string | symbol) ||
typeof prop === 'symbol'
)
return Reflect.get(target, prop, receiver);
let tracker = Reflect.get(target, TRACKER) as StateTrackerProperties;
const targetType = tracker._type;

// https://stackoverflow.com/questions/55057200/is-the-set-has-method-o1-and-array-indexof-on
switch (targetType) {
case Type.Array:
// length should be tracked
if (prop !== 'length' && ~arrayProtoOwnKeys.indexOf(prop as any))
if (prop !== 'length' && arrayProtoOwnKeys.has(prop as any))
return Reflect.get(target, prop, receiver);
break;
case Type.Object:
if (~objectProtoOwnKeys.indexOf(prop as any))
if (objectProtoOwnKeys.has(prop as any))
return Reflect.get(target, prop, receiver);
break;
}
Expand All @@ -101,15 +100,13 @@ export function createProxy(
});

if (!isPeeking) {
if (stateTrackerContext.getCurrent()) {
stateTrackerContext.getCurrent().track({
target,
key: prop,
// isDerived,
value: value,
path: nextAccessPath,
});
}
stateTrackerContext.getCurrent()?.track({
target,
key: prop,
// isDerived,
value: value,
path: nextAccessPath,
});
}

return value;
Expand Down Expand Up @@ -181,7 +178,7 @@ export function createProxy(
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_define_property_object_not_extensible
// if property value is not extensible, it will cause error. such as a ref value..
createHiddenProperty(proxy, TRACKER, tracker);
createHiddenProperty(proxy, 'unlink', function (this: IStateTracker) {
createHiddenProperty(proxy, 'unlink', function(this: IStateTracker) {
const tracker = this[TRACKER];
return tracker._base;
});
Expand Down
32 changes: 32 additions & 0 deletions test/Graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Graph from '../src/Graph';
import StateTrackerError from '../src/StateTrackerError';

describe('Graph', () => {
it('Graph will created', () => {
const paths: any = [['app'], ['app', 'title'], ['app', 'description']];
const graph = new Graph('app');
paths.forEach((path: string[]) => graph?.access(path));
expect(graph.getPoint()).toBe('app');
expect(graph.getPaths()).toEqual([
['app'],
['app', 'title'],
['app', 'description'],
]);
expect(graph.getPath()).toEqual(['app']);
});

it('Graph will created', () => {
try {
const paths: any = [[], []];
const graph = new Graph('app');
paths.forEach((path: string[]) => graph?.access(path));
expect(graph.getPoint()).toBe('app');
expect(true).toBe(false);
} catch (e) {
const error = new StateTrackerError(
`access Path '' should be start with 'app'`
);
expect(e).toStrictEqual(error);
}
});
});
Loading