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