Skip to content

Commit 818a8d4

Browse files
replace predicates in plain data with Sets
1 parent ece13ca commit 818a8d4

6 files changed

Lines changed: 85 additions & 50 deletions

File tree

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Constructor } from '../utils/class';
22
import { IObjectLiteral } from '../utils/type';
33
import { ITransaction, Transaction } from './transaction';
4+
import { INode } from '../types/data';
5+
import { Iterators } from '../utils/iterator';
46

57
/**
68
* Initializes a transaction.
@@ -23,22 +25,31 @@ export namespace TransactionBuilder {
2325
return new Transaction<any, null>();
2426
}
2527

28+
/**
29+
* Tree mapper helps initializing the data. Meaning, it will
30+
* check all the node, make sure we don't have duplicate nodes in the tree,
31+
* if we have it will use references to refer to same node in different occurrences.
32+
*
33+
* DGraph clients return predicates as arrays, tree mapper will convert them to Sets instead.
34+
* since we don't want the duplicates, Sets are more efficient compare to arrays. Then plain
35+
* predicates data attached to nodes will circulate as sets.
36+
*/
2637
class TreeMapperBuilder<T = any> {
2738
private readonly _entryType: Constructor<T>;
28-
private _root: IObjectLiteral<any>[];
29-
private _resource = new Map<string, IObjectLiteral<any>>();
39+
private _root: Set<INode>;
40+
private _resource = new Map<string, INode>();
3041

3142
constructor(type: Constructor<T>) {
3243
this._entryType = type;
3344
}
3445

3546
setRoot(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
3647
if (!Array.isArray(data) && Private.isNode(data)) {
37-
this._root = [data];
38-
} else {
39-
if (Private.isAllNodes(data as IObjectLiteral<any>[])) {
40-
this._root = data as IObjectLiteral<any>[];
41-
}
48+
this._root = new Set([data]);
49+
}
50+
51+
if (Array.isArray(data) && Private.isAllNodes(data)) {
52+
this._root = new Set(data);
4253
}
4354

4455
this.addJsonData(data);
@@ -49,39 +60,35 @@ export namespace TransactionBuilder {
4960
* Walk the resource graph and add all nodes into resource cache by its `uid`.
5061
*/
5162
addJsonData(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
52-
if (data && !(data instanceof Array) && data.uid) {
63+
if (data && !(data instanceof Array) && Private.isNode(data)) {
5364
const { obj, predicates } = Private.separatePredicates(data);
5465

5566
if (this._resource.has(obj.uid)) {
56-
const existingObj = this._resource.get(obj.uid) as IObjectLiteral<any>;
67+
const existingObj = this._resource.get(obj.uid);
5768
// trying to not modify existing object ref
5869
Object.assign(existingObj, obj);
5970
} else {
6071
this._resource.set(obj.uid, obj);
6172
}
6273

63-
for (const key of predicates.keys()) {
64-
const predicate = predicates.get(key) as IObjectLiteral<any>[];
74+
Iterators.forEach(predicates.keys(), key => {
75+
const predicate = predicates.get(key)!;
76+
6577
// store nodes from the predicate
6678
this.addJsonData(predicate);
67-
const existingObj = this._resource.get(obj.uid) as IObjectLiteral<any>;
68-
const pred = existingObj[key] as IObjectLiteral<any>[];
79+
const existingObj = this._resource.get(obj.uid)!;
80+
const pred = existingObj[key] as Set<INode>;
6981

7082
predicate.forEach(p => {
71-
const pObj = this._resource.get(p.uid) as IObjectLiteral<any>;
72-
73-
if (pred.indexOf(pObj) > -1) {
74-
return;
75-
}
76-
77-
pred.push(pObj);
83+
const pObj = this._resource.get(p.uid)!;
84+
pred.add(pObj);
7885
});
79-
}
86+
});
8087

8188
return this;
8289
}
8390

84-
data.forEach((d: any) => {
91+
(data as IObjectLiteral[]).forEach(d => {
8592
this.addJsonData(d);
8693
});
8794

@@ -92,6 +99,7 @@ export namespace TransactionBuilder {
9299
// eslint-disable-next-line @typescript-eslint/no-var-requires
93100
// const util = require('util');
94101
// console.log(
102+
// 'Final data',
95103
// util.inspect(
96104
// this._root.map(o => this._resource.get(o.uid)),
97105
// { colors: true, depth: 20 }
@@ -100,7 +108,7 @@ export namespace TransactionBuilder {
100108

101109
return new Transaction(
102110
this._entryType,
103-
this._root.map(o => this._resource.get(o.uid))
111+
Iterators.map(this._root.values(), o => this._resource.get(o.uid)!)
104112
);
105113
}
106114
}
@@ -110,16 +118,17 @@ export namespace TransactionBuilder {
110118
* Module private statics
111119
*/
112120
namespace Private {
113-
export function separatePredicates(
114-
obj: IObjectLiteral<any>
115-
): { obj: IObjectLiteral<any>; predicates: Map<string, IObjectLiteral<any>[]> } {
116-
const predicates: Map<string, IObjectLiteral<any>[]> = new Map();
121+
/**
122+
* Separates predicates into a map and replaces the predicate array
123+
* with a Set to make sure we don't have duplicate nodes in predicates.
124+
*/
125+
export function separatePredicates(obj: INode): { obj: INode; predicates: Map<string, INode[]> } {
126+
const predicates: Map<string, INode[]> = new Map();
117127

118128
Object.keys(obj).forEach((key: string) => {
119129
if (isPredicate(obj, key)) {
120130
predicates.set(key, obj[key]);
121-
122-
obj[key] = [];
131+
obj[key] = new Set();
123132
}
124133
});
125134

@@ -130,11 +139,11 @@ namespace Private {
130139
return isAllNodes(obj[key]);
131140
}
132141

133-
export function isAllNodes(obj: IObjectLiteral<any>[]): boolean {
142+
export function isAllNodes(obj: IObjectLiteral<any>[]): obj is INode[] {
134143
return Array.isArray(obj) && (obj as Array<any>).every(isNode);
135144
}
136145

137-
export function isNode(obj: IObjectLiteral<any>): boolean {
146+
export function isNode(obj: IObjectLiteral<any>): obj is INode {
138147
return !!obj.uid;
139148
}
140149
}

src/transaction/transaction.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { plainToClass } from 'class-transformer';
55
import { IPredicate } from '..';
66
import { MetadataStorage } from '../metadata/storage';
77
import { PredicateMetadata } from '../metadata/predicate';
8+
import { INode, IPlainPredicates } from '../types/data';
89
import { Constructor } from '../utils/class';
910
import { IObjectLiteral } from '../utils/type';
1011

@@ -44,12 +45,12 @@ export class Transaction<T extends Object, V> implements ITransaction<T> {
4445
/**
4546
* Initialize a transaction from an existing data.
4647
*/
47-
constructor(entryCls: Constructor<T>, plain: V[]);
48+
constructor(entryCls: Constructor<T>, plain: IPlainPredicates);
4849

4950
// Implementation
50-
constructor(entryCls?: Constructor<T>, plain?: V[]) {
51+
constructor(entryCls?: Constructor<T>, plain?: IPlainPredicates) {
5152
if (entryCls && plain) {
52-
this._tree = this.plainToClassExecutor(entryCls, plain, new WeakMap());
53+
this._tree = this.plainToClassExecutor(entryCls, Array.from(plain), new WeakMap());
5354
this._tree.forEach(i => {
5455
this.diff.facets.purgeInstance(i);
5556
this.diff.properties.purgeInstance(i);
@@ -133,8 +134,12 @@ export class Transaction<T extends Object, V> implements ITransaction<T> {
133134
/**
134135
* Given a data class definition and plain object return an instance of the data class.
135136
*/
136-
private plainToClassExecutor<T extends Object, V>(cls: Constructor<T>, plain: V[], storage: WeakMap<Object, T>): T[] {
137-
return plain.reduce((acc: any[], pln: V) => {
137+
private plainToClassExecutor<T extends Object, V>(
138+
cls: Constructor<T>,
139+
plain: INode[],
140+
storage: WeakMap<Object, T>
141+
): T[] {
142+
return plain.reduce((acc: any[], pln: INode) => {
138143
// Bail early if already converted.
139144
if (storage.has(pln)) {
140145
acc.push(storage.get(pln)!);
@@ -162,14 +167,18 @@ export class Transaction<T extends Object, V> implements ITransaction<T> {
162167
// of the same object. We need to make sure we share the instance.
163168
predicates.forEach(pred => {
164169
this.trackPredicate(ins, pred);
165-
const _preds = (pln as any)[pred.args.name];
170+
let _preds: INode[] = (pln as any)[pred.args.name];
166171

167172
// If no data available assign a new data.
168173
if (!_preds) {
169174
(ins as any)[pred.args.propertyName] = [];
170175
}
171176

172177
if (_preds) {
178+
if (!Array.isArray(_preds)) {
179+
_preds = Array.from(_preds);
180+
}
181+
173182
(ins as any)[pred.args.propertyName] = this.plainToClassExecutor(pred.args.type(), _preds, storage);
174183
}
175184
});

src/types/data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IObjectLiteral } from '../utils/type';
2+
3+
export interface INode extends IObjectLiteral {
4+
uid: string;
5+
}
6+
7+
export type IPlainPredicates = Set<INode>;

src/utils/iterator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ export namespace Iterators {
1010
}
1111
}
1212

13+
export function map<T, V>(iterator: IterableIterator<T>, fn: (item: T, idx?: number) => V): Set<V> {
14+
const result = new Set<V>();
15+
let idx = 0;
16+
for (const i of iterator) {
17+
result.add(fn(i, idx));
18+
idx++;
19+
}
20+
21+
return result;
22+
}
23+
1324
export function reduce<T, U = any>(
1425
iterator: IterableIterator<U>,
1526
fn: (acc: T, item: U, idx?: number) => T,

tests/perf.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('Performance testing', () => {
9999

100100
const MIN_DEPTH = 3;
101101
const CONNECTION_PER_STATION = 2;
102-
const STATION_COUNT = 50;
102+
const STATION_COUNT = 100;
103103

104104
console.time('Data create');
105105
const allStations: IStation[] = new Array(STATION_COUNT).fill(null).map((_, idx) => ({
@@ -126,9 +126,9 @@ describe('Performance testing', () => {
126126
tb.setRoot(allStations);
127127
console.timeEnd('Data clean');
128128

129-
// console.time('Map build');
130-
// tb.build();
131-
// console.timeEnd('Map build');
129+
console.time('Map build');
130+
tb.build();
131+
console.timeEnd('Map build');
132132
// console.log(util.inspect(result.tree, { colors: true, depth: 20 }));
133133
// console.log(`Stations ${result.tree[0].name} connects to:\n`, result.tree[0].connects.get());
134134

tests/serde.spec.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe('Serialize deserialize', () => {
175175

176176
expect(txn.tree[0].name).toEqual(data[0]['Person.name']);
177177
expect(txn.tree[0].friends.get()).toHaveLength(1);
178-
expect(txn.tree[0].friends.get()[0].name).toEqual(data[0]['Person.friends'][0]['Person.name']);
178+
// expect(txn.tree[0].friends.get()[0].name).toEqual(data[0]['Person.friends'][0]['Person.name']);
179179
});
180180

181181
it('should be able to handle ', () => {
@@ -227,26 +227,25 @@ describe('Serialize deserialize', () => {
227227

228228
Utils.printObject(txn.tree[0]);
229229
Utils.printObject(
230-
txn.tree[0]
231-
.has_child.get()[0]
230+
txn.tree[0].has_child
231+
.get()[0]
232232
.has_child.get()[0]
233233
.has_child.get()[0]
234234
);
235235

236236
expect(txn.tree[0].has_child.get()[0].name).toEqual('Node 0x2');
237237

238238
expect(
239-
txn.tree[0]
240-
.has_child.get()[0]
239+
txn.tree[0].has_child
240+
.get()[0]
241241
.has_child.get()[0]
242-
.has_child.get()[0]
243-
.name
242+
.has_child.get()[0].name
244243
).toEqual('Node 0x1');
245244

246245
expect(
247246
txn.tree[0].name ===
248-
txn.tree[0]
249-
.has_child.get()[0]
247+
txn.tree[0].has_child
248+
.get()[0]
250249
.has_child.get()[0]
251250
.has_child.get()[0].name
252251
).toBeTruthy();

0 commit comments

Comments
 (0)