-
Notifications
You must be signed in to change notification settings - Fork 59
/
data-store.js
255 lines (218 loc) · 7.6 KB
/
data-store.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import { isBefore } from '../util/date-time';
// An array-like collection of objects. Objects may be created, read, sorted,
// and deleted. The objects are ordered within the collection in order of
// creation.
class Collection {
/* eslint-disable no-unused-vars */
// Generates an object and adds it to the collection. If the object has a
// createdAt property, it will be in the past. Returns the collection for
// chaining.
createPast(count, options = undefined) { throw new Error('not implemented'); }
// Generates an object and adds it to the collection. If the object has a
// createdAt property, it will be set to the current time. Returns the new
// object.
createNew(options = undefined) { throw new Error('not implemented'); }
// Returns the number of objects in the store.
get size() { throw new Error('not implemented'); }
// Returns the object at the specified index. The index may be negative.
get(index) { throw new Error('not implemented'); }
// Returns the objects of the collection as an Array that is sorted in some
// way (in order of creation or otherwise).
sorted() { throw new Error('not implemented'); }
splice(start, deleteCount = undefined) { throw new Error('not implemented'); }
/* eslint-enable no-unused-vars */
first() { return this.get(0); }
last() { return this.get(-1); }
// Returns an iterator that iterates over the objects of the collection in
// creation order.
[Symbol.iterator]() {
let i = 0;
return {
next: () => {
if (i >= this.size) return { done: true };
const result = { value: this.get(i), done: false };
i += 1;
return result;
}
};
}
entries() {
let i = 0;
return {
next: () => {
if (i >= this.size) return { done: true };
const result = { value: [i, this.get(i)], done: false };
i += 1;
return result;
},
[Symbol.iterator]() { return this; }
};
}
filter(f) {
const result = [];
for (const [i, obj] of this.entries()) {
if (f(obj, i)) result.push(obj);
}
return result;
}
findIndex(f) {
for (const [i, obj] of this.entries()) {
if (f(obj, i)) return i;
}
return -1;
}
findLastIndex(f) {
for (let i = this.size - 1; i >= 0; i -= 1) {
if (f(this.get(i), i)) return i;
}
return -1;
}
find(f) {
const index = this.findIndex(f);
return index !== -1 ? this.get(index) : null;
}
findLast(f) {
const index = this.findLastIndex(f);
return index !== -1 ? this.get(index) : null;
}
}
// Implements the methods of Collection.
class Store extends Collection {
constructor({
// A factory function that generates objects for the store. createPast() and
// createNew() pass arguments to the factory; see _create() for details.
factory,
// A compare function that sorted() will use to sort the objects in the
// store
sort = undefined
}) {
super();
// When we run the callback later, we do not want it to be bound to the
// Store.
this._factory = factory.bind(null);
this._compareFunction = sort;
this.reset();
}
reset() {
this._objects = [];
this._id = 0;
this._lastCreatedAt = null;
}
createPast(count, options = undefined) {
for (let i = 0; i < count; i += 1)
this._create(true, options);
return this;
}
createNew(options = undefined) {
return this._create(false, options);
}
_create(inPast, options = undefined) {
this._id += 1;
const object = this._factory({
...options,
// We always pass the following arguments to the factory, but they might
// not be relevant to all factories.
// inPast is relevant to factories that return objects with a createdAt
// property. If inPast is `true`, the createdAt property of the resulting
// object should be in the past. If it is `false`, its createdAt property
// should be set to the current time.
inPast,
// id is relevant to factories that return objects with an `id` property.
id: this._id,
// lastCreatedAt is relevant to factories that return objects with a
// createdAt property. The createdAt property of the resulting object
// should be greater than or equal to lastCreatedAt.
lastCreatedAt: this._lastCreatedAt
});
if (object.createdAt != null && this._lastCreatedAt != null &&
isBefore(object.createdAt, this._lastCreatedAt))
throw new Error('invalid createdAt');
this._lastCreatedAt = object.createdAt;
this._objects.push(object);
return object;
}
get size() { return this._objects.length; }
get(index) {
if (index >= 0) return this._objects[index];
return index >= -this._objects.length
? this._objects[this._objects.length + index]
: undefined;
}
sorted() {
const copy = [...this._objects];
if (this._compareFunction != null) copy.sort(this._compareFunction);
return copy;
}
splice(start, deleteCount = undefined) {
return deleteCount == null
? this._objects.splice(start)
: this._objects.splice(start, deleteCount);
}
// Updates an object in the store, setting the properties specified by
// `props`. If the object has an updatedAt property, it will be set to the
// current time. Returns the updated object.
update(index, props = undefined) {
const normalizedIndex = index >= 0 ? index : this._objects.length + index;
if (normalizedIndex >= this._objects.length || normalizedIndex < 0)
throw new Error('invalid index');
if (props != null && 'createdAt' in props) {
// Objects are ordered in the store in order of creation. If the factory
// returns objects with a createdAt property, then the objects should also
// be ordered by createdAt. (The order by createdAt should match the
// actual order of creation.) Because of that, update() does not support
// updating an object's createdAt property.
throw new Error('createdAt cannot be updated');
}
const object = this._objects[normalizedIndex];
const updated = { ...object, ...props };
if ('updatedAt' in object && !(props != null && 'updatedAt' in props))
updated.updatedAt = new Date().toISOString();
this._objects[normalizedIndex] = updated;
return updated;
}
indexOf(obj) { return this.findIndex(o => o === obj); }
lastIndexOf(obj) { return this.findLastIndex(o => o === obj); }
}
// A view/transformation of a Store
class View extends Collection {
constructor(store, transform) {
super();
this._store = store;
// When we run the callback later, we do not want it to be bound to the
// View.
this._transform = transform.bind(null);
}
createPast(count, options = undefined) {
this._store.createPast(count, options);
return this;
}
createNew(options = undefined) {
return this._transform(this._store.createNew(options));
}
get size() { return this._store.size; }
get(index) {
const object = this._store.get(index);
return object !== undefined ? this._transform(object) : undefined;
}
sorted() {
const sorted = this._store.sorted();
for (let i = 0; i < sorted.length; i += 1)
sorted[i] = this._transform(sorted[i]);
return sorted;
}
splice(start, deleteCount = undefined) {
return this._store.splice(start, deleteCount)
.map(object => this._transform(object));
}
}
const stores = [];
export const dataStore = (options) => {
const store = new Store(options);
stores.push(store);
return store;
};
export const view = (store, transform) => new View(store, transform);
export const resetDataStores = () => {
for (const store of stores)
store.reset();
};