@activeledger/activestorage 4.5.13 → 4.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/es/cache.d.ts +18 -0
- package/es/cache.d.ts.map +1 -0
- package/es/cache.js +57 -0
- package/es/cache.js.map +1 -0
- package/es/datastore.d.ts +11 -0
- package/es/datastore.d.ts.map +1 -0
- package/es/datastore.js +67 -0
- package/es/datastore.js.map +1 -0
- package/es/driver.d.ts +15 -0
- package/es/driver.d.ts.map +1 -0
- package/es/driver.js +2 -0
- package/es/driver.js.map +1 -0
- package/es/drivers/leveldb.d.ts +20 -0
- package/es/drivers/leveldb.d.ts.map +1 -0
- package/es/drivers/leveldb.js +121 -0
- package/es/drivers/leveldb.js.map +1 -0
- package/es/drivers/rocksdb.d.ts +20 -0
- package/es/drivers/rocksdb.d.ts.map +1 -0
- package/es/drivers/rocksdb.js +98 -0
- package/es/drivers/rocksdb.js.map +1 -0
- package/es/heartbeat.d.ts +6 -0
- package/es/heartbeat.d.ts.map +1 -0
- package/es/heartbeat.js +15 -0
- package/es/heartbeat.js.map +1 -0
- package/es/index.d.ts +3 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +3 -0
- package/es/index.js.map +1 -0
- package/es/levelme.d.ts +82 -0
- package/es/levelme.d.ts.map +1 -0
- package/es/levelme.js +476 -0
- package/es/levelme.js.map +1 -0
- package/es/newlinestream.d.ts +3 -0
- package/es/newlinestream.d.ts.map +1 -0
- package/es/newlinestream.js +28 -0
- package/es/newlinestream.js.map +1 -0
- package/es/selfhost.d.ts +2 -0
- package/es/selfhost.d.ts.map +1 -0
- package/es/selfhost.js +479 -0
- package/es/selfhost.js.map +1 -0
- package/es/sse.d.ts +10 -0
- package/es/sse.d.ts.map +1 -0
- package/es/sse.js +41 -0
- package/es/sse.js.map +1 -0
- package/lib/cache.d.ts +18 -0
- package/lib/cache.d.ts.map +1 -0
- package/lib/cache.js +62 -0
- package/lib/cache.js.map +1 -0
- package/lib/datastore.d.ts +11 -0
- package/lib/datastore.d.ts.map +1 -0
- package/lib/datastore.js +94 -0
- package/lib/datastore.js.map +1 -0
- package/lib/driver.d.ts +15 -0
- package/lib/driver.d.ts.map +1 -0
- package/lib/driver.js +3 -0
- package/lib/driver.js.map +1 -0
- package/lib/drivers/leveldb.d.ts +20 -0
- package/lib/drivers/leveldb.d.ts.map +1 -0
- package/lib/drivers/leveldb.js +125 -0
- package/lib/drivers/leveldb.js.map +1 -0
- package/lib/drivers/rocksdb.d.ts +20 -0
- package/lib/drivers/rocksdb.d.ts.map +1 -0
- package/lib/drivers/rocksdb.js +102 -0
- package/lib/drivers/rocksdb.js.map +1 -0
- package/lib/heartbeat.d.ts +6 -0
- package/lib/heartbeat.d.ts.map +1 -0
- package/lib/heartbeat.js +19 -0
- package/lib/heartbeat.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -0
- package/lib/levelme.d.ts +82 -0
- package/lib/levelme.d.ts.map +1 -0
- package/lib/levelme.js +503 -0
- package/lib/levelme.js.map +1 -0
- package/lib/newlinestream.d.ts +3 -0
- package/lib/newlinestream.d.ts.map +1 -0
- package/lib/newlinestream.js +31 -0
- package/lib/newlinestream.js.map +1 -0
- package/lib/selfhost.d.ts +2 -0
- package/lib/selfhost.d.ts.map +1 -0
- package/lib/selfhost.js +504 -0
- package/lib/selfhost.js.map +1 -0
- package/lib/sse.d.ts +10 -0
- package/lib/sse.d.ts.map +1 -0
- package/lib/sse.js +45 -0
- package/lib/sse.js.map +1 -0
- package/package.json +9 -5
- package/src/cache.ts +0 -72
- package/src/datastore.ts +0 -191
- package/src/driver.ts +0 -18
- package/src/drivers/leveldb.ts +0 -145
- package/src/drivers/rocksdb.ts +0 -118
- package/src/heartbeat.ts +0 -60
- package/src/index.ts +0 -25
- package/src/levelme.ts +0 -938
- package/src/newlinestream.ts +0 -32
- package/src/selfhost.ts +0 -1104
- package/src/sse.ts +0 -136
- package/src/types/rocksdb.d.ts +0 -1
- package/tsconfig.json +0 -8
- /package/{src → lib}/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/activeledger.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/fauxtonicon5.svg +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/fontawesome-webfont.svg +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/github.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/googleplus.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/linkedin.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/loader.gif +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/img/twitter.png +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +0 -0
- /package/{src → lib}/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +0 -0
- /package/{src → lib}/fauxton/index.html +0 -0
package/src/levelme.ts
DELETED
|
@@ -1,938 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import {
|
|
4
|
-
createWriteStream,
|
|
5
|
-
createReadStream,
|
|
6
|
-
} from "fs";
|
|
7
|
-
import { ActiveLogger } from "@activeledger/activelogger";
|
|
8
|
-
import { EventEmitter } from "events";
|
|
9
|
-
import { newLineTransform } from "./newlinestream";
|
|
10
|
-
import { ActiveCacheManager, ActiveCache } from "@activeledger/activeoptions";
|
|
11
|
-
import { ActiveClone } from "@activeledger/activeutilities";
|
|
12
|
-
import { IStorageDriver } from "./driver";
|
|
13
|
-
import { LevelDBDriver } from "./drivers/leveldb";
|
|
14
|
-
import { LevelUpChain } from "levelup";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Generic Data Document
|
|
18
|
-
*
|
|
19
|
-
* @interface document
|
|
20
|
-
*/
|
|
21
|
-
interface document {
|
|
22
|
-
_id: string;
|
|
23
|
-
_rev?: string | null;
|
|
24
|
-
[key: string]: any;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* All Doc searching options
|
|
29
|
-
*
|
|
30
|
-
* @interface allDocOptions
|
|
31
|
-
*/
|
|
32
|
-
interface allDocOptions {
|
|
33
|
-
startkey?: string;
|
|
34
|
-
endkey?: string;
|
|
35
|
-
limit?: number;
|
|
36
|
-
skip?: number;
|
|
37
|
-
keys?: string[];
|
|
38
|
-
include_docs?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* change doc options
|
|
43
|
-
*
|
|
44
|
-
* @interface changesOptions
|
|
45
|
-
*/
|
|
46
|
-
interface changesOptions {
|
|
47
|
-
since?: number | "now";
|
|
48
|
-
live?: boolean;
|
|
49
|
-
limit?: number;
|
|
50
|
-
descending?: boolean;
|
|
51
|
-
include_docs?: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Root document schema for tracking changes
|
|
56
|
-
*
|
|
57
|
-
* @interface schema
|
|
58
|
-
* @extends {document}
|
|
59
|
-
*/
|
|
60
|
-
interface schema extends document {
|
|
61
|
-
_rev: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const ENABLE_CACHE = true;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* LevelUP Wrapper for Activeledger with PouchDB legacy support
|
|
68
|
-
*
|
|
69
|
-
* @export
|
|
70
|
-
* @class LevelMe
|
|
71
|
-
*/
|
|
72
|
-
export class LevelMe {
|
|
73
|
-
/**
|
|
74
|
-
* PouchDb Legacy Support
|
|
75
|
-
* Database metadata key
|
|
76
|
-
*
|
|
77
|
-
* ÿ is xff unicode, Character code 255
|
|
78
|
-
*
|
|
79
|
-
* @private
|
|
80
|
-
* @static
|
|
81
|
-
*/
|
|
82
|
-
private static META_PREFIX = "ÿmeta-storeÿ"; // \xff charcode 255
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* PouchDb Legacy Support
|
|
86
|
-
* Document metadata key
|
|
87
|
-
*
|
|
88
|
-
* ÿ is xff unicode, Character code 255
|
|
89
|
-
*
|
|
90
|
-
* @private
|
|
91
|
-
* @static
|
|
92
|
-
*/
|
|
93
|
-
private static DOC_PREFIX = "ÿdocument-storeÿ";
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* PouchDb Legacy Support
|
|
97
|
-
* Database value key
|
|
98
|
-
*
|
|
99
|
-
* ÿ is xff unicode, Character code 255
|
|
100
|
-
*
|
|
101
|
-
* ÿby-sequenceÿ0000000000000110
|
|
102
|
-
*
|
|
103
|
-
* @private
|
|
104
|
-
* @static
|
|
105
|
-
*/
|
|
106
|
-
private static SEQ_PREFIX = "ÿby-sequenceÿ";
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Warning: Not backwards compatible
|
|
110
|
-
* Different way to store sequence documents. Its like an index but without needing the indexers.
|
|
111
|
-
* While upgrading the change log will "start from here" there is no evidence that this will cause an issue as
|
|
112
|
-
* it was never really an exposed feature of Activeledger just "per node specific" write data.
|
|
113
|
-
*
|
|
114
|
-
* ÿ is xff unicode, Character code 255
|
|
115
|
-
*
|
|
116
|
-
* @private
|
|
117
|
-
* @static
|
|
118
|
-
*/
|
|
119
|
-
//private static SEQ_META_PREFIX = "ÿsequence-storeÿ";
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Live changes emitter
|
|
123
|
-
*
|
|
124
|
-
* @private
|
|
125
|
-
* @static
|
|
126
|
-
*/
|
|
127
|
-
private changeEmitter = new EventEmitter();
|
|
128
|
-
|
|
129
|
-
private driver: IStorageDriver;
|
|
130
|
-
|
|
131
|
-
private cache: ActiveCache;
|
|
132
|
-
|
|
133
|
-
private openingPromise: Promise<void> | null = null;
|
|
134
|
-
|
|
135
|
-
constructor(location: string, private name: string, provider: string) {
|
|
136
|
-
this.driver = new LevelDBDriver(location + name, provider);
|
|
137
|
-
if (ENABLE_CACHE) {
|
|
138
|
-
this.cache = ActiveCacheManager.fetch(`streams:${this.name}`, 30000);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Attempts to fetch document, If fails returns default
|
|
145
|
-
*
|
|
146
|
-
* @private
|
|
147
|
-
* @template T
|
|
148
|
-
* @param {string} document
|
|
149
|
-
* @param {T} defaultvalue
|
|
150
|
-
* @returns
|
|
151
|
-
*/
|
|
152
|
-
private async levelUpGet<T>(document: string, defaultvalue: T) {
|
|
153
|
-
try {
|
|
154
|
-
await this.open();
|
|
155
|
-
return (await this.driver.get(document)) as unknown as T;
|
|
156
|
-
} catch {
|
|
157
|
-
return defaultvalue;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Jump straight to the cached wining branch end
|
|
163
|
-
*
|
|
164
|
-
* @private
|
|
165
|
-
* @param {schema} doc
|
|
166
|
-
* @returns {string}
|
|
167
|
-
*/
|
|
168
|
-
private findCachedBranchEnd(doc: schema): string {
|
|
169
|
-
return doc.rev_map[doc.winningRev].toString().padStart(16, "0");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Gets the latest sequence data document, resolving old rev-tree-format
|
|
174
|
-
* root documents (winningRev/rev_map/rev_tree/seq) down to their actual
|
|
175
|
-
* data document. New-format documents are already the data document, so
|
|
176
|
-
* this is a no-op for them - kept for backwards compatibility with data
|
|
177
|
-
* written before the rev-tree model was dropped.
|
|
178
|
-
*
|
|
179
|
-
* @private
|
|
180
|
-
* @param {schema} doc
|
|
181
|
-
* @returns {Promise<document>}
|
|
182
|
-
*/
|
|
183
|
-
private async seqDocFromRoot(doc: schema): Promise<document> {
|
|
184
|
-
if (doc.winningRev && doc.rev_map && doc.rev_tree && doc.seq) {
|
|
185
|
-
const twig = this.findCachedBranchEnd(doc);
|
|
186
|
-
try {
|
|
187
|
-
await this.open();
|
|
188
|
-
return (await ActiveClone.deserialize(
|
|
189
|
-
await this.driver.get(LevelMe.SEQ_PREFIX + twig)
|
|
190
|
-
)) as document;
|
|
191
|
-
} catch {
|
|
192
|
-
return {} as document;
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
return doc as document;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Opens the database and caches the metadata
|
|
201
|
-
*
|
|
202
|
-
* @private
|
|
203
|
-
*/
|
|
204
|
-
private async open(): Promise<void> {
|
|
205
|
-
if (this.openingPromise) {
|
|
206
|
-
return this.openingPromise;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (!this.driver.isOpen()) {
|
|
210
|
-
this.openingPromise = (async () => {
|
|
211
|
-
try {
|
|
212
|
-
await this.driver.open();
|
|
213
|
-
} catch (e) {
|
|
214
|
-
throw e;
|
|
215
|
-
} finally {
|
|
216
|
-
this.openingPromise = null;
|
|
217
|
-
}
|
|
218
|
-
})();
|
|
219
|
-
return this.openingPromise;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Fetches information about the database or creates a new database
|
|
225
|
-
*
|
|
226
|
-
* @returns
|
|
227
|
-
*/
|
|
228
|
-
public async info() {
|
|
229
|
-
try {
|
|
230
|
-
await this.open();
|
|
231
|
-
return {
|
|
232
|
-
doc_count: "----",
|
|
233
|
-
update_seq: 0,
|
|
234
|
-
db_name: this.name,
|
|
235
|
-
data_size: 0,
|
|
236
|
-
};
|
|
237
|
-
} catch (e) {
|
|
238
|
-
// TODO Filter bad / unexpected creates such as favicon.ico
|
|
239
|
-
return {
|
|
240
|
-
doc_count: "----",
|
|
241
|
-
update_seq: 0,
|
|
242
|
-
db_name: this.name,
|
|
243
|
-
data_size: 0,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Close the underlying leveldb connction
|
|
250
|
-
*
|
|
251
|
-
*/
|
|
252
|
-
public async close() {
|
|
253
|
-
await this.driver.close();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Backup this database
|
|
258
|
-
*
|
|
259
|
-
* @param {string} [filename]
|
|
260
|
-
*/
|
|
261
|
-
public async backup(filename?: string) {
|
|
262
|
-
if (!filename) {
|
|
263
|
-
filename = `${Date.now()}.alb`;
|
|
264
|
-
}
|
|
265
|
-
await fs.promises.writeFile(`${filename}.status`, filename);
|
|
266
|
-
const writer = createWriteStream(filename);
|
|
267
|
-
|
|
268
|
-
return new Promise((resolve, reject) => {
|
|
269
|
-
this.driver
|
|
270
|
-
.createValueStream()
|
|
271
|
-
.on("data", async (data: any) => {
|
|
272
|
-
writer.write(data.toString() + "\n");
|
|
273
|
-
})
|
|
274
|
-
.on("error", reject)
|
|
275
|
-
.on("end", async () => {
|
|
276
|
-
writer.end();
|
|
277
|
-
try {
|
|
278
|
-
await fs.promises.unlink(`${filename}.status`);
|
|
279
|
-
resolve(undefined);
|
|
280
|
-
} catch (err) {
|
|
281
|
-
reject(err);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Restore (overwriting) to this database
|
|
289
|
-
*
|
|
290
|
-
* @param {string} filename
|
|
291
|
-
*/
|
|
292
|
-
public async restore(filename: string) {
|
|
293
|
-
await fs.promises.writeFile(`${filename}.status`, "running");
|
|
294
|
-
|
|
295
|
-
return new Promise((resolve, reject) => {
|
|
296
|
-
createReadStream(filename)
|
|
297
|
-
.pipe(newLineTransform())
|
|
298
|
-
.on("data", async (data: Buffer) => {
|
|
299
|
-
try {
|
|
300
|
-
const doc = JSON.parse(data.toString());
|
|
301
|
-
ActiveLogger.info(`Restoring ${doc._id}`);
|
|
302
|
-
await this.bulkDocs(doc, { new_edits: true });
|
|
303
|
-
} catch {
|
|
304
|
-
ActiveLogger.warn(`Restoring FAILED`);
|
|
305
|
-
}
|
|
306
|
-
})
|
|
307
|
-
.on("error", reject)
|
|
308
|
-
.on("end", async () => {
|
|
309
|
-
try {
|
|
310
|
-
await fs.promises.unlink(`${filename}.status`);
|
|
311
|
-
resolve(undefined);
|
|
312
|
-
} catch (err) {
|
|
313
|
-
reject(err);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// public async restore() {
|
|
320
|
-
// await this.open();
|
|
321
|
-
// this.driver
|
|
322
|
-
// .createReadStream()
|
|
323
|
-
// .pipe(JSONStream.stringify("", "", ""))
|
|
324
|
-
// .pipe(createWriteStream("./backup.txt"));
|
|
325
|
-
|
|
326
|
-
// createReadStream("backup.txt")
|
|
327
|
-
// .pipe(JSONStream.parse())
|
|
328
|
-
// .pipe(this.driver.createKeyStream);
|
|
329
|
-
// }
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Returns all the data documents with filter options
|
|
333
|
-
*
|
|
334
|
-
* @param {allDocOptions} options
|
|
335
|
-
* @returns {Promise<unknown>}
|
|
336
|
-
*/
|
|
337
|
-
public allDocs(options: allDocOptions): Promise<unknown> {
|
|
338
|
-
return new Promise(async (resolve, reject) => {
|
|
339
|
-
try {
|
|
340
|
-
const offset = parseInt(options.skip?.toString() || "0");
|
|
341
|
-
await this.open();
|
|
342
|
-
|
|
343
|
-
// Cache rows to be returned
|
|
344
|
-
let rows: any[] = [];
|
|
345
|
-
|
|
346
|
-
if (options.keys) {
|
|
347
|
-
const docs = await this.getMany(options.keys);
|
|
348
|
-
rows = docs.map(doc => ({ doc }));
|
|
349
|
-
return resolve({
|
|
350
|
-
total_rows: docs.length,
|
|
351
|
-
offset,
|
|
352
|
-
rows,
|
|
353
|
-
});
|
|
354
|
-
} else {
|
|
355
|
-
// No offset built in, Create one by skip + limit and counter on skip;
|
|
356
|
-
let limit = options.limit || -1;
|
|
357
|
-
if (options.skip && limit !== -1) {
|
|
358
|
-
// Convert to int
|
|
359
|
-
options.skip = parseInt(options.skip as unknown as string);
|
|
360
|
-
limit += options.skip;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Read / Search the database as a stream
|
|
364
|
-
const stream = this.driver.createReadStream({
|
|
365
|
-
gte: LevelMe.DOC_PREFIX + (options.startkey || ""),
|
|
366
|
-
lt: options.endkey
|
|
367
|
-
? LevelMe.DOC_PREFIX + options.endkey
|
|
368
|
-
: LevelMe.META_PREFIX,
|
|
369
|
-
limit,
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Track pending deserialization promises in stream order
|
|
373
|
-
const docPromises: Promise<any>[] = [];
|
|
374
|
-
|
|
375
|
-
stream.on("data", (data: { key: string; value: Buffer }) => {
|
|
376
|
-
// Filter out the "skipped" keys
|
|
377
|
-
if (options.skip) {
|
|
378
|
-
options.skip--;
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Push the promise into the array to maintain order
|
|
383
|
-
docPromises.push(
|
|
384
|
-
ActiveClone.deserialize(data.value).then((doc: any) => {
|
|
385
|
-
if (options.include_docs) {
|
|
386
|
-
return doc;
|
|
387
|
-
} else {
|
|
388
|
-
return {
|
|
389
|
-
_id: doc._id, // Compatibility Trick
|
|
390
|
-
id: doc._id,
|
|
391
|
-
key: doc._id,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
})
|
|
395
|
-
);
|
|
396
|
-
})
|
|
397
|
-
.on("error", (err: unknown) => {
|
|
398
|
-
stream.destroy();
|
|
399
|
-
reject(err);
|
|
400
|
-
})
|
|
401
|
-
.on("end", async () => {
|
|
402
|
-
// Await all promises in the original stream order
|
|
403
|
-
const rows = await Promise.all(docPromises);
|
|
404
|
-
resolve({
|
|
405
|
-
total_rows: rows.length,
|
|
406
|
-
offset,
|
|
407
|
-
rows,
|
|
408
|
-
});
|
|
409
|
-
}); }
|
|
410
|
-
} catch (e) {
|
|
411
|
-
reject(e);
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Get a specific data document
|
|
417
|
-
*
|
|
418
|
-
* @param {string} key
|
|
419
|
-
* @returns
|
|
420
|
-
*/
|
|
421
|
-
public async get(key: string, raw = false) {
|
|
422
|
-
if (ENABLE_CACHE) {
|
|
423
|
-
// raw and non-raw reads store different shapes for the same id, so
|
|
424
|
-
// they need distinct cache keys or one mode poisons the other.
|
|
425
|
-
const cacheKey = raw ? `raw:${key}` : key;
|
|
426
|
-
if (!this.cache.has(cacheKey)) {
|
|
427
|
-
await this.open();
|
|
428
|
-
// Allow errors to bubble up?
|
|
429
|
-
const doc = await ActiveClone.deserialize(await this.driver.get(LevelMe.DOC_PREFIX + key)) as any;
|
|
430
|
-
if (raw) {
|
|
431
|
-
this.cache.set(cacheKey, doc);
|
|
432
|
-
} else {
|
|
433
|
-
this.cache.set(cacheKey, await this.seqDocFromRoot(doc));
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
return this.cache.get(cacheKey, 30000);
|
|
437
|
-
} else {
|
|
438
|
-
await this.open();
|
|
439
|
-
// Allow errors to bubble up?
|
|
440
|
-
const doc = await ActiveClone.deserialize(await this.driver.get(LevelMe.DOC_PREFIX + key)) as any;
|
|
441
|
-
if (raw) {
|
|
442
|
-
return doc;
|
|
443
|
-
} else {
|
|
444
|
-
return await this.seqDocFromRoot(doc);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
public async getMany(keys: string[]): Promise<any[]> {
|
|
450
|
-
if (ENABLE_CACHE) {
|
|
451
|
-
let tmpKeys = [];
|
|
452
|
-
let cached = [];
|
|
453
|
-
for (let i = keys.length; i--;) {
|
|
454
|
-
if (!this.cache.has(keys[i])) {
|
|
455
|
-
tmpKeys.push(LevelMe.DOC_PREFIX + keys[i]);
|
|
456
|
-
} else {
|
|
457
|
-
// No copy here, same as the cache-miss branch below (cached.push(data)
|
|
458
|
-
// shares the object with cache.set() unconditionally) - this was an
|
|
459
|
-
// inconsistent, incomplete defensive copy: a stream that's already
|
|
460
|
-
// warm skipped mutation exposure, one that wasn't didn't. Contract
|
|
461
|
-
// code itself never touches either one directly regardless -
|
|
462
|
-
// Activity.getState() (contracts/stream.ts) always deep-clones
|
|
463
|
-
// before handing state to a contract - so this copy wasn't
|
|
464
|
-
// load-bearing for correctness, just an extra allocation on every
|
|
465
|
-
// cache hit.
|
|
466
|
-
cached.push(this.cache.get(keys[i], 30000));
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Get uncached keys
|
|
471
|
-
if (tmpKeys.length) {
|
|
472
|
-
const result = await this.driver.getMany(tmpKeys);
|
|
473
|
-
// Loop and cache
|
|
474
|
-
for (let i = result.length; i--;) {
|
|
475
|
-
const data = await ActiveClone.deserialize(result[i]) as any;
|
|
476
|
-
this.cache.set(data._id, data);
|
|
477
|
-
cached.push(data);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return cached;
|
|
481
|
-
} else {
|
|
482
|
-
const tmpKeys = [];
|
|
483
|
-
for (let i = keys.length; i--;) {
|
|
484
|
-
tmpKeys.push(LevelMe.DOC_PREFIX + keys[i]);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Get uncached keys
|
|
488
|
-
const result = await this.driver.getMany(tmpKeys);
|
|
489
|
-
|
|
490
|
-
// Loop and parse
|
|
491
|
-
const parsed = await Promise.all(result.map(async (data) => {
|
|
492
|
-
const doc = await ActiveClone.deserialize(data) as any;
|
|
493
|
-
return doc;
|
|
494
|
-
}));
|
|
495
|
-
return parsed;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Get a specific sequence document
|
|
501
|
-
*
|
|
502
|
-
* @param {string} seq
|
|
503
|
-
* @returns
|
|
504
|
-
*/
|
|
505
|
-
public async getSeq(seq: string) {
|
|
506
|
-
return this.driver.get(LevelMe.SEQ_PREFIX + seq);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Compact the database to reduce storage space
|
|
511
|
-
*
|
|
512
|
-
* Will keep compact for now, Will later update to compact direct written files no more sequence
|
|
513
|
-
*
|
|
514
|
-
* @returns
|
|
515
|
-
*/
|
|
516
|
-
public compact(): Promise<unknown> {
|
|
517
|
-
return new Promise((resolve, reject) => {
|
|
518
|
-
// No definition as of yet, So lets check it exists
|
|
519
|
-
//@ts-ignore
|
|
520
|
-
if (this.driver.compactRange) {
|
|
521
|
-
// We could range everything with null, null but only the sequence files create the mass storage
|
|
522
|
-
// so as a performance trade off we will only compact across that range
|
|
523
|
-
|
|
524
|
-
//@ts-ignore
|
|
525
|
-
this.driver.compactRange(
|
|
526
|
-
`${LevelMe.SEQ_PREFIX}0000000000000000`,
|
|
527
|
-
`${LevelMe.SEQ_PREFIX}9999999999999999`
|
|
528
|
-
).then(resolve).catch(reject);
|
|
529
|
-
} else {
|
|
530
|
-
reject("Compact Range not found");
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Writes a data document (following sequences and revision information)
|
|
537
|
-
*
|
|
538
|
-
* @param {document} doc
|
|
539
|
-
* @returns
|
|
540
|
-
*/
|
|
541
|
-
public async post(doc: document) {
|
|
542
|
-
const writer = await this.prepareForWrite(doc, await this.driver.batch());
|
|
543
|
-
try {
|
|
544
|
-
await writer.chain.write();
|
|
545
|
-
if (ENABLE_CACHE) {
|
|
546
|
-
// Raw root doc shares its cache key with get(key, true) - the write
|
|
547
|
-
// just committed exactly this shape, safe to reuse directly.
|
|
548
|
-
this.cache.set(`raw:${writer.changes.id}`, writer.changes.doc);
|
|
549
|
-
// The resolved (non-raw) shape may need seqDocFromRoot (old-format
|
|
550
|
-
// backward-compat data) - don't assume raw === resolved, just drop
|
|
551
|
-
// the stale entry so the next non-raw get() recomputes it.
|
|
552
|
-
this.cache.delete(writer.changes.id);
|
|
553
|
-
}
|
|
554
|
-
} catch (e) {
|
|
555
|
-
// Real write failure - every caller up the stack (streamUpdater.ts,
|
|
556
|
-
// shared.ts's storeError(), selfhost.ts's own POST handler) already
|
|
557
|
-
// has a try/catch written expecting post() to reject on failure; it
|
|
558
|
-
// was just unreachable dead code because this used to always resolve
|
|
559
|
-
// { ok: true } regardless. May contain multiple documents, Easier &
|
|
560
|
-
// safer to clear the cache.
|
|
561
|
-
this.cache.clear();
|
|
562
|
-
throw e;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Emit outside the try/catch, same reasoning as bulkDocs() - a
|
|
566
|
-
// listener's own bug should never be able to masquerade as this write
|
|
567
|
-
// having failed. EventEmitter doesn't isolate a listener's own thrown
|
|
568
|
-
// exception by default though, so this is its own try/catch too -
|
|
569
|
-
// otherwise a listener throwing would propagate straight out and
|
|
570
|
-
// reject this call, the same class of bug being fixed here.
|
|
571
|
-
try {
|
|
572
|
-
this.changeEmitter.emit("change", writer.changes);
|
|
573
|
-
} catch (listenerError) {
|
|
574
|
-
ActiveLogger.error(listenerError, "change listener threw");
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return {
|
|
578
|
-
ok: true,
|
|
579
|
-
id: doc._id,
|
|
580
|
-
rev: writer.rev,
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
public async writeRaw(key: string, value: unknown) {
|
|
585
|
-
await this.open();
|
|
586
|
-
return this.driver.put(LevelMe.DOC_PREFIX + key, await ActiveClone.serialize(value, { enableCompression: true }));
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Alias for post, Legacy from PouchDb
|
|
591
|
-
*
|
|
592
|
-
* @param {(document | unknown)} doc
|
|
593
|
-
* @returns
|
|
594
|
-
*/
|
|
595
|
-
public async put(doc: document | unknown) {
|
|
596
|
-
return await this.post(doc as document);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Deletes a data / sequence / meta document
|
|
601
|
-
* Warning: Shouldn't be so easy to call this
|
|
602
|
-
*
|
|
603
|
-
* @param {string} key
|
|
604
|
-
* @returns
|
|
605
|
-
*/
|
|
606
|
-
public async del(key: string): Promise<void> {
|
|
607
|
-
await this.open();
|
|
608
|
-
const batch = await this.driver.batch();
|
|
609
|
-
|
|
610
|
-
// For now just delete the document key (not sequence)
|
|
611
|
-
// _local_doc_count need to reduce count
|
|
612
|
-
batch.del(LevelMe.DOC_PREFIX + key);
|
|
613
|
-
|
|
614
|
-
if (ENABLE_CACHE) {
|
|
615
|
-
if (this.cache.has(key)) {
|
|
616
|
-
this.cache.delete(key);
|
|
617
|
-
}
|
|
618
|
-
const rawKey = `raw:${key}`;
|
|
619
|
-
if (this.cache.has(rawKey)) {
|
|
620
|
-
this.cache.delete(rawKey);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
await batch.write();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Deletes sequences as a batch
|
|
629
|
-
*
|
|
630
|
-
* @param {string[]} keys
|
|
631
|
-
* @returns {Promise<void>}
|
|
632
|
-
*/
|
|
633
|
-
public async delSeq(keys: string[]): Promise<void> {
|
|
634
|
-
await this.open();
|
|
635
|
-
const batch = await this.driver.batch();
|
|
636
|
-
|
|
637
|
-
for (let i = keys.length; i--;) {
|
|
638
|
-
batch.del(LevelMe.SEQ_PREFIX + keys[i]);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
await batch.write();
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Provide real-time document insertion with starting point supported
|
|
646
|
-
*
|
|
647
|
-
* @param {string} options
|
|
648
|
-
* @returns {*}
|
|
649
|
-
*/
|
|
650
|
-
public changes(): EventEmitter {
|
|
651
|
-
return this.changeEmitter;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Bulk write documents (While acting like post)
|
|
656
|
-
*
|
|
657
|
-
* @param {unknown[]} docs
|
|
658
|
-
* @param {unknown} options
|
|
659
|
-
* @returns
|
|
660
|
-
*/
|
|
661
|
-
public async bulkDocs(
|
|
662
|
-
docs: document[],
|
|
663
|
-
options: { new_edits: boolean; force_rev?: string }
|
|
664
|
-
): Promise<boolean> {
|
|
665
|
-
// Now we could loop post, But then its not a single atomic write.
|
|
666
|
-
let batch = await this.driver.batch();
|
|
667
|
-
const changes = [];
|
|
668
|
-
for (let i = docs.length; i--;) {
|
|
669
|
-
const writer = await this.prepareForWrite(docs[i], batch, options);
|
|
670
|
-
batch = writer.chain; // Do I need do do this, Reference kept?
|
|
671
|
-
changes.push(writer.changes);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
try {
|
|
675
|
-
await batch.write();
|
|
676
|
-
if (ENABLE_CACHE) {
|
|
677
|
-
for (let i = changes.length; i--;) {
|
|
678
|
-
// See post() - raw shape is safe to reuse directly, resolved
|
|
679
|
-
// shape may need seqDocFromRoot so just drop it and let the next
|
|
680
|
-
// non-raw get() recompute it.
|
|
681
|
-
this.cache.set(`raw:${changes[i].id}`, changes[i].doc);
|
|
682
|
-
this.cache.delete(changes[i].id);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
} catch (e) {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Emit Changed Docs - one event per document, matching post()'s shape
|
|
690
|
-
// (a flat object, not an array), so a "change" listener never has to
|
|
691
|
-
// handle two different shapes depending on which write path triggered
|
|
692
|
-
// it. This used to emit a single event carrying the whole array, which
|
|
693
|
-
// crashed any listener written for post()'s single-object shape -
|
|
694
|
-
// selfhost.ts's /events SSE handler is exactly one such listener
|
|
695
|
-
// (change.id.startsWith(...) threw on an array, since arrays don't
|
|
696
|
-
// have an .id). Emitting outside the try/catch above (rather than
|
|
697
|
-
// inside it, as before) means a listener's own bug can no longer be
|
|
698
|
-
// misattributed as this write having failed - that's what let a
|
|
699
|
-
// thrown listener exception get silently caught here and turn into
|
|
700
|
-
// bulkDocs() returning false even though batch.write() had already
|
|
701
|
-
// succeeded, which streamUpdater.ts's commit path took as a genuine
|
|
702
|
-
// disk failure (error 1510) on every transaction, for as long as any
|
|
703
|
-
// /events client stayed connected.
|
|
704
|
-
//
|
|
705
|
-
// EventEmitter doesn't isolate a listener's own exception by default -
|
|
706
|
-
// a synchronous throw inside emit() propagates straight out to us, so
|
|
707
|
-
// being outside the write's try/catch above isn't enough on its own to
|
|
708
|
-
// fully protect the return value. Each emit is individually try/caught
|
|
709
|
-
// here too, so one listener throwing can't stop the rest from being
|
|
710
|
-
// notified, and can never turn into bulkDocs() itself rejecting.
|
|
711
|
-
for (let i = changes.length; i--;) {
|
|
712
|
-
try {
|
|
713
|
-
this.changeEmitter.emit("change", changes[i]);
|
|
714
|
-
} catch (listenerError) {
|
|
715
|
-
ActiveLogger.error(listenerError, "change listener threw");
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Prepare batch written of all meta documents
|
|
723
|
-
*
|
|
724
|
-
* @private
|
|
725
|
-
* @param {document} doc
|
|
726
|
-
* @param {LevelUpChain<any, any>} chain
|
|
727
|
-
* @returns {Promise<{ chain: LevelUpChain<any, any>; rev: string }>}
|
|
728
|
-
*/
|
|
729
|
-
private async prepareForWrite(
|
|
730
|
-
doc: document,
|
|
731
|
-
chain: LevelUpChain<any, any>,
|
|
732
|
-
options: { new_edits: boolean; force_rev?: string } = { new_edits: true }
|
|
733
|
-
): Promise<{
|
|
734
|
-
chain: LevelUpChain<any, any>;
|
|
735
|
-
rev: string;
|
|
736
|
-
changes: {
|
|
737
|
-
id: string;
|
|
738
|
-
changes: { rev: string }[];
|
|
739
|
-
doc: document;
|
|
740
|
-
seq: number;
|
|
741
|
-
};
|
|
742
|
-
}> {
|
|
743
|
-
await this.open();
|
|
744
|
-
|
|
745
|
-
// MD5 input to act as tree position
|
|
746
|
-
// Use a copy to avoid mutating the original object
|
|
747
|
-
//const docToWrite = { ...doc };
|
|
748
|
-
//delete docToWrite._rev;
|
|
749
|
-
//const incomingDoc = JSON.stringify(docToWrite);
|
|
750
|
-
// Not backwards compatiable
|
|
751
|
-
const md5 = createHash("md5").update(JSON.stringify({ ...doc, _rev: null })).digest("hex");
|
|
752
|
-
let newRev = "";
|
|
753
|
-
|
|
754
|
-
// Does Document eixst?
|
|
755
|
-
try {
|
|
756
|
-
// Document exists, handle update
|
|
757
|
-
const currentDocRoot = await ActiveClone.deserialize(
|
|
758
|
-
await this.driver.get(LevelMe.DOC_PREFIX + doc._id)
|
|
759
|
-
) as schema;
|
|
760
|
-
|
|
761
|
-
if (doc._rev !== currentDocRoot._rev && !options.new_edits) {
|
|
762
|
-
throw new Error(`Revision Mismatch: ${doc._id} @ ${doc._rev} !== ${currentDocRoot._rev}`);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
if (options.force_rev) {
|
|
766
|
-
newRev = options.force_rev;
|
|
767
|
-
} else {
|
|
768
|
-
|
|
769
|
-
const [p1, curmd5] = currentDocRoot._rev.split("-");
|
|
770
|
-
//const pos = parseInt(p1) + 1;
|
|
771
|
-
//newRev = `${pos}-${md5}`;
|
|
772
|
-
|
|
773
|
-
// Md5s don't match we need to update rev, This will then get it written to disk
|
|
774
|
-
if (md5 !== curmd5) {
|
|
775
|
-
newRev = `${parseInt(p1) + 1}-${md5}`;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (newRev) {
|
|
781
|
-
doc._rev = newRev;
|
|
782
|
-
chain.put(LevelMe.DOC_PREFIX + doc._id, await ActiveClone.serialize(doc, { enableCompression: true }));
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
} catch (error) {
|
|
786
|
-
// Document doesn't exist, handle creation
|
|
787
|
-
if (error.notFound) {
|
|
788
|
-
if (!options.new_edits && doc._rev) {
|
|
789
|
-
newRev = doc._rev;
|
|
790
|
-
} else {
|
|
791
|
-
newRev = `1-${md5}`;
|
|
792
|
-
}
|
|
793
|
-
doc._rev = newRev;
|
|
794
|
-
chain.put(LevelMe.DOC_PREFIX + doc._id, await ActiveClone.serialize(doc, { enableCompression: true }));
|
|
795
|
-
} else {
|
|
796
|
-
// Re-throw other errors (like revision mismatch)
|
|
797
|
-
throw error;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return {
|
|
802
|
-
chain,
|
|
803
|
-
rev: newRev,
|
|
804
|
-
changes: {
|
|
805
|
-
id: doc._id,
|
|
806
|
-
changes: [{ rev: newRev }],
|
|
807
|
-
doc,
|
|
808
|
-
// No real sequence counter exists anywhere in this class - nothing
|
|
809
|
-
// ever writes a LevelMe.SEQ_PREFIX-keyed entry, despite
|
|
810
|
-
// changesFromSeq()'s whole design (a few lines below) assuming
|
|
811
|
-
// that range actually has data in it. That's a separate, deeper
|
|
812
|
-
// gap this doesn't attempt to fix (changesFromSeq() will keep
|
|
813
|
-
// returning an empty backfill regardless of `since`, for every
|
|
814
|
-
// caller, until real sequence tracking is implemented). This
|
|
815
|
-
// field exists only so the *live* change event this object feeds
|
|
816
|
-
// (selfhost.ts's _changes longpoll listener, `change.seq`) has
|
|
817
|
-
// something real to put in its response's "last_seq" field -
|
|
818
|
-
// without it, that field was the literal string "undefined"
|
|
819
|
-
// (JS's string-coercion of the missing property), which isn't
|
|
820
|
-
// valid JSON and broke every client trying to parse the response.
|
|
821
|
-
// Date.now() is monotonic enough for that one purpose; it is NOT
|
|
822
|
-
// a substitute for real backfill/replay support.
|
|
823
|
-
seq: Date.now(),
|
|
824
|
-
},
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Provide real-time document insertion with starting point supported
|
|
831
|
-
*
|
|
832
|
-
* @param {string} options
|
|
833
|
-
* @returns {*}
|
|
834
|
-
*/
|
|
835
|
-
public changesFromSeq(options: changesOptions): Promise<{
|
|
836
|
-
results: {
|
|
837
|
-
id: string;
|
|
838
|
-
changes: { rev: string }[];
|
|
839
|
-
doc?: document;
|
|
840
|
-
seq: number;
|
|
841
|
-
}[];
|
|
842
|
-
last_seq: number;
|
|
843
|
-
}> {
|
|
844
|
-
return new Promise((resolve, reject) => {
|
|
845
|
-
// since="now" (nano-gateway's ChangesWatcher default for a fresh
|
|
846
|
-
// subscription with no Last-Event-ID to resume from -
|
|
847
|
-
// packages/nano-gateway/src/changesWatcher.ts) means "nothing is
|
|
848
|
-
// pending as of this instant" - resolve that directly rather than
|
|
849
|
-
// falling through to the numeric-seq path below, which used to do
|
|
850
|
-
// "now".padStart(16, "0") = "0000000000000now" as a LevelDB range
|
|
851
|
-
// key. That's not a valid padded-numeric sequence key, and the
|
|
852
|
-
// underlying driver's createReadStream() throws on it - which,
|
|
853
|
-
// since selfhost.ts's _changes handler has no .catch() on this
|
|
854
|
-
// promise, surfaced as an opaque 500 with an empty {} body all the
|
|
855
|
-
// way up through nano-gateway's own client (live-confirmed: this is
|
|
856
|
-
// exactly what broke nano-gateway's very first subscribe attempt
|
|
857
|
-
// once the two other _changes bugs - unfinalised response, and the
|
|
858
|
-
// separate garbage-header-bytes bug - were fixed and this code path
|
|
859
|
-
// was reached for the first time).
|
|
860
|
-
if (options.since === "now") {
|
|
861
|
-
resolve({ results: [], last_seq: 0 });
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// get all sequenced documents with emitter, sequence "maybe up to date"
|
|
866
|
-
// Cache rows to be returned
|
|
867
|
-
const rows: any[] = [];
|
|
868
|
-
|
|
869
|
-
// For checking on end
|
|
870
|
-
const promises: Promise<document>[] = [];
|
|
871
|
-
|
|
872
|
-
// Filter for sequences metadata
|
|
873
|
-
const filter = {
|
|
874
|
-
gt: LevelMe.SEQ_PREFIX,
|
|
875
|
-
lt: LevelMe.DOC_PREFIX,
|
|
876
|
-
limit: parseInt((options.limit || 5).toString()),
|
|
877
|
-
reverse:
|
|
878
|
-
options.descending && options.descending.toString() === "true"
|
|
879
|
-
? true
|
|
880
|
-
: false, // (array reverse maybe faster, but wont work with filter)
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
if (options.since) {
|
|
884
|
-
filter.gt =
|
|
885
|
-
LevelMe.SEQ_PREFIX + options.since.toString().padStart(16, "0");
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Read / Search the database as a stream
|
|
889
|
-
this.driver
|
|
890
|
-
.createReadStream(filter)
|
|
891
|
-
.on("data", async (data: { key: string; value: Buffer }) => {
|
|
892
|
-
const doc = await ActiveClone.deserialize(data.value) as any;
|
|
893
|
-
// Get sequence from keyname
|
|
894
|
-
const seq = parseInt(
|
|
895
|
-
data.key.toString().replace(LevelMe.SEQ_PREFIX, "")
|
|
896
|
-
);
|
|
897
|
-
if (
|
|
898
|
-
options.include_docs &&
|
|
899
|
-
JSON.parse(options.include_docs.toString())
|
|
900
|
-
) {
|
|
901
|
-
rows.push({
|
|
902
|
-
id: doc._id,
|
|
903
|
-
seq,
|
|
904
|
-
changes: [
|
|
905
|
-
{
|
|
906
|
-
rev: doc._rev,
|
|
907
|
-
},
|
|
908
|
-
],
|
|
909
|
-
doc,
|
|
910
|
-
});
|
|
911
|
-
} else {
|
|
912
|
-
rows.push({
|
|
913
|
-
id: doc._id,
|
|
914
|
-
seq,
|
|
915
|
-
changes: [
|
|
916
|
-
{
|
|
917
|
-
rev: doc._rev,
|
|
918
|
-
},
|
|
919
|
-
],
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
})
|
|
923
|
-
.on("error", (err: unknown) => {
|
|
924
|
-
reject(err);
|
|
925
|
-
})
|
|
926
|
-
.on("close", () => { })
|
|
927
|
-
.on("end", async () => {
|
|
928
|
-
await Promise.all(promises);
|
|
929
|
-
resolve({
|
|
930
|
-
results: rows,
|
|
931
|
-
last_seq: 0,
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
|