Skip to content

Commit 2f3b718

Browse files
authored
fix(loki): implement deep clone as default clone option (#44)
* remove old shallow clone and just use the new shallow assign as shallow * move functions from utils and remove the file
1 parent dde34ed commit 2f3b718

File tree

7 files changed

+138
-106
lines changed

7 files changed

+138
-106
lines changed

packages/loki/spec/generic/cloning.spec.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,27 @@ describe("cloning behavior", () => {
108108
const cdb = new Loki("cloningEnabled");
109109
const citems = cdb.addCollection<User>("items", {
110110
clone: true
111-
//, clonemethod: "parse-stringify"
111+
});
112+
113+
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
114+
citems.insert({name: "gungnir", owner: "odin", maker: "elves"});
115+
citems.insert({name: "tyrfing", owner: "Svafrlami", maker: "dwarves"});
116+
citems.insert({name: "draupnir", owner: "odin", maker: "elves"});
117+
118+
// just to prove that resultset.data() is not giving the user the actual object reference we keep internally
119+
// we will modify the object and see if future requests for that object show the change
120+
const mj = citems.find({name: "mjolnir"})[0];
121+
mj.maker = "the dwarves";
122+
123+
const mj2 = citems.find({name: "mjolnir"})[0];
124+
expect(mj2.maker).toBe("dwarves");
125+
});
126+
127+
it("works with stringify", () => {
128+
const cdb = new Loki("cloningEnabled");
129+
const citems = cdb.addCollection<User>("items", {
130+
clone: true,
131+
cloneMethod: CloneMethod.PARSE_STRINGIFY
112132
});
113133

114134
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
@@ -131,7 +151,6 @@ describe("cloning behavior", () => {
131151
const cdb = new Loki("cloningEnabled");
132152
const citems = cdb.addCollection<User>("items", {
133153
clone: true
134-
//, clonemethod: "parse-stringify"
135154
});
136155

137156
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
@@ -155,7 +174,6 @@ describe("cloning behavior", () => {
155174
const cdb = new Loki("cloningEnabled");
156175
const citems = cdb.addCollection<User>("items", {
157176
clone: true
158-
//, clonemethod: "parse-stringify"
159177
});
160178

161179
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
@@ -179,7 +197,6 @@ describe("cloning behavior", () => {
179197
const citems = cdb.addCollection<User>("items", {
180198
clone: true,
181199
unique: ["name"]
182-
//, clonemethod: "parse-stringify"
183200
});
184201

185202
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
@@ -221,7 +238,6 @@ describe("cloning behavior", () => {
221238
const cdb = new Loki("cloningEnabled");
222239
const citems = cdb.addCollection<User>("items", {
223240
clone: true
224-
//, clonemethod: "parse-stringify"
225241
});
226242

227243
citems.insert({name: "mjolnir", owner: "thor", maker: "dwarves"});
@@ -245,7 +261,6 @@ describe("cloning behavior", () => {
245261
// within resultset.data() options
246262
const mj = items.chain().find({name: "mjolnir"}).data({
247263
forceClones: true
248-
//,forceCloneMethod: 'parse-stringify'
249264
})[0];
250265
mj.maker = "the dwarves";
251266

packages/loki/spec/generic/transforms.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ describe("transforms", () => {
155155
expect(items.chain(tx1, {foo: 5}) as any as number).toBe(7);
156156
// params will cause a recursive shallow clone of objects before substitution
157157
expect(items.chain(tx2, {minimumAge: 4}) as any as number).toBe(7);
158-
158+
159159
// make sure original transform is unchanged
160160
expect(tx2[0].type).toEqual("find");
161161
expect(tx2[0].value.age.$gt).toEqual("[%lktxp]minimumAge");

packages/loki/src/clone.ts

+62-37
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,63 @@
1-
export type ANY = any;
1+
export enum CloneMethod {
2+
PARSE_STRINGIFY,
3+
DEEP,
4+
SHALLOW,
5+
SHALLOW_ASSIGN,
6+
SHALLOW_RECURSE_OBJECTS,
7+
}
8+
9+
function add(copy: any, key: any, value: any) {
10+
if (copy instanceof Array) {
11+
copy.push(value);
12+
return copy[copy.length - 1];
13+
}
14+
else if (copy instanceof Object) {
15+
copy[key] = value;
16+
return copy[key];
17+
}
18+
}
19+
20+
function walk(target: any, copy: any) {
21+
for (let key in target) {
22+
let obj = target[key];
23+
if (obj instanceof Date) {
24+
let value = new Date(obj.getTime());
25+
add(copy, key, value);
26+
}
27+
else if (obj instanceof Function) {
28+
let value = obj;
29+
add(copy, key, value);
30+
}
31+
else if (obj instanceof Array) {
32+
let value: any[] = [];
33+
let last = add(copy, key, value);
34+
walk(obj, last);
35+
}
36+
else if (obj instanceof Object) {
37+
let value = {};
38+
let last = add(copy, key, value);
39+
walk(obj, last);
40+
}
41+
else {
42+
let value = obj;
43+
add(copy, key, value);
44+
}
45+
}
46+
}
47+
48+
// Deep copy from Simeon Velichkov.
49+
function deepCopy(target: any) {
50+
if (/number|string|boolean/.test(typeof target)) {
51+
return target;
52+
}
53+
if (target instanceof Date) {
54+
return new Date(target.getTime());
55+
}
56+
57+
const copy = (target instanceof Array) ? [] : {};
58+
walk(target, copy);
59+
return copy;
60+
}
261

362
export function clone<T>(data: T, method: CloneMethod = CloneMethod.PARSE_STRINGIFY): T {
463
if (data === null || data === undefined) {
@@ -11,19 +70,10 @@ export function clone<T>(data: T, method: CloneMethod = CloneMethod.PARSE_STRING
1170
case CloneMethod.PARSE_STRINGIFY:
1271
cloned = JSON.parse(JSON.stringify(data));
1372
break;
14-
case CloneMethod.JQUERY_EXTEND_DEEP:
15-
//cloned = jQuery.extend(true, {}, data);
16-
// TODO
73+
case CloneMethod.DEEP:
74+
cloned = deepCopy(data);
1775
break;
1876
case CloneMethod.SHALLOW:
19-
// more compatible method for older browsers
20-
cloned = Object.create(data.constructor.prototype);
21-
Object.keys(data).map((i) => {
22-
cloned[i] = data[i];
23-
});
24-
break;
25-
case CloneMethod.SHALLOW_ASSIGN:
26-
// should be supported by newer environments/browsers
2777
cloned = Object.create(data.constructor.prototype);
2878
Object.assign(cloned, data);
2979
break;
@@ -45,28 +95,3 @@ export function clone<T>(data: T, method: CloneMethod = CloneMethod.PARSE_STRING
4595

4696
return cloned as any as T;
4797
}
48-
49-
export function cloneObjectArray(objarray: object[], method: CloneMethod) {
50-
let i;
51-
const result = [];
52-
53-
if (method === CloneMethod.PARSE_STRINGIFY) {
54-
return clone(objarray, method);
55-
}
56-
57-
i = objarray.length - 1;
58-
59-
for (; i <= 0; i--) {
60-
result.push(clone(objarray[i], method));
61-
}
62-
63-
return result;
64-
}
65-
66-
export enum CloneMethod {
67-
PARSE_STRINGIFY,
68-
JQUERY_EXTEND_DEEP,
69-
SHALLOW,
70-
SHALLOW_ASSIGN,
71-
SHALLOW_RECURSE_OBJECTS,
72-
}

packages/loki/src/collection.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import {LokiEventEmitter} from "./event_emitter";
22
import {UniqueIndex} from "./unique_index";
33
import {Resultset} from "./resultset";
44
import {DynamicView} from "./dynamic_view";
5-
import {clone, CloneMethod} from "./clone";
65
import {ltHelper, gtHelper, aeqHelper} from "./helper";
76
import {Loki} from "./loki";
8-
import {copyProperties} from "./utils";
7+
import {clone, CloneMethod} from "./clone";
98
import {Doc, Dict, Query} from "../../common/types";
109
import {FullTextSearch} from "../../full-text-search/src/full_text_search";
1110
import {PLUGINS} from "../../common/plugin";
@@ -172,7 +171,7 @@ export class Collection<E extends object = object> extends LokiEventEmitter {
172171
* @param {boolean} [options.disableDeltaChangesApi=true] - set to false to enable Delta Changes API (requires Changes API, forces cloning)
173172
* @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
174173
* @param {boolean} [options.serializableIndices =true] - converts date values on binary indexed property values are serializable
175-
* @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow'
174+
* @param {string} [options.cloneMethod=CloneMethod.DEEP] - the clone method
176175
* @param {number} [options.transactional=false] - ?
177176
* @param {number} options.ttl - ?
178177
* @param {number} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
@@ -239,7 +238,7 @@ export class Collection<E extends object = object> extends LokiEventEmitter {
239238
this.disableDeltaChangesApi = options.disableDeltaChangesApi !== undefined ? options.disableDeltaChangesApi : true;
240239

241240
// .
242-
this.cloneMethod = options.cloneMethod !== undefined ? options.cloneMethod : CloneMethod.PARSE_STRINGIFY;
241+
this.cloneMethod = options.cloneMethod !== undefined ? options.cloneMethod : CloneMethod.DEEP;
243242
if (this.disableChangesApi) {
244243
this.disableDeltaChangesApi = true;
245244
}
@@ -359,7 +358,7 @@ export class Collection<E extends object = object> extends LokiEventEmitter {
359358
coll.asyncListeners = obj.asyncListeners;
360359
coll.disableChangesApi = obj.disableChangesApi;
361360
coll.cloneObjects = obj.cloneObjects;
362-
coll.cloneMethod = obj.cloneMethod || "parse-stringify";
361+
coll.cloneMethod = obj.cloneMethod || CloneMethod.DEEP;
363362
coll.changes = obj.changes;
364363

365364
coll.dirty = (options && options.retainDirtyFlags === true) ? obj.dirty : false;
@@ -368,7 +367,11 @@ export class Collection<E extends object = object> extends LokiEventEmitter {
368367
const collOptions = options[coll.name];
369368

370369
if (collOptions.proto) {
371-
let inflater = collOptions.inflate || copyProperties;
370+
let inflater = collOptions.inflate || ((src: object, dest: object) => {
371+
for (let prop in src) {
372+
dest[prop] = src[prop];
373+
}
374+
});
372375

373376
return (data: ANY) => {
374377
const collObj = new (collOptions.proto)();

packages/loki/src/loki.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export class Loki extends LokiEventEmitter {
260260
* @param {boolean} [options.asyncListeners=false] - whether listeners are called asynchronously
261261
* @param {boolean} [options.disableChangesApi=true] - set to false to enable Changes Api
262262
* @param {boolean} [options.clone=false] - specify whether inserts and queries clone to/from user
263-
* @param {string} [options.cloneMethod='parse-stringify'] - 'parse-stringify', 'jquery-extend-deep', 'shallow, 'shallow-assign'
263+
* @param {string} [options.cloneMethod=CloneMethod.DEEP] - the clone method
264264
* @param {int} options.ttlInterval - time interval for clearing out 'aged' documents; not set by default.
265265
* @returns {Collection} a reference to the collection which was just added
266266
*/

packages/loki/src/resultset.ts

+44-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import {clone, CloneMethod} from "./clone";
21
import {Collection} from "./collection";
3-
import {resolveTransformParams} from "./utils";
2+
import {clone, CloneMethod} from "./clone";
43
import {ltHelper, gtHelper, aeqHelper, sortHelper} from "./helper";
54
import {Doc, Query} from "../../common/types";
65

@@ -17,6 +16,45 @@ export type ANY = any;
1716
1817
*/
1918

19+
// used to recursively scan hierarchical transform step object for param substitution
20+
function resolveTransformObject(subObj: object, params: object, depth: number = 0) {
21+
let prop;
22+
let pname;
23+
24+
if (++depth >= 10) return subObj;
25+
26+
for (prop in subObj) {
27+
if (typeof subObj[prop] === "string" && subObj[prop].indexOf("[%lktxp]") === 0) {
28+
pname = subObj[prop].substring(8);
29+
if (params[pname] !== undefined) {
30+
subObj[prop] = params[pname];
31+
}
32+
} else if (typeof subObj[prop] === "object") {
33+
subObj[prop] = resolveTransformObject(subObj[prop], params, depth);
34+
}
35+
}
36+
37+
return subObj;
38+
}
39+
40+
// top level utility to resolve an entire (single) transform (array of steps) for parameter substitution
41+
function resolveTransformParams(transform: any, params: object) {
42+
let idx;
43+
let clonedStep;
44+
const resolvedTransform = [];
45+
46+
if (typeof params === "undefined") return transform;
47+
48+
// iterate all steps in the transform array
49+
for (idx = 0; idx < transform.length; idx++) {
50+
// clone transform so our scan/replace can operate directly on cloned transform
51+
clonedStep = clone(transform[idx], CloneMethod.SHALLOW_RECURSE_OBJECTS);
52+
resolvedTransform.push(resolveTransformObject(clonedStep, params));
53+
}
54+
55+
return resolvedTransform;
56+
}
57+
2058
function containsCheckFn(a: ANY) {
2159
if (typeof a === "string" || Array.isArray(a)) {
2260
return (b: ANY) => (a as string).indexOf(b) !== -1;
@@ -1014,10 +1052,10 @@ export class Resultset<E extends object = object> {
10141052
forceCloneMethod = CloneMethod.SHALLOW;
10151053
}
10161054

1017-
// if collection has delta changes active, then force clones and use 'parse-stringify' for effective change tracking of nested objects
1055+
// if collection has delta changes active, then force clones and use CloneMethod.DEEP for effective change tracking of nested objects
10181056
if (!this.collection.disableDeltaChangesApi) {
10191057
forceClones = true;
1020-
forceCloneMethod = CloneMethod.PARSE_STRINGIFY;
1058+
forceCloneMethod = CloneMethod.DEEP;
10211059
}
10221060

10231061
// if this has no filters applied, just return collection.data
@@ -1140,7 +1178,7 @@ export class Resultset<E extends object = object> {
11401178
*/
11411179
//eqJoin<T extends object>(joinData: T[] | Resultset<T>, leftJoinKey: string | ((obj: E) => string), rightJoinKey: string | ((obj: T) => string)): Resultset<{ left: E; right: T; }>;
11421180
// eqJoin<T extends object, U extends object>(joinData: T[] | Resultset<T>, leftJoinKey: string | ((obj: E) => string), rightJoinKey: string | ((obj: T) => string), mapFun?: (a: E, b: T) => U, dataOptions?: Resultset.DataOptions): Resultset<U> {
1143-
eqJoin(joinData: ANY, leftJoinKey: string | Function, rightJoinKey: string | Function, mapFun?: Function, dataOptions?: ANY) : ANY {
1181+
eqJoin(joinData: ANY, leftJoinKey: string | Function, rightJoinKey: string | Function, mapFun?: Function, dataOptions?: ANY): ANY {
11441182
// eqJoin<T extends object, U extends object>(joinData: T[] | Resultset<T>, leftJoinKey: string | ((obj: E) => string), rightJoinKey: string | ((obj: T) => string), mapFun?: (a: E, b: T) => U, dataOptions?: Resultset.DataOptions): Resultset<U> {
11451183
let leftData = [];
11461184
let leftDataLength;
@@ -1204,7 +1242,7 @@ export class Resultset<E extends object = object> {
12041242
* @param {boolean} dataOptions.forceClones - forcing the return of cloned objects to your map object
12051243
* @param {string} dataOptions.forceCloneMethod - Allows overriding the default or collection specified cloning method.
12061244
*/
1207-
map<U extends object>(mapFun: (obj: E, index: number, array: E[]) => U, dataOptions?: Resultset.DataOptions) : Resultset<U> {
1245+
map<U extends object>(mapFun: (obj: E, index: number, array: E[]) => U, dataOptions?: Resultset.DataOptions): Resultset<U> {
12081246
let data = this.data(dataOptions).map(mapFun);
12091247
//return return a new resultset with no filters
12101248
this.collection = new Collection("mappedData");

packages/loki/src/utils.ts

-49
This file was deleted.

0 commit comments

Comments
 (0)