Skip to content

Commit c79b9c2

Browse files
Almost complete test coverage
1 parent 39a62c9 commit c79b9c2

File tree

11 files changed

+414
-183
lines changed

11 files changed

+414
-183
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
22
built
3+
coverage

.vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "node",
99
"request": "launch",
1010
"name": "Launch Program",
11-
"program": "${workspaceRoot}/built/test/arrays.js",
11+
"program": "${workspaceRoot}/built/test/polymorph.js",
1212
"cwd": "${workspaceRoot}",
1313
"outFiles": []
1414
},

fix-coverage.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const fs = require("fs"),
2+
path = require("path"),
3+
under = "built/src",
4+
prefixes = ["var __decorate =", "var __assign ="];
5+
6+
// tell istanbul to ignore TS-generated decorator code
7+
8+
fs.readdirSync(under).forEach(file => {
9+
file = path.join(under, file);
10+
let src = fs.readFileSync(file, "utf8");
11+
prefixes.forEach(prefix => {
12+
src = src.replace(prefix, "/* istanbul ignore next */\n" + prefix);
13+
});
14+
fs.writeFileSync(file, src);
15+
});

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "json-mobx",
3-
"version": "0.6.0",
3+
"version": "0.6.1",
44
"description": "Simple undo/redo and persistence for MobX",
55
"main": "built/index.js",
66
"types": "built/index.d.ts",
77
"scripts": {
88
"build": "tsc",
99
"test": "tape built/test/**/*.js",
10-
"coverage": "istanbul cover tape built/test/**/*.js",
10+
"coverage": "node fix-coverage.js && istanbul cover tape built/test/**/*.js",
1111
"prepublish": "npm run build && npm run test",
1212
"all": "npm run build && npm run test && npm run coverage"
1313
},

src/array.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { getOrCreateComputed, isArray, save, load } from "./core";
2+
3+
import { observable } from "mobx";
4+
import { Disposable } from "./Disposable";
5+
6+
const arrayItemIdKey = "<id>";
7+
8+
export function getArrayItemId(item: any) {
9+
return (item && typeof item === "object" && item[arrayItemIdKey]) || 0;
10+
}
11+
12+
function setArrayItemId(item: any, id: number) {
13+
if (item && typeof item === "object") {
14+
item[arrayItemIdKey] = id;
15+
}
16+
}
17+
18+
function setArrayItemIds(ar: any[]) {
19+
let nextId = 1;
20+
const usedIds: { [id: string]: boolean } = {};
21+
22+
// First pass - clear IDs that are duplicates
23+
for (const item of ar) {
24+
const id = getArrayItemId(item);
25+
if (id) {
26+
nextId = Math.max(nextId, id + 1);
27+
28+
if (usedIds[id]) {
29+
setArrayItemId(item, 0);
30+
} else {
31+
usedIds[id] = true;
32+
}
33+
}
34+
}
35+
36+
// Second pass - allocate IDs
37+
for (const item of ar) {
38+
const id = getArrayItemId(item);
39+
if (!id) {
40+
setArrayItemId(item, nextId++);
41+
}
42+
}
43+
}
44+
45+
function saveArrayItem(item: any) {
46+
return { ...save(item), [arrayItemIdKey]: getArrayItemId(item) };
47+
}
48+
49+
function getArrayJsonComputed(ar: any[], itemFactory: () => any) {
50+
51+
return getOrCreateComputed(ar, "<json>", () => ({
52+
53+
get() {
54+
setArrayItemIds(ar);
55+
return ar.map(saveArrayItem);
56+
},
57+
58+
set(data: any) {
59+
if (!isArray(data)) {
60+
ar.length = 0; // most likely schema has changed
61+
return;
62+
}
63+
64+
// Build map of existing items by ID
65+
const existing: { [id: string]: any } = {};
66+
for (const item of ar) {
67+
const id = getArrayItemId(item);
68+
if (!existing[id]) {
69+
existing[id] = item;
70+
}
71+
}
72+
73+
// Bring into line with supplied data
74+
ar.length = data.length;
75+
76+
for (let i = 0; i < data.length; i++) {
77+
const itemJson = data[i];
78+
const itemId = getArrayItemId(itemJson);
79+
80+
// Reuse existing item with same id
81+
let item = existing[itemId];
82+
if (item) {
83+
delete existing[itemId];
84+
} else {
85+
item = itemFactory();
86+
setArrayItemId(item, itemId);
87+
}
88+
89+
load(item, itemJson);
90+
ar[i] = item;
91+
}
92+
93+
// Dispose any items not reused
94+
for (const key of Object.keys(existing)) {
95+
const item = existing[key];
96+
if (item.dispose) {
97+
item.dispose();
98+
}
99+
}
100+
}
101+
}));
102+
}
103+
104+
export function array<T extends Partial<Disposable>>(factory: () => T): T[] {
105+
106+
const result: T[] = observable([]);
107+
108+
Object.defineProperty(result, "json", {
109+
get(this: any) {
110+
return getArrayJsonComputed(this, factory).get();
111+
},
112+
set(this: any, data: any) {
113+
getArrayJsonComputed(this, factory).set(data);
114+
}
115+
});
116+
117+
return result;
118+
}
119+
120+
export function arrayOf<T extends Partial<Disposable>>(ctor: new() => T): T[] {
121+
return array(() => new ctor());
122+
}

src/core.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { computed, IComputedValue, isObservableArray } from "mobx";
2+
3+
export function getOrCreateComputed(obj: any, key: string, options: () => { get(): any; set(data: any): void }) {
4+
let result: IComputedValue<any>;
5+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
6+
const { get, set } = options();
7+
obj[key] = result = computed(get, set);
8+
} else {
9+
result = obj[key];
10+
}
11+
return result;
12+
}
13+
14+
export function isArray(obj: any) {
15+
return Array.isArray(obj) || isObservableArray(obj);
16+
}
17+
18+
export function hasJsonProperty(obj: any) {
19+
return obj && typeof obj === "object" && ("json" in obj);
20+
}
21+
22+
export function canLoadInto(obj: any) {
23+
return hasJsonProperty(obj) || (obj && isArray(obj));
24+
}
25+
26+
export function save(obj: any): any {
27+
return hasJsonProperty(obj) ? obj.json : obj;
28+
}
29+
30+
export function load(obj: any, data: any) {
31+
if (data === "undefined" || !obj) {
32+
return;
33+
}
34+
35+
if (!canLoadInto(obj)) {
36+
throw new Error("Can only load JSON into an object with a json property, or an array");
37+
}
38+
39+
if (hasJsonProperty(obj)) {
40+
obj.json = data;
41+
return;
42+
}
43+
44+
if (isArray(obj)) {
45+
// Plain array data, so just replace everything
46+
if (isArray(data)) {
47+
obj.splice.apply(obj, [0, obj.length].concat(data));
48+
} else {
49+
obj.length = 0;
50+
}
51+
return;
52+
}
53+
}

0 commit comments

Comments
 (0)