-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathindex.js
257 lines (223 loc) · 6.33 KB
/
index.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
256
257
'use strict';
var utils = require('./pouch-utils'); // TODO: is it ok that this causes warnings with uglifyjs??
var Promise = utils.Promise;
var events = require('events');
function empty(obj) {
for (var i in obj) { // jshint unused:false
return false;
}
return true;
}
function isString(obj) {
return typeof obj === 'string' || obj instanceof String;
}
function isNumeric(obj) {
return !isNaN(obj);
}
function notDefined(obj) {
return typeof obj === 'undefined';
}
exports.delta = new events.EventEmitter();
exports.deltaInit = function () {
this.on('create', function (object) {
onCreate(this, object);
});
this.on('destroyed', function () {
onDestroyed(this);
});
};
exports.clone = function (obj) {
return JSON.parse(JSON.stringify(obj));
};
exports.merge = function (obj1, obj2) {
var merged = {};
for (var i in obj1) {
merged[i] = obj1[i];
}
for (i in obj2) {
merged[i] = obj2[i];
}
return merged;
};
function save(db, doc) {
delete(doc._rev); // delete any revision numbers copied from previous docs
doc.$createdAt = (new Date()).toJSON();
if (doc.$id) { // update?
// this format guarantees the docs will be retrieved in order they were created
doc._id = doc.$id + '_' + doc.$createdAt;
return db.put(doc).then(function (response) {
response.$id = doc.$id;
return response;
}).catch(/* istanbul ignore next */ function (err) {
// It appears there is a bug in pouch that causes a doc conflict even though we are creating a
// new doc
if (err.status !== 409) {
throw err;
}
});
} else { // new
return db.post(doc).then(function (response) {
response.$id = response.id;
return response;
});
}
}
exports.save = function (doc) {
return save(this, doc);
};
exports.delete = function (docOrId) {
var id = isString(docOrId) || isNumeric(docOrId) ? docOrId : docOrId.$id;
if (notDefined(id)) {
throw new Error('missing $id');
}
return save(this, {$id: id, $deleted: true});
};
exports.all = function () {
var db = this;
var docs = {},
deletions = {};
return db.allDocs({include_docs: true}).then(function (doc) {
doc.rows.forEach(function (el) {
if (!el.doc.$id) { // first delta for doc?
el.doc.$id = el.doc._id;
}
if (el.doc.$deleted) { // deleted?
delete(docs[el.doc.$id]);
deletions[el.doc.$id] = true;
} else if (!deletions[el.doc.$id]) { // update before any deletion?
if (docs[el.doc.$id]) { // exists?
docs[el.doc.$id] = exports.merge(docs[el.doc.$id], el.doc);
} else {
docs[el.doc.$id] = el.doc;
}
}
});
return docs;
});
};
var deletions = {};
exports.wasDeleted = function (id) {
return deletions[id] ? true : false;
};
exports.markDeletion = function (id) {
deletions[id] = true;
};
function onCreate(db, object) {
db.get(object.id).then(function (doc) {
var id = doc.$id ? doc.$id : doc._id;
if (!exports.wasDeleted(id)) { // not previously deleted?
if (doc.$deleted) { // deleted?
exports.markDeletion(id);
exports.delta.emit('delete', id);
} else if (doc.$id) { // update?
exports.delta.emit('update', doc);
} else {
doc.$id = id;
exports.delta.emit('create', doc);
}
}
});
}
function onDestroyed(db) {
db.delta.removeAllListeners();
}
function getChanges(oldDoc, newDoc) {
var changes = {}, change = false;
for (var i in newDoc) {
if (oldDoc[i] !== newDoc[i]) {
change = true;
changes[i] = newDoc[i];
}
}
return change ? changes : null;
}
exports.saveChanges = function (oldDoc, newDoc) {
var db = this, changes = getChanges(oldDoc, newDoc);
if (changes !== null) {
changes.$id = oldDoc.$id;
return db.save(changes).then(function () {
return changes;
});
}
return Promise.resolve();
};
function getAndRemove(db, id) {
return db.get(id).then(function (object) {
return db.remove(object);
}).catch(function (err) {
// If the doc isn't found, no biggie. Else throw.
/* istanbul ignore if */
if (err.status !== 404) {
throw err;
}
});
}
exports.getAndRemove = function (id) {
return getAndRemove(this, id);
};
/*
* We need a second pass for deletions as client 1 may delete and then
* client 2 updates afterwards
* e.g. {id: 1, title: 'one'}, {$id: 1, $deleted: true}, {$id: 1, title: 'two'}
*/
function removeDeletions(db, doc, deletions) {
var promises = [];
doc.rows.forEach(function (el) {
if (deletions[el.doc.$id]) { // deleted?
promises.push(getAndRemove(db, el.id));
}
});
// promise shouldn't resolve until all deletions have completed
return Promise.all(promises);
}
function cleanupDoc(db, el, docs, deletions) {
return db.get(el.doc._id).then(function (object) {
if (!el.doc.$id) { // first delta for doc?
el.doc.$id = el.doc._id;
}
if (el.doc.$deleted || deletions[el.doc.$id]) { // deleted?
deletions[el.doc.$id] = true;
return db.remove(object);
} else if (docs[el.doc.$id]) { // exists?
var undef = false;
for (var k in el.doc) {
if (typeof docs[el.doc.$id][k] === 'undefined') {
undef = true;
break;
}
}
if (undef) {
docs[el.doc.$id] = exports.merge(docs[el.doc.$id], el.doc);
} else { // duplicate update, remove
return db.remove(object);
}
} else {
docs[el.doc.$id] = el.doc;
}
});
}
// TODO: also create fn like noBufferCleanup that uses REST to cleanup??
// This way can use timestamp so not cleaning same range each time
exports.cleanup = function () {
var db = this;
return db.allDocs({ include_docs: true }).then(function (doc) {
var docs = {}, deletions = {}, chain = Promise.resolve();
// reverse sort by createdAt
doc.rows.sort(function (a, b) {
return a.doc.$createdAt < b.doc.$createdAt;
});
// The cleanupDoc() calls must execute in sequential order
doc.rows.forEach(function (el) {
chain = chain.then(function () { return cleanupDoc(db, el, docs, deletions); });
});
return chain.then(function () {
if (!empty(deletions)) {
return removeDeletions(db, doc, deletions);
}
});
});
};
/* istanbul ignore next */
if (typeof window !== 'undefined' && window.PouchDB) {
window.PouchDB.plugin(exports);
}