@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.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/es/cache.d.ts +18 -0
  3. package/es/cache.d.ts.map +1 -0
  4. package/es/cache.js +57 -0
  5. package/es/cache.js.map +1 -0
  6. package/es/datastore.d.ts +11 -0
  7. package/es/datastore.d.ts.map +1 -0
  8. package/es/datastore.js +67 -0
  9. package/es/datastore.js.map +1 -0
  10. package/es/driver.d.ts +15 -0
  11. package/es/driver.d.ts.map +1 -0
  12. package/es/driver.js +2 -0
  13. package/es/driver.js.map +1 -0
  14. package/es/drivers/leveldb.d.ts +20 -0
  15. package/es/drivers/leveldb.d.ts.map +1 -0
  16. package/es/drivers/leveldb.js +121 -0
  17. package/es/drivers/leveldb.js.map +1 -0
  18. package/es/drivers/rocksdb.d.ts +20 -0
  19. package/es/drivers/rocksdb.d.ts.map +1 -0
  20. package/es/drivers/rocksdb.js +98 -0
  21. package/es/drivers/rocksdb.js.map +1 -0
  22. package/es/heartbeat.d.ts +6 -0
  23. package/es/heartbeat.d.ts.map +1 -0
  24. package/es/heartbeat.js +15 -0
  25. package/es/heartbeat.js.map +1 -0
  26. package/es/index.d.ts +3 -0
  27. package/es/index.d.ts.map +1 -0
  28. package/es/index.js +3 -0
  29. package/es/index.js.map +1 -0
  30. package/es/levelme.d.ts +82 -0
  31. package/es/levelme.d.ts.map +1 -0
  32. package/es/levelme.js +476 -0
  33. package/es/levelme.js.map +1 -0
  34. package/es/newlinestream.d.ts +3 -0
  35. package/es/newlinestream.d.ts.map +1 -0
  36. package/es/newlinestream.js +28 -0
  37. package/es/newlinestream.js.map +1 -0
  38. package/es/selfhost.d.ts +2 -0
  39. package/es/selfhost.d.ts.map +1 -0
  40. package/es/selfhost.js +479 -0
  41. package/es/selfhost.js.map +1 -0
  42. package/es/sse.d.ts +10 -0
  43. package/es/sse.d.ts.map +1 -0
  44. package/es/sse.js +41 -0
  45. package/es/sse.js.map +1 -0
  46. package/lib/cache.d.ts +18 -0
  47. package/lib/cache.d.ts.map +1 -0
  48. package/lib/cache.js +62 -0
  49. package/lib/cache.js.map +1 -0
  50. package/lib/datastore.d.ts +11 -0
  51. package/lib/datastore.d.ts.map +1 -0
  52. package/lib/datastore.js +94 -0
  53. package/lib/datastore.js.map +1 -0
  54. package/lib/driver.d.ts +15 -0
  55. package/lib/driver.d.ts.map +1 -0
  56. package/lib/driver.js +3 -0
  57. package/lib/driver.js.map +1 -0
  58. package/lib/drivers/leveldb.d.ts +20 -0
  59. package/lib/drivers/leveldb.d.ts.map +1 -0
  60. package/lib/drivers/leveldb.js +125 -0
  61. package/lib/drivers/leveldb.js.map +1 -0
  62. package/lib/drivers/rocksdb.d.ts +20 -0
  63. package/lib/drivers/rocksdb.d.ts.map +1 -0
  64. package/lib/drivers/rocksdb.js +102 -0
  65. package/lib/drivers/rocksdb.js.map +1 -0
  66. package/lib/heartbeat.d.ts +6 -0
  67. package/lib/heartbeat.d.ts.map +1 -0
  68. package/lib/heartbeat.js +19 -0
  69. package/lib/heartbeat.js.map +1 -0
  70. package/lib/index.d.ts +3 -0
  71. package/lib/index.d.ts.map +1 -0
  72. package/lib/index.js +6 -0
  73. package/lib/index.js.map +1 -0
  74. package/lib/levelme.d.ts +82 -0
  75. package/lib/levelme.d.ts.map +1 -0
  76. package/lib/levelme.js +503 -0
  77. package/lib/levelme.js.map +1 -0
  78. package/lib/newlinestream.d.ts +3 -0
  79. package/lib/newlinestream.d.ts.map +1 -0
  80. package/lib/newlinestream.js +31 -0
  81. package/lib/newlinestream.js.map +1 -0
  82. package/lib/selfhost.d.ts +2 -0
  83. package/lib/selfhost.d.ts.map +1 -0
  84. package/lib/selfhost.js +504 -0
  85. package/lib/selfhost.js.map +1 -0
  86. package/lib/sse.d.ts +10 -0
  87. package/lib/sse.d.ts.map +1 -0
  88. package/lib/sse.js +45 -0
  89. package/lib/sse.js.map +1 -0
  90. package/package.json +9 -5
  91. package/src/cache.ts +0 -72
  92. package/src/datastore.ts +0 -191
  93. package/src/driver.ts +0 -18
  94. package/src/drivers/leveldb.ts +0 -145
  95. package/src/drivers/rocksdb.ts +0 -118
  96. package/src/heartbeat.ts +0 -60
  97. package/src/index.ts +0 -25
  98. package/src/levelme.ts +0 -938
  99. package/src/newlinestream.ts +0 -32
  100. package/src/selfhost.ts +0 -1104
  101. package/src/sse.ts +0 -136
  102. package/src/types/rocksdb.d.ts +0 -1
  103. package/tsconfig.json +0 -8
  104. /package/{src → lib}/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +0 -0
  105. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
  106. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
  107. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
  108. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
  109. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
  110. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
  111. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
  112. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger.png +0 -0
  113. /package/{src → lib}/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
  114. /package/{src → lib}/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
  115. /package/{src → lib}/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
  116. /package/{src → lib}/fauxton/dashboard.assets/img/fauxtonicon5.svg +0 -0
  117. /package/{src → lib}/fauxton/dashboard.assets/img/fontawesome-webfont.svg +0 -0
  118. /package/{src → lib}/fauxton/dashboard.assets/img/github.png +0 -0
  119. /package/{src → lib}/fauxton/dashboard.assets/img/googleplus.png +0 -0
  120. /package/{src → lib}/fauxton/dashboard.assets/img/linkedin.png +0 -0
  121. /package/{src → lib}/fauxton/dashboard.assets/img/loader.gif +0 -0
  122. /package/{src → lib}/fauxton/dashboard.assets/img/twitter.png +0 -0
  123. /package/{src → lib}/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +0 -0
  124. /package/{src → lib}/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +0 -0
  125. /package/{src → lib}/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +0 -0
  126. /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
-