@activeledger/activestorage 4.5.12 → 4.5.14

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 (125) hide show
  1. package/es/cache.d.ts +18 -0
  2. package/es/cache.d.ts.map +1 -0
  3. package/es/cache.js +57 -0
  4. package/es/cache.js.map +1 -0
  5. package/es/datastore.d.ts +11 -0
  6. package/es/datastore.d.ts.map +1 -0
  7. package/es/datastore.js +67 -0
  8. package/es/datastore.js.map +1 -0
  9. package/es/driver.d.ts +15 -0
  10. package/es/driver.d.ts.map +1 -0
  11. package/es/driver.js +2 -0
  12. package/es/driver.js.map +1 -0
  13. package/es/drivers/leveldb.d.ts +20 -0
  14. package/es/drivers/leveldb.d.ts.map +1 -0
  15. package/es/drivers/leveldb.js +121 -0
  16. package/es/drivers/leveldb.js.map +1 -0
  17. package/es/drivers/rocksdb.d.ts +20 -0
  18. package/es/drivers/rocksdb.d.ts.map +1 -0
  19. package/es/drivers/rocksdb.js +98 -0
  20. package/es/drivers/rocksdb.js.map +1 -0
  21. package/es/heartbeat.d.ts +6 -0
  22. package/es/heartbeat.d.ts.map +1 -0
  23. package/es/heartbeat.js +15 -0
  24. package/es/heartbeat.js.map +1 -0
  25. package/es/index.d.ts +3 -0
  26. package/es/index.d.ts.map +1 -0
  27. package/es/index.js +3 -0
  28. package/es/index.js.map +1 -0
  29. package/es/levelme.d.ts +82 -0
  30. package/es/levelme.d.ts.map +1 -0
  31. package/es/levelme.js +488 -0
  32. package/es/levelme.js.map +1 -0
  33. package/es/newlinestream.d.ts +3 -0
  34. package/es/newlinestream.d.ts.map +1 -0
  35. package/es/newlinestream.js +28 -0
  36. package/es/newlinestream.js.map +1 -0
  37. package/es/selfhost.d.ts +2 -0
  38. package/es/selfhost.d.ts.map +1 -0
  39. package/es/selfhost.js +479 -0
  40. package/es/selfhost.js.map +1 -0
  41. package/es/sse.d.ts +10 -0
  42. package/es/sse.d.ts.map +1 -0
  43. package/es/sse.js +41 -0
  44. package/es/sse.js.map +1 -0
  45. package/lib/cache.d.ts +18 -0
  46. package/lib/cache.d.ts.map +1 -0
  47. package/lib/cache.js +62 -0
  48. package/lib/cache.js.map +1 -0
  49. package/lib/datastore.d.ts +11 -0
  50. package/lib/datastore.d.ts.map +1 -0
  51. package/lib/datastore.js +94 -0
  52. package/lib/datastore.js.map +1 -0
  53. package/lib/driver.d.ts +15 -0
  54. package/lib/driver.d.ts.map +1 -0
  55. package/lib/driver.js +3 -0
  56. package/lib/driver.js.map +1 -0
  57. package/lib/drivers/leveldb.d.ts +20 -0
  58. package/lib/drivers/leveldb.d.ts.map +1 -0
  59. package/lib/drivers/leveldb.js +125 -0
  60. package/lib/drivers/leveldb.js.map +1 -0
  61. package/lib/drivers/rocksdb.d.ts +20 -0
  62. package/lib/drivers/rocksdb.d.ts.map +1 -0
  63. package/lib/drivers/rocksdb.js +102 -0
  64. package/lib/drivers/rocksdb.js.map +1 -0
  65. package/lib/heartbeat.d.ts +6 -0
  66. package/lib/heartbeat.d.ts.map +1 -0
  67. package/lib/heartbeat.js +19 -0
  68. package/lib/heartbeat.js.map +1 -0
  69. package/lib/index.d.ts +3 -0
  70. package/lib/index.d.ts.map +1 -0
  71. package/lib/index.js +6 -0
  72. package/lib/index.js.map +1 -0
  73. package/lib/levelme.d.ts +82 -0
  74. package/lib/levelme.d.ts.map +1 -0
  75. package/lib/levelme.js +515 -0
  76. package/lib/levelme.js.map +1 -0
  77. package/lib/newlinestream.d.ts +3 -0
  78. package/lib/newlinestream.d.ts.map +1 -0
  79. package/lib/newlinestream.js +31 -0
  80. package/lib/newlinestream.js.map +1 -0
  81. package/lib/selfhost.d.ts +2 -0
  82. package/lib/selfhost.d.ts.map +1 -0
  83. package/lib/selfhost.js +504 -0
  84. package/lib/selfhost.js.map +1 -0
  85. package/lib/sse.d.ts +10 -0
  86. package/lib/sse.d.ts.map +1 -0
  87. package/lib/sse.js +45 -0
  88. package/lib/sse.js.map +1 -0
  89. package/package.json +12 -8
  90. package/src/cache.ts +0 -72
  91. package/src/datastore.ts +0 -187
  92. package/src/driver.ts +0 -18
  93. package/src/drivers/leveldb.ts +0 -145
  94. package/src/drivers/rocksdb.ts +0 -118
  95. package/src/heartbeat.ts +0 -60
  96. package/src/index.ts +0 -25
  97. package/src/levelme.ts +0 -938
  98. package/src/newlinestream.ts +0 -32
  99. package/src/selfhost.ts +0 -1078
  100. package/src/sse.ts +0 -136
  101. package/src/types/rocksdb.d.ts +0 -1
  102. package/tsconfig.json +0 -8
  103. /package/{src → lib}/fauxton/dashboard.assets/css/styles.d43d3249088a6c04b633.css +0 -0
  104. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.eot +0 -0
  105. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.ttf +0 -0
  106. /package/{src → lib}/fauxton/dashboard.assets/fonts/fauxtonicon5.woff +0 -0
  107. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.eot +0 -0
  108. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.ttf +0 -0
  109. /package/{src → lib}/fauxton/dashboard.assets/fonts/fontawesome-webfont.woff +0 -0
  110. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger-logo.png +0 -0
  111. /package/{src → lib}/fauxton/dashboard.assets/img/activeledger.png +0 -0
  112. /package/{src → lib}/fauxton/dashboard.assets/img/asf-feather-logo.png +0 -0
  113. /package/{src → lib}/fauxton/dashboard.assets/img/couch-watermark.png +0 -0
  114. /package/{src → lib}/fauxton/dashboard.assets/img/couchdb-logo.png +0 -0
  115. /package/{src → lib}/fauxton/dashboard.assets/img/fauxtonicon5.svg +0 -0
  116. /package/{src → lib}/fauxton/dashboard.assets/img/fontawesome-webfont.svg +0 -0
  117. /package/{src → lib}/fauxton/dashboard.assets/img/github.png +0 -0
  118. /package/{src → lib}/fauxton/dashboard.assets/img/googleplus.png +0 -0
  119. /package/{src → lib}/fauxton/dashboard.assets/img/linkedin.png +0 -0
  120. /package/{src → lib}/fauxton/dashboard.assets/img/loader.gif +0 -0
  121. /package/{src → lib}/fauxton/dashboard.assets/img/twitter.png +0 -0
  122. /package/{src → lib}/fauxton/dashboard.assets/js/bundle.d43d3249088a6c04b633.js +0 -0
  123. /package/{src → lib}/fauxton/dashboard.assets/js/manifest.a154524a560e5f6f431b.js +0 -0
  124. /package/{src → lib}/fauxton/dashboard.assets/js/vendor.156793045cdeb7c63707.js +0 -0
  125. /package/{src → lib}/fauxton/index.html +0 -0
package/src/selfhost.ts DELETED
@@ -1,1078 +0,0 @@
1
- /*
2
- * MIT License (MIT)
3
- * Copyright (c) 2018 Activeledger
4
- *
5
- * Permission is hereby granted, free of charge, to any person obtaining a copy
6
- * of this software and associated documentation files (the "Software"), to deal
7
- * in the Software without restriction, including without limitation the rights
8
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- * copies of the Software, and to permit persons to whom the Software is
10
- * furnished to do so, subject to the following conditions:
11
- *
12
- * The above copyright notice and this permission notice shall be included in all
13
- * copies or substantial portions of the Software.
14
- *
15
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- * SOFTWARE.
22
- */
23
- import * as http from "http";
24
- import * as path from "path";
25
- import { promises as fsPromises } from "fs";
26
- import { randomUUID } from "crypto";
27
- import { ActiveHttpd, IActiveHttpIncoming } from "@activeledger/httpd";
28
- import { LevelMe } from "./levelme";
29
- //import { Socket } from "net";
30
- import { SSE } from "./sse";
31
- import { ActiveLogger } from "@activeledger/activelogger";
32
- import { IActiveHttpResponse } from "@activeledger/httpd/lib/httpd";
33
-
34
- (function () {
35
- const fauxtonCache: { [key: string]: { mime: string; data: Buffer } } = {};
36
- // Fauxton Path
37
- const FAUXTON_PATH = __dirname + "/fauxton/";
38
-
39
- // Directory Prefix
40
- const DIR_PREFIX = "./" + process.argv[2] + "/";
41
-
42
- // Listening Port
43
- const PORT = process.argv[3];
44
-
45
- // Data Storage Engine Provider, level provides backwards compatiblility
46
- const DS_PROVIDER = process.argv[4] || "level";
47
-
48
- // Database Connection Cache
49
- let dbCache: { [index: string]: LevelMe } = {};
50
-
51
- // Track if we are shutting down
52
- let isShuttingDown = false;
53
-
54
- /**
55
- * Graceful Shutdown
56
- *
57
- * @param {ActiveHttpd} httpd host
58
- */
59
- const terminate = async (host: ActiveHttpd) => {
60
- if (!isShuttingDown) {
61
- isShuttingDown = true;
62
- host.shutdown();
63
-
64
- // Close all databases
65
- const dbNames = Object.keys(dbCache);
66
- for (const name of dbNames) {
67
- try {
68
- await dbCache[name].close();
69
- ActiveLogger.info(`Closed database: ${name}`);
70
- } catch (err) {
71
- ActiveLogger.error(`Error closing database ${name}:`, err);
72
- }
73
- }
74
- process.exit(0);
75
- }
76
- }
77
-
78
- /**
79
- * Manages Pouch Connections
80
- *
81
- * @param {string} name
82
- * @returns
83
- */
84
- const getDB = (name: string): LevelMe => {
85
- // Must start with activeledger (will cause problem with config need to resolve)
86
- if (!name.startsWith("activeledger")) {
87
- throw new Error(`invalid database - ${name}`);
88
- }
89
- if (!dbCache[name]) {
90
- try {
91
- dbCache[name] = new LevelMe(DIR_PREFIX, name, DS_PROVIDER);
92
- } catch (err) {
93
- throw err;
94
- }
95
- } else {
96
- }
97
- return dbCache[name];
98
- };
99
-
100
- /**
101
- * Fast uuidV4 Generator
102
- * Credit : https://gist.github.com/jed/982883
103
- *
104
- * @param {number} [a]
105
- * @returns {number}
106
- */
107
- const uuidGenV4 = (): string => randomUUID();
108
-
109
- /**
110
- * Checks for the existant of _id if not ads it and returns
111
- *
112
- * @param {*} doc
113
- * @returns {*}
114
- */
115
- const checkDoc = (doc: any): any => {
116
- if (!doc._id) doc._id = uuidGenV4();
117
- return doc;
118
- };
119
-
120
- // Create Light Server
121
- let http = new ActiveHttpd();
122
-
123
- // Attempt better shutdown
124
- process.on("SIGINT", () => {
125
- terminate(http);
126
- });
127
- process.on("SIGTERM", () => {
128
- terminate(http);
129
- });
130
- process.on("SIGQUIT", () => {
131
- terminate(http);
132
- });
133
-
134
-
135
- // A hand-rolled PID-file "already running" check used to live here:
136
- // record our PID on start, and on the next start, if a previous PID file
137
- // exists and `process.kill(pid, 0)` finds *any* live process with that
138
- // PID, refuse to start. That's unreliable the moment PIDs get reused,
139
- // which happens on every container restart (each gets a fresh, small PID
140
- // namespace) and eventually on bare metal too - a stale recorded PID
141
- // just needs to coincide with some unrelated process for this to produce
142
- // a false "Instance already running" that no amount of restarting fixes,
143
- // since the file was also never cleaned up except on a graceful
144
- // SIGINT/SIGTERM/SIGQUIT (see terminate() above) - a SIGKILL or crash
145
- // left it behind for good. Reproduced live: two nodes' storage refused
146
- // to restart after being recreated mid-session, in full isolation, with
147
- // no other process touching the same data - deleting the stale PID file
148
- // was the only fix.
149
- //
150
- // Removed rather than patched to clean up more reliably: LevelDB (via
151
- // LevelMe below) already takes a real OS-level advisory lock on its own
152
- // LOCK file per database when opened, which the kernel releases
153
- // automatically no matter how the holding process dies - no cleanup
154
- // logic needed, and it actually enforces single-writer correctness
155
- // instead of approximating it by PID. This custom check was redundant
156
- // with that at best, actively wrong at worst.
157
-
158
- http.use("/", "GET", () => {
159
- return {
160
- activeledger: "Welcome to Activeledger data!",
161
- adapters: ["levelme"],
162
- engine: DS_PROVIDER,
163
- };
164
- });
165
-
166
- // Standard Session
167
- http.use("_session", "GET", () => {
168
- return { ok: true, userCtx: { name: null, roles: ["_admin"] } };
169
- });
170
-
171
- // List all databases
172
- http.use("_all_dbs", "GET", async () => {
173
- // Check to see if it is a directory and not a special directory
174
- const files = await fsPromises.readdir(DIR_PREFIX);
175
- // Same pattern deleteDb() below already uses - stat every entry
176
- // concurrently instead of one at a time.
177
- const stats = await Promise.all(
178
- files.map((file) => fsPromises.lstat(DIR_PREFIX + file))
179
- );
180
- const dbs = files.filter(
181
- (file, i) =>
182
- stats[i].isDirectory() &&
183
- file !== process.argv[2] &&
184
- file !== "pouch__all_dbs__" &&
185
- file !== "_replicator" &&
186
- !file.includes("-mrview-")
187
- );
188
-
189
- // Return directories
190
- return dbs;
191
- });
192
-
193
- // Get Database Info
194
- http.use("*", "GET", async (incoming: IActiveHttpIncoming) => {
195
- // Get Database
196
- let db = getDB(incoming.url[0]);
197
- let info = await db.info();
198
-
199
- // Now add data_size
200
- const files = await fsPromises.readdir(DIR_PREFIX + incoming.url[0]);
201
- const stats = await Promise.all(
202
- files.map((source) =>
203
- fsPromises.stat(DIR_PREFIX + incoming.url[0] + "/" + source)
204
- )
205
- );
206
- info.data_size = stats.reduce((total, stat) => total + stat.size, 0);
207
- return info;
208
- });
209
-
210
- // Create Database
211
- http.use("*", "PUT", async (incoming: IActiveHttpIncoming) => {
212
- // Get Database
213
- let db = getDB(incoming.body.name);
214
-
215
- // Create Database
216
- await db.info();
217
- return { ok: true };
218
- });
219
-
220
- // Delete Database Local Var
221
- const deleteDb = async (dir: string) => {
222
- // Read all files and delete
223
- const files = await fsPromises.readdir(dir);
224
- await Promise.all(files.map(source => fsPromises.unlink(dir + "/" + source)));
225
-
226
- // Delete the folder
227
- await fsPromises.rmdir(dir);
228
- };
229
-
230
- // Delete Database
231
- http.use("*", "DELETE", async (incoming: IActiveHttpIncoming) => {
232
- // Can just delete the folder, It shouldn't have nested so can delete all the files first
233
- let dir = DIR_PREFIX + incoming.url[0];
234
-
235
- // Check Folder
236
- try {
237
- await fsPromises.access(dir);
238
- } catch {
239
- // Directory doesn't exist, so we're good.
240
- return { ok: true };
241
- }
242
- // If we have this db open we need to close it
243
- if (dbCache[incoming.url[0]]) {
244
- await dbCache[incoming.url[0]].close();
245
- delete dbCache[incoming.url[0]];
246
- }
247
- // Delete Database
248
- await deleteDb(dir);
249
- return { ok: true };
250
- });
251
-
252
- // Get UUID
253
- http.use("_uuids", "GET", async (incoming: IActiveHttpIncoming) => {
254
- // uuid holder
255
- let uuids = [];
256
-
257
- // Loop for how many requested
258
- for (let i = 0; i < incoming.query.count; i++) {
259
- uuids.push(uuidGenV4());
260
- }
261
- return { uuids };
262
- });
263
-
264
- // Get all docs from a database
265
- http.use("*/_all_docs", "GET", async (incoming: IActiveHttpIncoming) => {
266
- // Get Database
267
- let db = getDB(incoming.url[0]);
268
- return await db.allDocs(prepareAllDocs(incoming.query));
269
- });
270
-
271
- // Get all docs filtered from a database
272
- http.use("*/_all_docs", "POST", async (incoming: IActiveHttpIncoming) => {
273
- // Get Database
274
- let db = getDB(incoming.url[0]);
275
- return await db.allDocs(
276
- prepareAllDocs(Object.assign(incoming.query, incoming.body))
277
- );
278
- });
279
-
280
- // Bit of a proxy call to focus on umids
281
- http.use("*/umids", "GET", async (incoming: IActiveHttpIncoming) => {
282
- return getTransactionUmids(incoming);
283
- });
284
-
285
- http.use("*/transactions", "GET", async (incoming: IActiveHttpIncoming) => {
286
- return getTransactionUmids(incoming);
287
- });
288
-
289
- // Filters the call to focus on umid transaction (assuming right database!)
290
- const getTransactionUmids = async (incoming: IActiveHttpIncoming) => {
291
- // Get Database
292
- let db = getDB(incoming.url[0]);
293
- // Lets make sure it starts with umid:
294
- if (incoming.query["from"].startsWith("umid:")) {
295
- incoming.query["startkey"] = incoming.query["from"];
296
- } else {
297
- incoming.query["startkey"] = `umid:${incoming.query["from"]}`;
298
- }
299
- const txs = (await db.allDocs(prepareAllDocs(incoming.query))) as any;
300
- for (let i = txs.rows.length; i--;) {
301
- const [timestamp, umid] = txs.rows[i]._id.split(",");
302
- txs.rows[i] = {
303
- umid,
304
- timestamp: timestamp.replace("umid:", ""),
305
- };
306
- }
307
- return txs;
308
- };
309
-
310
- /**
311
- * C/Pouch conversion utility tool for _all_docs
312
- *
313
- * @param {*} options
314
- * @returns
315
- */
316
- const prepareAllDocs = (options: any) => {
317
- // Convert Limit
318
- if (options.limit) {
319
- options.limit = parseInt(options.limit) || null;
320
- }
321
-
322
- // Convert startkey to remove "
323
- if (options.startkey) {
324
- options.startkey = options.startkey.replace(/"/g, "");
325
- }
326
-
327
- // Convert endkey to remove " and keep unicode search
328
- if (options.endkey) {
329
- options.endkey = options.endkey.replace(/"/g, "");
330
- options.endkey = options.endkey.slice(0, -1) + "\u9999";
331
- }
332
-
333
- // Filter prefixes
334
- // Leaving this in even though filtering in permissionsChecker as well
335
- if (options.keys) {
336
- options.keys = options.keys.map((id: string) => filterPrefix(id));
337
- }
338
- return options;
339
- };
340
-
341
- /**
342
- * Reusable Get for mutliple paths
343
- *
344
- * @param {string} dbLoc
345
- * @param {string} path
346
- * @returns {Promise<any>}
347
- */
348
- const genericGet = (dbLoc: string, path: string): Promise<any> => {
349
- // Get Database
350
- return new Promise(async (resolve, reject) => {
351
- // Get Database
352
- const db = getDB(dbLoc);
353
- // Filter
354
- db.get(filterPrefix(decodeURIComponent(path), dbLoc))
355
- .then((doc: unknown) => resolve(doc))
356
- .catch((e: unknown) => {
357
- reject(e);
358
- });
359
- });
360
- };
361
-
362
- /**
363
- * Reusable delete (purge like) for mutliple paths
364
- * TODO: Allow recusive delete docName []
365
- *
366
- * @param {string} dbLoc
367
- * @param {string} path
368
- * @returns {Promise<any>}
369
- */
370
- const genericDelete = (dbLoc: string, docName: string): Promise<any> => {
371
- return new Promise(async (resolve, reject) => {
372
- // Get Database
373
- const db = getDB(dbLoc);
374
- try {
375
- await db.del(filterPrefix(docName, dbLoc));
376
- return resolve({ success: "ok" });
377
- } catch (e) {
378
- return reject(e);
379
- }
380
- });
381
- };
382
-
383
- // TODO : Verify request source
384
- http.use("*/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
385
- return await genericDelete(
386
- incoming.url[0],
387
- decodeURIComponent(incoming.url[1])
388
- );
389
- });
390
-
391
- // Start compacting process
392
- http.use("*/_compact", "GET", async (incoming: IActiveHttpIncoming) => {
393
- const db = getDB(incoming.url[0]);
394
- return await db.compact();
395
- });
396
-
397
- // Direct sequence get
398
- http.use("*/_seq/*", "GET", async (incoming: IActiveHttpIncoming) => {
399
- const db = getDB(incoming.url[0]);
400
- return await db.getSeq(incoming.url[2]);
401
- });
402
-
403
- // Direct sequence delete
404
- http.use("*/_seq/*", "DELETE", async (incoming: IActiveHttpIncoming) => {
405
- const db = getDB(incoming.url[0]);
406
- try {
407
- await db.delSeq([incoming.url[2]]);
408
- return { success: "ok" };
409
- } catch {
410
- throw new Error("Seq Delete Failed");
411
- }
412
- });
413
-
414
- // filename comes straight from the request body here - without this
415
- // check it's an arbitrary file write/read primitive (LevelMe.backup()/
416
- // restore() pass it directly to createWriteStream()/createReadStream()
417
- // with no validation of their own, since they're also called from the
418
- // CLI's --backup/--restore flags where an operator-chosen path outside
419
- // the current directory is legitimate - the restriction belongs here,
420
- // at the boundary where the value is actually untrusted remote input,
421
- // not inside LevelMe itself). Restricting to a bare filename (no path
422
- // separators, no ".." components) blocks traversal and absolute paths
423
- // while still allowing any normal filename.
424
- const isSafeFilename = (filename: unknown): filename is string =>
425
- typeof filename === "string" &&
426
- filename.length > 0 &&
427
- path.basename(filename) === filename;
428
-
429
- http.use("*/_backup", "POST", async (incoming: IActiveHttpIncoming) => {
430
- const filename = incoming.body?.filename;
431
- if (filename !== undefined && !isSafeFilename(filename)) {
432
- throw new Error("Invalid backup filename");
433
- }
434
- getDB(incoming.url[0]).backup(filename);
435
- return { status: "started" };
436
- });
437
-
438
- http.use("*/_restore", "POST", async (incoming: IActiveHttpIncoming) => {
439
- const filename = incoming.body?.filename;
440
- if (!isSafeFilename(filename)) {
441
- throw new Error("Invalid restore filename");
442
- }
443
- getDB(incoming.url[0]).restore(filename);
444
- return { status: "started" };
445
- });
446
-
447
- // Get specific docs from a database
448
- http.use("*/*", "GET", async (incoming: IActiveHttpIncoming) => {
449
- return await genericGet(incoming.url[0], incoming.url[1]);
450
- });
451
-
452
- // Gets raw unparsed document straight from leveldb
453
- // Document Store http://localhost:5259/activeledger/_raw/[document._id]
454
- // Sequence (data) http://localhost:5259/activeledger/_raw/[document._id]@[revision] (revision Example = 10-f0b21ef22ec86f7c6d71c250b4563bba)
455
- http.use("*/_raw/*", "GET", async (incoming: IActiveHttpIncoming) => {
456
- // Getting revision?
457
- const [root, revision] = incoming.url[2].split("@");
458
-
459
- // Get Database
460
- const db = getDB(incoming.url[0]);
461
-
462
- // Get Main Doc
463
- const rootDoc = await db.get(decodeURIComponent(root), true);
464
-
465
- // If getting revision we need sequence
466
- if (revision) {
467
- try {
468
- return await db.getSeq(rootDoc.rev_map[revision]);
469
- } catch (e) {
470
- throw new Error("Revision Fetch Failed");
471
- }
472
- }
473
- return rootDoc;
474
- });
475
-
476
- // Add new / updated document to the database
477
- http.use("*/**", "PUT", async (incoming: IActiveHttpIncoming) => {
478
- // Archive Document?
479
- //await markAsArchived(incoming);
480
- return genericPut(incoming.url[0], incoming.body);
481
- });
482
-
483
- /**
484
- * Detects if root pouch document needs to be archived and archives
485
- *
486
- * @param {IActiveHttpIncoming} incoming
487
- * @returns {Promise<boolean>}
488
- */
489
- // const markAsArchived = async (
490
- // incoming: IActiveHttpIncoming
491
- // ): Promise<boolean> => {
492
- // // No more btree archives, return false for now
493
- // return false;
494
-
495
- // const position = isArchivable(incoming.body._rev);
496
- // if (position) {
497
- // try {
498
- // // Get Database
499
- // const db = getDB(incoming.url[0]);
500
-
501
- // // Raw meta doc
502
- // const metaDoc = await db.get(decodeURIComponent(incoming.url[1]), true);
503
-
504
- // // Make sure archive database exists
505
- // const archDb = await getCreateArchiveDb(incoming.url[0]);
506
-
507
- // // Time to modify!
508
- // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
509
-
510
- // // Prevent clashes
511
- // metaDoc._id += ":" + position;
512
-
513
- // // We could add to existing but then we would archive the archive
514
- // // Write document to archive
515
- // await archDb.post(checkDoc(metaDoc));
516
-
517
- // // Rewrite meta root document
518
- // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
519
- // return true;
520
- // } catch (e) {
521
- // // Any errors continue
522
- // return false;
523
- // }
524
- // }
525
- // return false;
526
- // };
527
-
528
- /**
529
- * Pouchdb Metadoc rewrite (trimming)
530
- *
531
- * @param {*} metaDoc
532
- * @param {string} position
533
- * @returns
534
- */
535
- // const prepareArchiveDoc = (metaDoc: any, position: number) => {
536
- // // Time to modify!
537
- // const newMetaDoc = {
538
- // _id: metaDoc._id,
539
- // rev_tree: [
540
- // {
541
- // // pos: metaDoc.rev_tree[0].pos,
542
- // pos: position - 1, // Get parent starting pos
543
- // ids: getLastInTree(metaDoc.rev_tree[0].ids),
544
- // },
545
- // ],
546
- // rev_map: {
547
- // [metaDoc.winningRev]: metaDoc.rev_map[metaDoc.winningRev],
548
- // },
549
- // winningRev: metaDoc.winningRev,
550
- // deleted: metaDoc.deleted,
551
- // seq: metaDoc.seq,
552
- // };
553
- // return newMetaDoc;
554
- // };
555
-
556
- /**
557
- * Create Archive Database
558
- *
559
- * @param {string} name
560
- * @returns
561
- */
562
- // const getCreateArchiveDb = async (name: string): Promise<LevelMe> => {
563
- // const archDb = getDB(name + "_archive");
564
- // await archDb.info();
565
- // return archDb;
566
- // };
567
-
568
- /**
569
- * Gets the last 2 leafs in the tree (parent, current)
570
- *
571
- * @param {any[]} tree
572
- * @param {any[]} [prevTree]
573
- * @returns {*}
574
- */
575
- // const getLastInTree = (tree: any[], prevTree?: any[]): any => {
576
- // if (tree[2].length) {
577
- // return getLastInTree(tree[2][0], tree);
578
- // }
579
- // return prevTree ? prevTree : tree;
580
- // };
581
-
582
- /**
583
- * Should we Archive the document
584
- *
585
- * @param {string} revString
586
- * @returns {number}
587
- */
588
- // const isArchivable = (revString: string): number => {
589
- // if (revString) {
590
- // const position = parseInt(revString.split("-")[0]);
591
-
592
- // // Set at 300 to have 3 attempts before default 1000 id failure error triggered above
593
- // //return position % 300 === 0 ? position : 0;
594
- // return position % 15 === 0 ? position : 0;
595
- // }
596
- // return 0;
597
- // };
598
-
599
- /**
600
- * Reusable Get for mutliple paths
601
- *
602
- * @param {string} dbLoc
603
- * @param {string} path
604
- * @returns {Promise<any>}
605
- */
606
- const genericPut = async (dbLoc: string, body: string): Promise<any> => {
607
- // Get Database
608
- let db = getDB(dbLoc);
609
- try {
610
- return await db.put(body);
611
- } catch (e) {
612
- e.reason = e.message;
613
- throw e;
614
- }
615
- };
616
-
617
- // Filters any prefix (so they're virtual) (: reserved character) to real stream id
618
- const filterPrefix = (stream: string, db?: string): string => {
619
-
620
- // Only need to filter activeledger, making optional just in case not passed
621
- if (db && db !== 'activeledger') {
622
- return stream;
623
- } else {
624
- // Remove any suffix like :volatile :stream :umid
625
- let [streamId, suffix] = stream.split(":");
626
-
627
- // If id length more than 64 trim the start
628
- if (streamId.length > 64 && !streamId.startsWith("0x")) {
629
- streamId = streamId.slice(-64);
630
- }
631
-
632
- // If suffix add it back to return
633
- if (suffix) {
634
- return streamId + ":" + suffix;
635
- }
636
-
637
- // Return just the id
638
- return streamId;
639
- }
640
- };
641
-
642
- // Add new / updated document to the database with auto id
643
- http.use("*", "POST", async (incoming: IActiveHttpIncoming) => {
644
- // Get Database
645
- let db = getDB(incoming.url[0]);
646
-
647
- // Archive Document?
648
- //await markAsArchived(incoming);
649
-
650
- try {
651
- return await db.post(checkDoc(incoming.body));
652
- } catch (e) {
653
- e.reason = e.message;
654
- throw e;
655
- }
656
- });
657
-
658
- http.use(
659
- "*/events",
660
- "GET",
661
- async (
662
- incoming: IActiveHttpIncoming,
663
- req: http.IncomingMessage,
664
- res: IActiveHttpResponse
665
- ) => {
666
- // Get Database
667
- let db = getDB(incoming.url[0]);
668
-
669
- const sse = new SSE(res);
670
- const lastEventId = req.headers["Last-Event-ID"];
671
-
672
- // Fetch anything newer since then and push
673
- if (lastEventId) {
674
- // Merge with getTransactionUmids as there is little difference
675
- const events = (await db.allDocs(
676
- prepareAllDocs({
677
- startkey: `event:${lastEventId}`,
678
- endkey: `event:`,
679
- include_docs: true,
680
- })
681
- )) as any;
682
- // Start at 1 to skip the event that was already sent
683
- for (let i = 1; i < events.rows.length; i++) {
684
- const id = events.rows[i]._id.replace("event:", "");
685
- delete events.rows[i]._id;
686
- delete events.rows[i]._rev;
687
- sse.write(id, events.rows[i]);
688
- }
689
- }
690
-
691
- // Listener Process event (to turn off)
692
- const listener = (change: any) => {
693
- if (change.id.startsWith("event:")) {
694
- delete change.doc._id;
695
- delete change.doc._rev;
696
- if (!sse.write(change.id.replace("event:", ""), change.doc)) {
697
- cancelChanges();
698
- }
699
- }
700
- };
701
-
702
- // Stop listening for changes
703
- const cancelChanges = () => {
704
- changes.off("change", listener);
705
- };
706
-
707
- // Run on disconnect immediately, rather than only being discovered
708
- // reactively the next time a change event fires and listener() sees
709
- // sse.write() return false - on a quiet database that could be a long
710
- // time (or never) after the client actually disconnects.
711
- sse.onDisconnect(cancelChanges);
712
-
713
- // Listening for changes
714
- let changes = db.changes().on("change", listener);
715
-
716
- return "handled";
717
- }
718
- );
719
-
720
- // Listen for changes on the database
721
- http.use(
722
- "*/_changes",
723
- "GET",
724
- async (
725
- incoming: IActiveHttpIncoming,
726
- req: http.IncomingMessage,
727
- res: IActiveHttpResponse
728
- ) => {
729
- // Get Database
730
- let db = getDB(incoming.url[0]);
731
-
732
- // Limit check
733
- if (incoming.query.limit < 1) {
734
- incoming.query.limit = 1;
735
- }
736
-
737
- // Convert since into number if not now
738
- if (incoming.query.since !== "now") {
739
- incoming.query.since = parseInt(incoming.query.since);
740
- }
741
-
742
- if (
743
- incoming.query.feed === "continuous" ||
744
- incoming.query.feed === "longpoll"
745
- ) {
746
- if (incoming.query.feed === "continuous") {
747
- // Currently we do not do continuous
748
- } else {
749
- // Long polling heartbeat - armed only once the body is actually
750
- // open for writing, further down.
751
- let hBInterval: ReturnType<typeof setInterval> | null = null;
752
-
753
- // Clean up
754
- let cleanUp = () => {
755
- if (hBInterval) {
756
- clearInterval(hBInterval);
757
- hBInterval = null;
758
- }
759
- };
760
-
761
- // `res` here is the RAW uWebSockets.js HttpResponse (see
762
- // IActiveHttpResponse/@activeledger/httpd/src/httpd.ts -
763
- // it constructs a Node-like synthetic object for `req`, but
764
- // passes `res` straight through unwrapped). The `http`/
765
- // `http.ServerResponse` types this file imports for `req`/`res`
766
- // are Node's own - never real here, a holdover from before this
767
- // framework's uWS.js migration. Two real, compounding bugs
768
- // followed from believing them: res.setHeader() doesn't exist on
769
- // a real uWS HttpResponse (a prior "fix", 425ccc6 "make sse work
770
- // again!", replaced a setHeader() call with two res.write() calls
771
- // writing literal "HTTP/1.1 200 OK\r\n\r\n" text into the BODY
772
- // instead - also wrong, since write() there is real and does
773
- // append to the body); and every write from here on happens in a
774
- // LATER tick than the request handler's own synchronous top
775
- // portion (a .then() callback, an EventEmitter listener firing
776
- // whenever a future change occurs) - uWS requires those to be
777
- // wrapped in res.cork(), same as the already-working /events SSE
778
- // handler (sse.ts) already does, or the write silently corrupts/
779
- // fails. None of this was ever caught because BOTH bugs (and two
780
- // more besides - see levelme.ts's since="now" and seq fixes)
781
- // left the response either hanging forever or throwing before
782
- // any client ever got as far as trying to parse a body - so nothing
783
- // ever actually exercised this branch successfully until all of
784
- // them were fixed together.
785
- res.cork(() => {
786
- res.writeHeader("Content-type", ActiveHttpd.mimeType[".json"]);
787
- });
788
-
789
- // Read Type
790
- incoming.query.live = incoming.query.continuous = false;
791
-
792
- db.changesFromSeq(incoming.query).then((complete: any) => {
793
- if (complete.results.length) {
794
- if (res.writable) {
795
- res.cork(() => {
796
- res.write(JSON.stringify(complete));
797
- res.end();
798
- });
799
- }
800
- cleanUp();
801
- } else {
802
- // do the longpolling
803
- // mimicking CouchDB, start sending the JSON immediately
804
- if (res.writable) {
805
- res.cork(() => {
806
- res.write('{"results":[\n');
807
- });
808
- }
809
- incoming.query.live = incoming.query.continuous = true;
810
-
811
- // Keep bytes flowing while nothing is changing.
812
- //
813
- // This write was commented out, and that is what made a quiet
814
- // feed fatal rather than merely quiet. The response holds open
815
- // with nothing on the wire, so the client's undici bodyTimeout
816
- // (300s - ActiveRequest's default, packages/utilities) fires and
817
- // the round comes back with no body. Downstream,
818
- // ActiveDSChanges.listen() dereferenced response.data.results on
819
- // that null and threw, and nothing re-armed the loop - so the
820
- // changes feed died for the lifetime of the process. Every five
821
- // idle minutes was enough. Live-confirmed: nano-gateway kept its
822
- // SSE socket open and heartbeating to its own subscribers the
823
- // whole time, so every health signal said the link was fine
824
- // while no change could ever arrive again. Background push on a
825
- // real device stopped and nothing anywhere reported a fault.
826
- //
827
- // A newline is insignificant whitespace between JSON array
828
- // elements, so it cannot corrupt the body being streamed - this
829
- // is exactly what CouchDB sends. It has to be corked: uWS
830
- // requires it for any write from a later tick, and an uncorked
831
- // one is silently dropped or corrupts the stream, which is the
832
- // likeliest reason this was disabled rather than fixed.
833
- //
834
- // Default is well under the client's timeout so an ordinary
835
- // quiet period never reaches it. The client-side guard in
836
- // dsconnect.ts is still required: this reduces how often a bad
837
- // round happens, it does not make one survivable.
838
- hBInterval = setInterval(() => {
839
- if (!res.writable) {
840
- // The client went away mid-longpoll. Without this the
841
- // interval and the change listener both leak for the
842
- // lifetime of the process, one per abandoned request.
843
- cleanUp();
844
- cancelChanges();
845
- return;
846
- }
847
- res.cork(() => {
848
- res.write("\n");
849
- });
850
- }, Number(incoming.query.heartbeat) || 30000);
851
-
852
- // Listener Process event (to turn off)
853
- // A longpoll round returns EVERY pending change, not one.
854
- //
855
- // bulkDocs emits one "change" per document in a synchronous
856
- // loop (levelme.ts). The previous listener responded to the
857
- // FIRST emit by writing it, ending the response and detaching
858
- // itself - all synchronously - so every remaining document in
859
- // the same commit found no listener at all. With no sequence
860
- // backfill (changesFromSeq can never return rows, see its own
861
- // comment) those changes were gone permanently: `since` moved
862
- // past them and nothing could replay them.
863
- //
864
- // For a ledger that means an ordinary multi-stream transaction -
865
- // a transfer touching sender and recipient - notified about one
866
- // of them. Which one was arbitrary: levelme emits backwards, so
867
- // it was the LAST document written. Downstream that reads as an
868
- // intermittent push bug rather than as data loss.
869
- //
870
- // So changes are accumulated and flushed on the next tick. Every
871
- // emit from one commit is synchronous, so a single setImmediate
872
- // catches the whole batch without waiting for anything.
873
- let pending: any[] = [];
874
- let flushScheduled = false;
875
-
876
- const flush = () => {
877
- try {
878
- if (!res.writable || pending.length === 0) return;
879
- const batch = pending;
880
- pending = [];
881
- // The highest seq in the batch, not the last one written -
882
- // levelme emits in reverse order, so "last" is the oldest.
883
- const lastSeq = batch.reduce(
884
- (max: number, c: any) => (Number(c.seq) > max ? Number(c.seq) : max),
885
- 0
886
- );
887
- // res.end() is load-bearing, not tidiness. Both the write
888
- // and the end used to be commented out, so a client whose
889
- // HTTP layer waits for the response to COMPLETE before
890
- // resolving - ActiveRequest/axios does - hung forever the
891
- // moment a real change occurred, even though the bytes were
892
- // already on the socket. Live-confirmed: it is why a
893
- // nano-gateway subscriber connected fine, kept heartbeating,
894
- // and never received a push for a committed change.
895
- res.cork(() => {
896
- res.write(batch.map((c: any) => JSON.stringify(c)).join(",\n"));
897
- res.write('],\n"last_seq":' + lastSeq + "}\n");
898
- res.end();
899
- });
900
- } finally {
901
- // In a finally so a throw while writing - an unserialisable
902
- // doc, a discarded uWS response - cannot leak the heartbeat
903
- // interval and the change listener for the process lifetime.
904
- cleanUp();
905
- cancelChanges();
906
- }
907
- };
908
-
909
- const listener = (change: any) => {
910
- // Deliberately does no writability check of its own - flush()
911
- // does it once, for the whole batch, immediately before
912
- // writing. (For the record on `res.writable`: it is the real,
913
- // framework-maintained "is this connection still open" flag,
914
- // unlike req.connection.destroyed, which this handler used to
915
- // consult and which was always undefined - the httpd
916
- // framework's synthetic `req` carries only
917
- // connection.remoteAddress, so that check never gated anything
918
- // and never threw. sse.ts relies on res.writable too.)
919
- pending.push(change);
920
- if (!flushScheduled) {
921
- flushScheduled = true;
922
- setImmediate(flush);
923
- }
924
- };
925
-
926
-
927
- // Stop listening for changes
928
- const cancelChanges = () => {
929
- changes.off("change", listener);
930
- };
931
-
932
- // Listening for changes
933
- let changes = db.changes().on("change", listener);
934
- }
935
- }).catch((error: unknown) => {
936
- // Previously unhandled - a rejection here (e.g. a malformed
937
- // `since`) surfaced only as an opaque 500 with an empty {}
938
- // body and no indication anything had actually gone wrong
939
- // server-side, all the way up through whatever HTTP client hit
940
- // this route. Finalise the response with a real error instead
941
- // of leaving it hanging or silently swallowed.
942
- if (res.writable) {
943
- res.cork(() => {
944
- res.writeStatus("500");
945
- res.write(JSON.stringify({ error: String(error) }));
946
- res.end();
947
- });
948
- }
949
- cleanUp();
950
- });
951
- }
952
- return "handled";
953
- } else {
954
- // Just get the latest
955
- return await db.changesFromSeq(incoming.query);
956
- }
957
- }
958
- );
959
-
960
- // Add new / updated BULK document to the database
961
- http.use("*/_bulk_docs", "POST", async (incoming: IActiveHttpIncoming) => {
962
- // Can no longer be an array
963
- if (!Array.isArray(incoming.body)) {
964
- // Must have docs
965
- if (incoming.body.docs) {
966
- // Get Database
967
- let db = getDB(incoming.url[0]);
968
-
969
- // Prepare Archive database
970
- // Make sure archive database exists
971
- //const archDb = await getCreateArchiveDb(incoming.url[0]);
972
-
973
- // We need to see if the docs need archiving
974
- // for (let i = incoming.body.docs.length; i--; ) {
975
- // const doc = incoming.body.docs[i];
976
- // const position = isArchivable(doc._rev);
977
- // if (position) {
978
- // try {
979
- // // Raw meta doc
980
- // const metaDoc = await db.get(doc._id, true);
981
-
982
- // // Time to modify!
983
- // const newMetaDoc = prepareArchiveDoc(metaDoc, position);
984
-
985
- // // Prevent clashes
986
- // metaDoc._id += ":" + position;
987
-
988
- // // We could add to existing but then we would archive the archive
989
- // // Write document to archive
990
- // await archDb.post(checkDoc(metaDoc));
991
-
992
- // // Rewrite meta root document
993
- // await db.writeRaw(newMetaDoc._id, JSON.stringify(newMetaDoc));
994
- // } catch (e) {
995
- // // Ignore errors and continue
996
- // }
997
- // }
998
- // }
999
-
1000
- // Bulk Insert
1001
- if (await db.bulkDocs(incoming.body.docs, incoming.body.options)) {
1002
- return {
1003
- ok: true,
1004
- };
1005
- } else {
1006
- return {
1007
- ok: false,
1008
- };
1009
- }
1010
- }
1011
- }
1012
- });
1013
-
1014
- // Find API
1015
- http.use("*/_find", "POST", async (incoming: IActiveHttpIncoming) => {
1016
- return { error: "find is deprecated" };
1017
- });
1018
-
1019
- // Explain
1020
- http.use("*/_explain", "POST", async (incoming: IActiveHttpIncoming) => {
1021
- return { error: "explain is deprecated" };
1022
- });
1023
-
1024
- // Fauxton
1025
- const fauxton = async (
1026
- incoming: IActiveHttpIncoming,
1027
- req: {
1028
- headers: {
1029
- [index: string]: string;
1030
- };
1031
- method: string;
1032
- url: string;
1033
- connection: {
1034
- remoteAddress: string;
1035
- };
1036
- },
1037
- res: IActiveHttpResponse
1038
- ) => {
1039
- // We want to force /_utils to /_utils/ as this is the CouchDB behavior
1040
- if (req.url === "/_utils") {
1041
- // res.writeHead(301, {
1042
- // Location: "/_utils/",
1043
- // });
1044
- // return;
1045
- req.url = "/_utils/";
1046
- }
1047
-
1048
- // File to send
1049
- let file = FAUXTON_PATH + "/index.html";
1050
-
1051
- // If path is not default overwrite
1052
- if (req.url !== "/_utils/") {
1053
- file = FAUXTON_PATH + (req.url as string).replace("/_utils", "");
1054
- }
1055
-
1056
- if (fauxtonCache[file]) {
1057
- return fauxtonCache[file];
1058
- }
1059
-
1060
- try {
1061
- await fsPromises.access(file);
1062
- const data = await fsPromises.readFile(file);
1063
- const response = {
1064
- mime: `${ActiveHttpd.mimeType[path.parse(file).ext] || "text/plain"}`,
1065
- data: data as Buffer,
1066
- }
1067
- fauxtonCache[file] = response;
1068
- return response;
1069
- } catch {
1070
- // Not found, will result in a 404 from httpd
1071
- }
1072
- };
1073
- http.use("_utils", "GET", fauxton);
1074
- http.use("_utils/**", "GET", fauxton);
1075
-
1076
- // Start Server
1077
- http.listen(parseInt(PORT));
1078
- })();