diff --git a/package.json b/package.json
index efac420ef..26e1c02b0 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,6 @@
"es6-promise": "^3.1.2",
"es6-shim": "^0.35.0",
"file-loader": "^0.8.5",
- "md5": "^2.2.1",
"msgpack-lite": "^0.1.20",
"object-assign": "4.0.1",
"postcss-cssnext": "^2.5.2",
diff --git a/src/frontend/components/component-info/component-info.html b/src/frontend/components/component-info/component-info.html
index 63ff601b1..bd024e7cc 100644
--- a/src/frontend/components/component-info/component-info.html
+++ b/src/frontend/components/component-info/component-info.html
@@ -68,6 +68,7 @@
();
diff --git a/src/frontend/components/info-panel/info-panel.html b/src/frontend/components/info-panel/info-panel.html
index 392450fa1..7c3523335 100644
--- a/src/frontend/components/info-panel/info-panel.html
+++ b/src/frontend/components/info-panel/info-panel.html
@@ -9,6 +9,7 @@
[tree]="tree"
[loadingState]="loadingState"
[state]="state"
+ [componentMetadata]="componentMetadata"
[metadata]="metadata"
[hidden]="selectedTab !== StateTab.Properties"
[ngClass]="{flex: selectedTab === StateTab.Properties}"
diff --git a/src/frontend/components/info-panel/info-panel.ts b/src/frontend/components/info-panel/info-panel.ts
index 0cb12934f..fbc9fa118 100644
--- a/src/frontend/components/info-panel/info-panel.ts
+++ b/src/frontend/components/info-panel/info-panel.ts
@@ -9,6 +9,7 @@ import {ComponentLoadState} from '../../state';
import {StateTab} from '../../state';
import {UserActions} from '../../actions/user-actions/user-actions';
import {
+ ComponentMetadata,
InstanceWithMetadata,
Metadata,
Node,
@@ -57,6 +58,12 @@ export class InfoPanel {
: new Map();
}
+ private get componentMetadata(): ComponentMetadata {
+ return this.instanceValue
+ ? this.instanceValue.componentMetadata
+ : new Map();
+ }
+
private onSelectedTabChanged(tab: StateTab) {
this.selectedTab = tab;
}
diff --git a/src/frontend/components/render-state/render-state.html b/src/frontend/components/render-state/render-state.html
index db5d5aa7d..c030d2cdf 100644
--- a/src/frontend/components/render-state/render-state.html
+++ b/src/frontend/components/render-state/render-state.html
@@ -12,22 +12,22 @@
transparent: !expandable(k)
}">
-
+
@Input('{{getAlias(k)}}')
-
+
@Output('{{getAlias(k)}}')
-
+
@ViewChild({{getSelector(k)}})
-
+
@ViewChildren({{getSelector(k)}})
-
+
@ContentChild({{getSelector(k)}})
-
+
@ContentChildren({{getSelector(k)}})
@@ -61,6 +61,7 @@
[id]="id"
[level]="level + 1"
[path]="path.concat([k])"
+ [componentMetadata]="componentMetadata"
[metadata]="metadata"
[state]="state[k]">
diff --git a/src/frontend/components/render-state/render-state.ts b/src/frontend/components/render-state/render-state.ts
index ecd2e3507..729d8e6a5 100644
--- a/src/frontend/components/render-state/render-state.ts
+++ b/src/frontend/components/render-state/render-state.ts
@@ -1,5 +1,3 @@
-import md5 = require('md5');
-
import {
ChangeDetectionStrategy,
Component,
@@ -17,12 +15,15 @@ import {
import {getPropertyPath} from '../../../backend/utils';
+import {functionName} from '../../../utils';
+
import {
ComponentPropertyState,
ExpandState,
} from '../../state';
import {
+ ComponentMetadata,
Metadata,
Path,
ObjectType,
@@ -43,6 +44,7 @@ export enum EmitState {
})
export class RenderState {
@Input() id: string;
+ @Input() componentMetadata: ComponentMetadata;
@Input() metadata: Metadata;
@Input() level: number;
@Input() path: Path;
@@ -88,13 +90,17 @@ export class RenderState {
return `Array[${object.length}]`;
}
else if (object != null) {
- if (Object.keys(object).length === 0) {
- return '{}';
+ const constructor = functionName(object.constructor) || typeof object;
+
+ if (/object/i.test(constructor)) {
+ if (Object.keys(object).length === 0) {
+ return '{}'; // special case to denote an empty object
+ }
}
- return 'Object';
- }
- if (object === null) {
+ return constructor;
+ }
+ else if (object === null) {
return 'null';
}
else if (object === undefined) {
@@ -118,12 +124,19 @@ export class RenderState {
}
}
- private getMetadata(key: string): [ObjectType, any] {
- return this.metadata.get(md5(serializePath(this.path.concat([key]))));
+ private getComponentMetadata(key: string): [ObjectType, any] {
+ const properties = this.componentMetadata.get(this.state);
+ if (properties) {
+ const matchingProperty = properties.find(p => p[0] === key);
+ if (matchingProperty) {
+ return [matchingProperty[1], matchingProperty[2]];
+ }
+ }
+ return null;
}
private isEmittable(key: string): boolean {
- const metadata = this.getMetadata(key);
+ const metadata = this.metadata.get(this.state[key]);
if (metadata) {
return (metadata[0] & ObjectType.EventEmitter) !== 0
|| (metadata[0] & ObjectType.Subject) !== 0;
@@ -131,16 +144,15 @@ export class RenderState {
return false;
}
- private isObjectType(key: string, type: ObjectType): boolean {
- const metadata = this.getMetadata(key);
+ private isComponentObjectType(key: string, type: ObjectType): boolean {
+ const metadata = this.getComponentMetadata(key);
if (metadata) {
return (metadata[0] & type) !== 0;
}
- return false;
}
private getAlias(key: string): string {
- const metadata = this.getMetadata(key);
+ const metadata = this.getComponentMetadata(key);
if (metadata) {
const additionalProperties = metadata[1];
if (additionalProperties) {
@@ -150,7 +162,7 @@ export class RenderState {
}
private getSelector(key: string): string {
- const metadata = this.getMetadata(key);
+ const metadata = this.getComponentMetadata(key);
if (metadata) {
const additionalProperties = metadata[1];
if (additionalProperties) {
diff --git a/src/frontend/frontend.ts b/src/frontend/frontend.ts
index 2fdd7dc3b..a253ae5eb 100644
--- a/src/frontend/frontend.ts
+++ b/src/frontend/frontend.ts
@@ -257,13 +257,19 @@ class App {
const m = MessageFactory.selectComponent(node, node.isComponent);
if (node.isComponent) {
-
const promise = this.directConnection.handleImmediate(m)
.then(response => {
if (typeof beforeLoad === 'function') {
beforeLoad();
}
- return response;
+
+ const {instance, metadata, componentMetadata} = response;
+
+ return {
+ instance,
+ metadata: new Map(metadata),
+ componentMetadata: new Map(componentMetadata),
+ };
});
this.componentState.wait(node, promise);
diff --git a/src/tree/metadata.ts b/src/tree/metadata.ts
index 4723ae63c..f1f855e90 100644
--- a/src/tree/metadata.ts
+++ b/src/tree/metadata.ts
@@ -1,5 +1,3 @@
-import md5 = require('md5');
-
import {
AsyncSubject,
BehaviorSubject,
@@ -9,12 +7,6 @@ import {
Subject,
} from 'rxjs';
-import {
- Path,
- serializePath,
- deserializePath,
-} from './path';
-
import {Node} from './node';
import {
@@ -42,53 +34,56 @@ export enum ObjectType {
ContentChildren = 0x100,
}
-export type Metadata = Map;
+export type Metadata = Map;
+
+export type ComponentMetadata = Map;
export interface InstanceWithMetadata {
instance: any;
metadata: Metadata;
+ componentMetadata: ComponentMetadata;
}
-export const instanceWithMetadata = (node: Node, instance): InstanceWithMetadata => {
+// It is imperative that the metadata and the instance value itself travel together
+// through the serializer, otherwise we are going to have to serialize the entire
+// object structure twice, once for the instance and once for the metadata. But if
+// we put them together as part of the same object, the serializer will be smart
+// enough not to duplicate objects. If someone breaks apart the instance and the
+// metadata into two objects, a lot of code that depends on reference equality is
+// going to get broken! So do not change this!
+export const instanceWithMetadata = (node: Node, instance) => {
if (node == null || instance == null) {
return null;
}
- const metadata = new Map();
+ const objectMetadata = new Map();
- const nodePath = deserializePath(node.id);
+ const components = new Map();
- const serialize = (path: Path): string => md5(serializePath(path));
-
- recurse(nodePath, instance,
- (path: Path, obj) => {
- let type = objectType(obj);
-
- const update = (p: Path, flag: ObjectType, additionalProps) => {
- const serializedPath = serialize(p);
-
- const existing = metadata.get(serializedPath);
+ recurse(instance,
+ obj => {
+ const update = (key: string, flag: ObjectType, additionalProps) => {
+ const existing = components.get(obj);
if (existing) {
- existing[0] |= flag;
- Object.assign(existing, additionalProps);
+ existing.push([key, flag, additionalProps]);
}
else {
- metadata.set(serializedPath, [flag, additionalProps]);
+ components.set(obj, [[key, flag, additionalProps]]);
}
};
const component = componentMetadata(obj);
if (component) {
for (const input of componentInputs(component, obj)) {
- update(path.concat([input.propertyKey]), ObjectType.Input, {alias: input.bindingPropertyName});
+ update(input.propertyKey, ObjectType.Input, {alias: input.bindingPropertyName});
}
for (const output of componentOutputs(component, obj)) {
- update(path.concat([output.propertyKey]), ObjectType.Output, {alias: output.bindingPropertyName});
+ update(output.propertyKey, ObjectType.Output, {alias: output.bindingPropertyName});
}
const addQuery = (decoratorType: string, objectType: ObjectType) => {
for (const vc of componentQueryChildren(decoratorType, component, obj)) {
- update(path.concat([vc.propertyKey]), objectType, {selector: vc.selector});
+ update(vc.propertyKey, objectType, {selector: vc.selector});
}
};
@@ -98,20 +93,23 @@ export const instanceWithMetadata = (node: Node, instance): InstanceWithMetadata
addQuery('@ContentChildren', ObjectType.ContentChildren);
}
+ const type = objectType(obj);
if (type !== 0) {
- const serializedPath = serialize(path);
-
- const existing = metadata.get(serializedPath);
+ const existing = objectMetadata.get(obj);
if (existing) {
- metadata.set(serializedPath, [existing[0] | type, existing[1]]);
+ objectMetadata.set(obj, [existing[0] | type, existing[1]]);
}
else {
- metadata.set(serializedPath, [type, null]);
+ objectMetadata.set(obj, [type, null]);
}
}
});
- return {instance, metadata};
+ return {
+ instance,
+ metadata: Array.from( objectMetadata),
+ componentMetadata: Array.from( components),
+ };
};
const objectType = (object): ObjectType => {
diff --git a/src/utils/circular-recurse.ts b/src/utils/circular-recurse.ts
index 91a74f8f0..c1caf4339 100644
--- a/src/utils/circular-recurse.ts
+++ b/src/utils/circular-recurse.ts
@@ -1,29 +1,36 @@
import {Path} from '../tree';
-export type Apply = (path: Path, value) => void;
+import {isScalar} from './scalar';
-export const recurse = (initialPath: Path, object, exec: Apply) => {
+export type Apply = (value) => void;
+
+// Recursive traversal of an object tree, but will not traverse circular references or DOM elements
+export const recurse = (object, apply: Apply) => {
const visited = new Set();
- const apply = (path: Path, value, fn: Apply) => {
- if (value == null || visited.has(value)) {
+ const visit = value => {
+ if (value == null || isScalar(value) || /Element/.test(Object.prototype.toString.call(value))) {
+ return;
+ }
+
+ if (visited.has(value)) { // circular loop
return;
}
visited.add(value);
- fn(path, value);
+ apply(value);
if (Array.isArray(value) || value instanceof Set) {
- (value).forEach((v, k) => apply(path.concat([k]), v, fn));
+ (value).forEach((v, k) => visit(v));
}
else if (value instanceof Map) {
- value.forEach((v, k) => apply(path.concat([k]), v, fn));
+ value.forEach((v, k) => visit(v));
}
else {
- Object.keys(value).forEach(k => apply(path.concat([k]), value[k], fn));
+ Object.keys(value).forEach(k => visit(value[k]));
}
};
- apply(initialPath, object, exec);
+ visit(object);
};
diff --git a/src/utils/scalar.ts b/src/utils/scalar.ts
index 22ba67e7e..9df1438b8 100644
--- a/src/utils/scalar.ts
+++ b/src/utils/scalar.ts
@@ -1,6 +1,7 @@
export const isScalar = value => {
switch (typeof value) {
case 'string':
+ case 'number':
case 'boolean':
case 'function':
case 'undefined':
diff --git a/src/utils/serialize.ts b/src/utils/serialize.ts
index e086cb47f..ac9756d8c 100644
--- a/src/utils/serialize.ts
+++ b/src/utils/serialize.ts
@@ -191,20 +191,30 @@ function map(operation: Operation, value) {
break;
default:
operation.tails.push(() => {
- operation.objref[index] = `{${Object.getOwnPropertyNames(value).map(key => {
- const mapped = map(operation, value[key]);
+ const ctor = functionName((value || {}).constructor) || '';
+ const mapProps = (key: string) => {
+ const mapped = map(operation, value[key]);
if (mapped instanceof Reference) {
mapped.source = index;
mapped.key = key;
-
operation.hashes.push(mapped);
-
return mapped;
}
return `${JSON.stringify(key)}: ${mapped}`;
- }).filter(v => v instanceof Reference === false).join(',')}}`;
+ };
+
+ const keys = Object.keys(value)
+ .map(key => mapProps(key))
+ .filter(v => v instanceof Reference === false).join(',');
+
+ if (nonstandardType(ctor)) { // retain object type information
+ operation.objref[index] = `new (function ${ctor}() {Object.assign(this, {${keys}});})()`;
+ }
+ else {
+ operation.objref[index] = `{${keys}}`;
+ }
});
break;
}
@@ -213,3 +223,17 @@ function map(operation: Operation, value) {
}
}
}
+
+const nonstandardType = (type: string) => {
+ switch (type.toLowerCase()) {
+ case 'object':
+ case 'function':
+ case 'string':
+ case 'number':
+ case 'regexp':
+ case 'date':
+ return false;
+ default:
+ return true;
+ }
+};
diff --git a/typings.json b/typings.json
index f036b8bf1..9b4cfe587 100644
--- a/typings.json
+++ b/typings.json
@@ -17,7 +17,6 @@
"es6-shim": "registry:dt/es6-shim#0.31.2+20160602141504",
"filesystem": "registry:dt/filesystem#0.0.0+20160316155526",
"filewriter": "registry:dt/filewriter#0.0.0+20160316155526",
- "md5": "registry:dt/md5#2.1.0+20160521153251",
"node": "registry:dt/node#6.0.0+20160823142437",
"tape": "registry:dt/tape#4.2.2+20160317120654",
"webrtc/mediastream": "registry:dt/webrtc/mediastream#0.0.0+20160317120654"