Skip to content

Commit 767a7fd

Browse files
committed
feat(model): add factory function, shareReplay perf, tests
- add factory function with config object which supports all config params - use shareReplay({ bufferSize: 1, refCount: true }) to prevent memory leaks - add tests - update docs
1 parent 65c933d commit 767a7fd

File tree

4 files changed

+100
-50
lines changed

4 files changed

+100
-50
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Multiple model factories are provided out of the box to support different use ca
9696
- `createMutable(initialData: T): Model<T>` - create model with no immutability guarantees (you have to make sure that model consumers don't mutate and corrupt model state) but much more performance because whole cloning step is skipped
9797
- `createMutableWithSharedSubscription(initialData: T): Model<T>` - gain even more performance by skipping both immutability and sharing subscription between all consumers (eg situation in which many components are subscribed to single model)
9898
- `createWithCustomClone(initialData: T, clone: (data: T) => T)` - create immutable model by passing your custom clone function (`JSON` cloning doesn't support properties containing function or regex so custom cloning functionality might be needed)
99+
- `createWithConfig(config)` - create model by passing in config object with values of all configuration properties (`initialData: T`, `immutable: boolean`, `sharedSubscription: boolean`, `clone: (data: T) => T`)
99100
100101
## Model Schematics API
101102

lib/model/model.test.ts

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,80 +6,91 @@ import { ModelFactory } from './model';
66
const modelFactory = new ModelFactory<TestModel>();
77

88
describe('Model', () => {
9-
it('should be an immutable array', () => {
10-
const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();
9+
it('should expose model data in observable', () => {
10+
const model = modelFactory.create({ value: 'test' });
1111

12-
const initial: TestModel[] = [];
13-
const store = factory.create(initial);
12+
model.data$.subscribe(data =>
13+
assert.deepStrictEqual(data, { value: 'test' })
14+
);
15+
});
1416

15-
const initialState = store.get();
16-
initialState.push({ value: 'updated' });
17+
it('should expose raw data getter', () => {
18+
const model = modelFactory.create({ value: 'test' });
1719

18-
assert.equal(store.get().length, 0);
20+
assert.deepStrictEqual(model.get(), { value: 'test' });
1921
});
2022

21-
it('should be an immutable array after set', () => {
22-
const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();
23-
24-
const initial: TestModel[] = [];
25-
const store = factory.create(initial);
23+
it('should expose raw data setter', () => {
24+
const model = modelFactory.create(<TestModel>{ value: 'test' });
2625

27-
const updateArray = [{ value: 'first element' }];
28-
store.set(updateArray);
26+
model.set({ value: 'changed' });
2927

30-
updateArray.push({ value: '2nd element' });
31-
assert.equal(store.get().length, 1);
28+
assert.deepStrictEqual(model.get(), { value: 'changed' });
3229
});
3330

34-
it('should be immutable array on sub', () => {
35-
const factory: ModelFactory<TestModel[]> = new ModelFactory<TestModel[]>();
36-
37-
const initial: TestModel[] = [];
38-
const store = factory.create(initial);
39-
40-
const initialState = store.get();
41-
initialState.push({ value: 'updated' });
31+
it('should use immutable data in exposed observable by default', () => {
32+
const model = modelFactory.create({ value: 'test' });
4233

43-
store.data$.subscribe(v => {
44-
assert.equal(store.get().length, 0);
34+
model.data$.subscribe(data => {
35+
data.value = 'changed';
36+
assert.deepStrictEqual(model.get(), { value: 'test' });
4537
});
4638
});
4739

48-
it('should expose model data in observable', () => {
40+
it('should use immutable data in model by default (get)', () => {
4941
const model = modelFactory.create({ value: 'test' });
5042

51-
model.data$.subscribe(data => assert.deepEqual(data, { value: 'test' }));
43+
const modelData = model.get();
44+
modelData.value = 'changed';
45+
46+
model.data$.subscribe(data => {
47+
assert.deepStrictEqual(data, { value: 'test' });
48+
});
5249
});
5350

54-
it('should expose raw data getter', () => {
51+
it('should use immutable data in model by default (set)', () => {
5552
const model = modelFactory.create({ value: 'test' });
5653

57-
assert.deepEqual(model.get(), { value: 'test' });
58-
});
54+
const changedData = { value: 'changed' };
55+
model.set(changedData);
5956

60-
it('should expose raw data setter', () => {
61-
const model = modelFactory.create(<TestModel>{ value: 'test' });
57+
changedData.value = 'changed even more';
6258

63-
model.set({ value: 'changed' });
64-
65-
assert.deepEqual(model.get(), { value: 'changed' });
59+
model.data$.subscribe(data => {
60+
assert.deepStrictEqual(data, { value: 'changed' });
61+
});
6662
});
6763

68-
it('should use immutable data in exposed observable by default', () => {
69-
const model = modelFactory.create({ value: 'test' });
64+
it('should use mutable data in exposed observable when configured', () => {
65+
const model = modelFactory.createMutable({ value: 'test' });
7066

7167
model.data$.subscribe(data => {
7268
data.value = 'changed';
73-
assert.deepEqual(model.get(), { value: 'test' });
69+
assert.deepStrictEqual(model.get(), { value: 'changed' });
7470
});
7571
});
7672

77-
it('should use mutable data in exposed observable when configured', () => {
73+
it('should use mutable data in model (get) when configured', () => {
7874
const model = modelFactory.createMutable({ value: 'test' });
7975

76+
const modelData = model.get();
77+
modelData.value = 'changed';
78+
8079
model.data$.subscribe(data => {
81-
data.value = 'changed';
82-
assert.deepEqual(model.get(), { value: 'changed' });
80+
assert.deepStrictEqual(data, { value: 'changed' });
81+
});
82+
});
83+
84+
it('should use mutable data in model (set) when configured', () => {
85+
const model = modelFactory.createMutable({ value: 'test' });
86+
87+
const changedData = { value: 'changed' };
88+
model.set(changedData);
89+
90+
changedData.value = 'changed even more';
91+
92+
model.data$.subscribe(data => {
93+
assert.deepStrictEqual(data, { value: 'changed even more' });
8394
});
8495
});
8596

@@ -96,15 +107,41 @@ describe('Model', () => {
96107
});
97108
});
98109

110+
it('should use custom clone function when configured (get)', () => {
111+
const cloneSpy = sinon.spy();
112+
const model = modelFactory.createWithCustomClone(
113+
{ value: 'test' },
114+
cloneSpy
115+
);
116+
117+
model.get();
118+
sinon.assert.calledOnce(cloneSpy);
119+
sinon.assert.calledWith(cloneSpy, { value: 'test' });
120+
});
121+
122+
it('should use custom clone function when configured (set)', () => {
123+
const cloneSpy = sinon.spy();
124+
const model = modelFactory.createWithCustomClone(
125+
{ value: 'test' },
126+
cloneSpy
127+
);
128+
129+
model.set({ value: 'changed' });
130+
sinon.assert.calledOnce(cloneSpy);
131+
sinon.assert.calledWith(cloneSpy, { value: 'changed' });
132+
});
133+
99134
it('should create multiple independent instances', () => {
100135
const model1 = modelFactory.create({ value: 'test1' });
101136
const model2 = modelFactory.create({ value: 'test2' });
102137

103138
model2.set({ value: 'changed' });
104139

105-
model1.data$.subscribe(data => assert.deepEqual(data, { value: 'test1' }));
140+
model1.data$.subscribe(data =>
141+
assert.deepStrictEqual(data, { value: 'test1' })
142+
);
106143
model2.data$.subscribe(data =>
107-
assert.deepEqual(data, { value: 'changed' })
144+
assert.deepStrictEqual(data, { value: 'changed' })
108145
);
109146
});
110147

@@ -113,10 +150,10 @@ describe('Model', () => {
113150

114151
model.data$.subscribe(data => {
115152
data.value = 'changed';
116-
assert.deepEqual(data, { value: 'changed' });
153+
assert.deepStrictEqual(data, { value: 'changed' });
117154
});
118155
model.data$.subscribe(data => {
119-
assert.deepEqual(data, { value: 'test' });
156+
assert.deepStrictEqual(data, { value: 'test' });
120157
});
121158
});
122159

@@ -127,10 +164,10 @@ describe('Model', () => {
127164

128165
model.data$.subscribe(data => {
129166
data.value = 'changed';
130-
assert.deepEqual(data, { value: 'changed' });
167+
assert.deepStrictEqual(data, { value: 'changed' });
131168
});
132169
model.data$.subscribe(data => {
133-
assert.deepEqual(data, { value: 'changed' });
170+
assert.deepStrictEqual(data, { value: 'changed' });
134171
});
135172
});
136173
});

lib/model/model.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export class Model<T> {
2222
: JSON.parse(JSON.stringify(data))
2323
: data
2424
),
25-
sharedSubscription ? shareReplay(1) : map((data: T) => data)
25+
sharedSubscription
26+
? shareReplay({ bufferSize: 1, refCount: true })
27+
: map((data: T) => data)
2628
);
2729
}
2830

@@ -67,4 +69,14 @@ export class ModelFactory<T> {
6769
createWithCustomClone(initialData: T, clone: (data: T) => T) {
6870
return new Model<T>(initialData, true, false, clone);
6971
}
72+
73+
createWithConfig(config: {
74+
initialData: T;
75+
immutable: boolean;
76+
sharedSubscription: boolean;
77+
clone: (data: T) => T;
78+
}) {
79+
const { initialData, immutable, sharedSubscription, clone } = config;
80+
return new Model<T>(initialData, immutable, sharedSubscription, clone);
81+
}
7082
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build": "npm run clean && npm run lib:build && npm run schm:build",
1111
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
1212
"release": "npm run test && standard-version && git push --follow-tags origin master && npm run build && npm publish ./dist --access public",
13-
"format": "prettier **/*.{ts,json,md} --write",
13+
"format:write": "prettier **/*.{ts,json,md} --write",
1414
"format:test": "prettier **/*.{ts,json,md} --list-different",
1515
"lib:build": "ng-packagr -p package.json",
1616
"lib:test": "mocha lib/model/model.test.ts --require ts-node/register",

0 commit comments

Comments
 (0)