UNPKG

8.61 kBJavaScriptView Raw
1
2/*
3 Loki (node) fs structured Adapter (need to require this script to instance and use it).
4
5 This adapter will save database container and each collection to separate files and
6 save collection only if it is dirty. It is also designed to use a destructured serialization
7 method intended to lower the memory overhead of json serialization.
8
9 This adapter utilizes ES6 generator/iterator functionality to stream output and
10 uses node linereader module to stream input. This should lower memory pressure
11 in addition to individual object serializations rather than loki's default deep object
12 serialization.
13*/
14
15(function (root, factory) {
16 if (typeof define === 'function' && define.amd) {
17 // AMD
18 define([], factory);
19 } else if (typeof exports === 'object') {
20 // Node, CommonJS-like
21 module.exports = factory();
22 } else {
23 // Browser globals (root is window)
24 root.LokiFsStructuredAdapter = factory();
25 }
26}(this, function () {
27 return (function() {
28
29 const fs = require('fs');
30 const readline = require('readline');
31 const stream = require('stream');
32
33 /**
34 * Loki structured (node) filesystem adapter class.
35 * This class fulfills the loki 'reference' abstract adapter interface which can be applied to other storage methods.
36 *
37 * @constructor LokiFsStructuredAdapter
38 *
39 */
40 function LokiFsStructuredAdapter()
41 {
42 this.mode = "reference";
43 this.dbref = null;
44 this.dirtyPartitions = [];
45 }
46
47 /**
48 * Generator for constructing lines for file streaming output of db container or collection.
49 *
50 * @param {object=} options - output format options for use externally to loki
51 * @param {int=} options.partition - can be used to only output an individual collection or db (-1)
52 *
53 * @returns {string|array} A custom, restructured aggregation of independent serializations.
54 * @memberof LokiFsStructuredAdapter
55 */
56 LokiFsStructuredAdapter.prototype.generateDestructured = function*(options) {
57 var idx, sidx;
58 var dbcopy;
59
60 options = options || {};
61
62 if (!options.hasOwnProperty("partition")) {
63 options.partition = -1;
64 }
65
66 // if partition is -1 we will return database container with no data
67 if (options.partition === -1) {
68 // instantiate lightweight clone and remove its collection data
69 dbcopy = this.dbref.copy();
70
71 for(idx=0; idx < dbcopy.collections.length; idx++) {
72 dbcopy.collections[idx].data = [];
73 }
74
75 yield dbcopy.serialize({
76 serializationMethod: "normal"
77 });
78
79 return;
80 }
81
82 // 'partitioned' along with 'partition' of 0 or greater is a request for single collection serialization
83 if (options.partition >= 0) {
84 var doccount,
85 docidx;
86
87 // dbref collections have all data so work against that
88 doccount = this.dbref.collections[options.partition].data.length;
89
90 for(docidx=0; docidx<doccount; docidx++) {
91 yield JSON.stringify(this.dbref.collections[options.partition].data[docidx]);
92 }
93 }
94 };
95
96 /**
97 * Loki persistence adapter interface function which outputs un-prototype db object reference to load from.
98 *
99 * @param {string} dbname - the name of the database to retrieve.
100 * @param {function} callback - callback should accept string param containing db object reference.
101 * @memberof LokiFsStructuredAdapter
102 */
103 LokiFsStructuredAdapter.prototype.loadDatabase = function(dbname, callback)
104 {
105 var instream,
106 outstream,
107 rl,
108 self=this;
109
110 this.dbref = null;
111
112 // make sure file exists
113 fs.stat(dbname, function (err, stats) {
114 if (!err && stats.isFile()) {
115 instream = fs.createReadStream(dbname);
116 outstream = new stream();
117 rl = readline.createInterface(instream, outstream);
118
119 // first, load db container component
120 rl.on('line', function(line) {
121 // it should single JSON object (a one line file)
122 if (self.dbref === null && line !== "") {
123 self.dbref = JSON.parse(line);
124 }
125 });
126
127 // when that is done, examine its collection array to sequence loading each
128 rl.on('close', function() {
129 if (self.dbref.collections.length > 0) {
130 self.loadNextCollection(dbname, 0, function() {
131 callback(self.dbref);
132 });
133 }
134 });
135 }
136 else {
137 // file does not exist, so callback with null
138 callback(null);
139 }
140 });
141 };
142
143 /**
144 * Recursive function to chain loading of each collection one at a time.
145 * If at some point i can determine how to make async driven generator, this may be converted to generator.
146 *
147 * @param {string} dbname - the name to give the serialized database within the catalog.
148 * @param {int} collectionIndex - the ordinal position of the collection to load.
149 * @param {function} callback - callback to pass to next invocation or to call when done
150 * @memberof LokiFsStructuredAdapter
151 */
152 LokiFsStructuredAdapter.prototype.loadNextCollection = function(dbname, collectionIndex, callback) {
153 var instream = fs.createReadStream(dbname + "." + collectionIndex);
154 var outstream = new stream();
155 var rl = readline.createInterface(instream, outstream);
156 var self=this,
157 obj;
158
159 rl.on('line', function (line) {
160 if (line !== "") {
161 obj = JSON.parse(line);
162 self.dbref.collections[collectionIndex].data.push(obj);
163 }
164 });
165
166 rl.on('close', function (line) {
167 instream = null;
168 outstream = null;
169 rl = null;
170 obj = null;
171
172 // if there are more collections, load the next one
173 if (++collectionIndex < self.dbref.collections.length) {
174 self.loadNextCollection(dbname, collectionIndex, callback);
175 }
176 // otherwise we are done, callback to loadDatabase so it can return the new db object representation.
177 else {
178 callback();
179 }
180 });
181 };
182
183 /**
184 * Generator for yielding sequence of dirty partition indices to iterate.
185 *
186 * @memberof LokiFsStructuredAdapter
187 */
188 LokiFsStructuredAdapter.prototype.getPartition = function*() {
189 var idx,
190 clen = this.dbref.collections.length;
191
192 // since database container (partition -1) doesn't have dirty flag at db level, always save
193 yield -1;
194
195 // yield list of dirty partitions for iterateration
196 for(idx=0; idx<clen; idx++) {
197 if (this.dbref.collections[idx].dirty) {
198 yield idx;
199 }
200 }
201 };
202
203 /**
204 * Loki reference adapter interface function. Saves structured json via loki database object reference.
205 *
206 * @param {string} dbname - the name to give the serialized database within the catalog.
207 * @param {object} dbref - the loki database object reference to save.
208 * @param {function} callback - callback passed obj.success with true or false
209 * @memberof LokiFsStructuredAdapter
210 */
211 LokiFsStructuredAdapter.prototype.exportDatabase = function(dbname, dbref, callback)
212 {
213 var idx;
214
215 this.dbref = dbref;
216
217 // create (dirty) partition generator/iterator
218 var pi = this.getPartition();
219
220 this.saveNextPartition(dbname, pi, function() {
221 callback(null);
222 });
223
224 };
225
226 /**
227 * Utility method for queueing one save at a time
228 */
229 LokiFsStructuredAdapter.prototype.saveNextPartition = function(dbname, pi, callback) {
230 var li;
231 var filename;
232 var self = this;
233 var pinext = pi.next();
234
235 if (pinext.done) {
236 callback();
237 return;
238 }
239
240 // db container (partition -1) uses just dbname for filename,
241 // otherwise append collection array index to filename
242 filename = dbname + ((pinext.value === -1)?"":("." + pinext.value));
243
244 var wstream = fs.createWriteStream(filename);
245 //wstream.on('finish', function() {
246 wstream.on('close', function() {
247 self.saveNextPartition(dbname, pi, callback);
248 });
249
250 li = this.generateDestructured({ partition: pinext.value });
251
252 // iterate each of the lines generated by generateDestructured()
253 for(var outline of li) {
254 wstream.write(outline + "\n");
255 }
256
257 wstream.end();
258 };
259
260 return LokiFsStructuredAdapter;
261
262 }());
263}));