@live-change/db-server 0.9.83 → 0.9.85

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.
@@ -0,0 +1,423 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import http from 'http';
4
+ import express from 'express';
5
+ import sockjs from '@live-change/sockjs';
6
+ import { server as WebSocketServer } from 'websocket';
7
+ import { server as ReactiveDaoWebsocketServer, client as ReactiveDaoWebsocketClient } from '@live-change/dao-websocket';
8
+ import ScriptContext from '@live-change/db/lib/ScriptContext.js';
9
+ import * as dbDao from './dbDao.js';
10
+ import * as storeDao from './storeDao.js';
11
+ import createBackend from './backend.js';
12
+ import Replicator from './Replicator.js';
13
+ import { profileLog } from '@live-change/db';
14
+ import { Database } from '@live-change/db';
15
+ import ReactiveDao from '@live-change/dao';
16
+ import { fileURLToPath } from 'url';
17
+ const packageInfo = await fs.promises.readFile(fileURLToPath(new URL(import.meta.resolve('@live-change/db-server/package.json'))), 'utf8');
18
+ import Debug from 'debug';
19
+ const debug = Debug('db-server');
20
+ class DatabaseStore {
21
+ constructor(path, backends, options) {
22
+ this.path = path;
23
+ this.backends = backends;
24
+ this.stores = new Map();
25
+ this.dbs = {};
26
+ this.dbs.default = this.backends.default.createDb(path, options);
27
+ }
28
+ close() {
29
+ for (let key in this.dbs) {
30
+ return this.backends[key].closeDb(this.dbs[key]);
31
+ }
32
+ }
33
+ delete() {
34
+ for (let key in this.dbs) {
35
+ return this.backends[key].deleteDb(this.dbs[key]);
36
+ }
37
+ }
38
+ getStore(name, options = {}) {
39
+ let store = this.stores.get(name);
40
+ if (store)
41
+ return store;
42
+ const backendName = options.backend ?? (options.memory ? 'memory' : 'default');
43
+ if (!this.backends[backendName]) {
44
+ throw new Error(`db ${path} backend ${backendName} not configured`);
45
+ }
46
+ if (!this.dbs[backendName]) {
47
+ this.dbs[backendName] = this.backends[backendName].createDb(this.path, options);
48
+ }
49
+ store = this.backends[backendName].createStore(this.dbs[backendName], name, options);
50
+ store.backendName = backendName;
51
+ this.stores.set(name, store);
52
+ return store;
53
+ }
54
+ closeStore(name) {
55
+ let store = this.stores.get(name);
56
+ if (!store)
57
+ return;
58
+ return this.backends[store.backendName].closeStore(store);
59
+ }
60
+ deleteStore(name) {
61
+ let store = this.getStore(name);
62
+ return this.backends[store.backendName].deleteStore(store);
63
+ }
64
+ }
65
+ class Server {
66
+ constructor(config) {
67
+ this.config = config;
68
+ this.databases = new Map();
69
+ this.metadata = null;
70
+ this.databaseStores = new Map();
71
+ this.metadataSavePromise = null;
72
+ this.databasesListObservable = new ReactiveDao.ObservableList([]);
73
+ this.databasesListObservable.observe(() => { }); // prevent dispose and clear
74
+ this.apiServer = new ReactiveDao.ReactiveServer((sessionId) => this.createDao(sessionId));
75
+ this.backends = {};
76
+ if (config.backend && !this.backends.default) { // backward compatibility
77
+ this.backends.default = createBackend({
78
+ name: config.backend,
79
+ url: config.backendUrl,
80
+ maxDbs: config.maxDbs,
81
+ maxDbSize: config.maxDbSize,
82
+ });
83
+ }
84
+ for (let backend of config.backends || []) {
85
+ if (typeof backend == 'string') {
86
+ backend = { name: backend };
87
+ }
88
+ this.backends[backend.name] = createBackend(backend);
89
+ if (!this.backends.default) {
90
+ this.backends.default = this.backends[backend.name];
91
+ }
92
+ }
93
+ if (!this.backends.default) {
94
+ throw new Error("No default backend configured");
95
+ }
96
+ if (!this.backends.memory) {
97
+ this.backends.memory = createBackend({
98
+ name: "memory"
99
+ });
100
+ }
101
+ if (this.config.master) {
102
+ this.masterDao = new ReactiveDao('app', {
103
+ remoteUrl: this.config.master,
104
+ protocols: {
105
+ 'ws': ReactiveDaoWebsocketClient
106
+ },
107
+ connectionSettings: {
108
+ queueRequestsWhenDisconnected: true,
109
+ requestSendTimeout: 2000,
110
+ requestTimeout: 5000,
111
+ queueActiveRequestsOnDisconnect: false,
112
+ autoReconnectDelay: 200,
113
+ logLevel: 1
114
+ },
115
+ database: {
116
+ type: 'remote',
117
+ generator: ReactiveDao.ObservableList
118
+ },
119
+ store: {
120
+ type: 'remote',
121
+ generator: ReactiveDao.ObservableList
122
+ }
123
+ });
124
+ this.replicator = new Replicator(this);
125
+ }
126
+ }
127
+ createDaoConfig(session) {
128
+ const store = {
129
+ type: 'local',
130
+ source: new ReactiveDao.SimpleDao({
131
+ methods: {
132
+ ...(profileLog.started
133
+ ? profileLog.profileFunctions(storeDao.localRequests(this))
134
+ : storeDao.localRequests(this))
135
+ },
136
+ values: {
137
+ ...storeDao.localReads(this)
138
+ }
139
+ })
140
+ };
141
+ const version = {
142
+ type: 'local',
143
+ source: new ReactiveDao.SimpleDao({
144
+ methods: {},
145
+ values: {
146
+ version: {
147
+ async observable() {
148
+ return new ReactiveDao.ObservableValue((await packageInfo).version);
149
+ },
150
+ async get() {
151
+ return (await packageInfo).version;
152
+ }
153
+ }
154
+ }
155
+ })
156
+ };
157
+ const emptyServices = {
158
+ observable(parameters) {
159
+ return ReactiveDao.ObservableList([]);
160
+ },
161
+ async get(parameters) {
162
+ return [];
163
+ }
164
+ };
165
+ const sessionInfo = {
166
+ client: { session: 'dbRoot' },
167
+ services: []
168
+ };
169
+ const metadata = {
170
+ type: "local",
171
+ source: new ReactiveDao.SimpleDao({
172
+ methods: {},
173
+ values: {
174
+ serviceNames: emptyServices,
175
+ serviceDefinitions: emptyServices,
176
+ api: {
177
+ observable(parameters) {
178
+ ReactiveDao.ObservableValue(sessionInfo);
179
+ },
180
+ async get(parameters) {
181
+ return sessionInfo;
182
+ }
183
+ }
184
+ }
185
+ })
186
+ };
187
+ const scriptContext = new ScriptContext({
188
+ /// TODO: script available routines
189
+ console
190
+ });
191
+ let database;
192
+ if (this.config.master) {
193
+ database = {
194
+ type: 'local',
195
+ source: new ReactiveDao.SimpleDao({
196
+ methods: {
197
+ ...(profileLog.started
198
+ ? profileLog.profileFunctions(dbDao.remoteRequests(this))
199
+ : dbDao.remoteRequests(this))
200
+ },
201
+ values: {
202
+ ...dbDao.localReads(this, scriptContext)
203
+ }
204
+ })
205
+ };
206
+ }
207
+ else {
208
+ database = {
209
+ type: 'local',
210
+ source: new ReactiveDao.SimpleDao({
211
+ methods: {
212
+ ...(profileLog.started
213
+ ? profileLog.profileFunctions(dbDao.localRequests(this, scriptContext))
214
+ : dbDao.localRequests(this, scriptContext))
215
+ },
216
+ values: {
217
+ ...dbDao.localReads(this, scriptContext)
218
+ }
219
+ })
220
+ };
221
+ }
222
+ return {
223
+ //remoteUrl: this.config.master,
224
+ database,
225
+ serverDatabase: database,
226
+ store, version, metadata
227
+ };
228
+ }
229
+ createDao(session) {
230
+ return new ReactiveDao(session, {
231
+ ...this.createDaoConfig(session),
232
+ });
233
+ }
234
+ async initialize(initOptions = {}) {
235
+ if (!this.config.temporary) {
236
+ const normalMetadataPath = path.resolve(this.config.dbRoot, 'metadata.json');
237
+ const backupMetadataPath = path.resolve(this.config.dbRoot, 'metadata.json.bak');
238
+ const normalMetadataExists = await fs.promises.access(normalMetadataPath).catch(err => false);
239
+ const backupMetadataExists = await fs.promises.access(backupMetadataPath).catch(err => false);
240
+ if (initOptions.forceNew && (normalMetadataExists || backupMetadataExists))
241
+ throw new Error("database already exists");
242
+ const normalMetadata = await fs.promises.readFile(normalMetadataPath, "utf8")
243
+ .then(json => JSON.parse(json)).catch(err => null);
244
+ const backupMetadata = await fs.promises.readFile(backupMetadataPath, "utf8")
245
+ .then(json => JSON.parse(json)).catch(err => null);
246
+ this.metadata = normalMetadata;
247
+ if (!normalMetadata)
248
+ this.metadata = backupMetadata;
249
+ if (this.metadata && backupMetadata && this.metadata.timestamp < backupMetadata.timestamp)
250
+ this.metadata = backupMetadata;
251
+ if (!this.metadata && (normalMetadataExists || backupMetadataExists))
252
+ throw new Error("database is broken");
253
+ }
254
+ if (!this.metadata) {
255
+ this.metadata = {
256
+ databases: {
257
+ system: {
258
+ tables: {},
259
+ indexes: {},
260
+ logs: {}
261
+ }
262
+ }
263
+ };
264
+ }
265
+ for (const dbName in this.metadata.databases) {
266
+ const dbConfig = this.metadata.databases[dbName];
267
+ this.databases.set(dbName, await this.initDatabase(dbName, dbConfig));
268
+ this.databasesListObservable.push(dbName);
269
+ }
270
+ await this.checkInfoIntegrity();
271
+ if (this.config.master) {
272
+ await this.replicator.start();
273
+ }
274
+ }
275
+ async checkInfoIntegrity() {
276
+ for (const dbName in this.metadata.databases) {
277
+ if (dbName !== 'system') {
278
+ const metadata = this.metadata.databases[dbName];
279
+ const indexesTable = this.databases.get('system').table(dbName + '_indexes');
280
+ const indexesInfo = await indexesTable.rangeGet({});
281
+ const infoByName = new Map();
282
+ for (const indexInfo of indexesInfo) {
283
+ const indexMeta = metadata?.indexes[indexInfo.name];
284
+ if (!indexMeta || (indexMeta.uid !== indexInfo.id)) {
285
+ console.error("CORRUPTED INDEX INFO", indexInfo.name, indexInfo.id);
286
+ console.log("DELETING CORRUPTED INFO");
287
+ await indexesTable.delete(indexInfo.id);
288
+ continue;
289
+ }
290
+ else if (infoByName.has(indexInfo.name)) {
291
+ console.error("INDEX INFO DUPLIACTED", indexInfo.name);
292
+ console.log("DB META");
293
+ console.log("INFO", indexInfo);
294
+ }
295
+ infoByName.set(indexInfo.name, indexInfo);
296
+ }
297
+ }
298
+ }
299
+ }
300
+ async initDatabase(dbName, dbConfig) {
301
+ const dbPath = this.config.temporary ? 'memory' : path.resolve(this.config.dbRoot, dbName + '.db');
302
+ let dbStore = this.databaseStores.get(dbName);
303
+ if (!dbStore) {
304
+ debug("CREATE DB", dbPath, dbConfig.storage);
305
+ const backend = this.backends[dbConfig.backend?.name ?? dbConfig.backend ?? 'default'];
306
+ dbStore = new DatabaseStore(dbPath, { ...this.backends, default: backend }, typeof dbConfig.backend == 'object' ? dbConfig.backend : dbConfig.storage);
307
+ this.databaseStores.set(dbName, dbStore);
308
+ }
309
+ const database = new Database(dbConfig, (name, config) => dbStore.getStore(name, config), (configToSave) => {
310
+ this.metadata.databases[dbName] = configToSave;
311
+ this.saveMetadata();
312
+ }, (name) => dbStore.deleteStore(name), dbName, (context) => new ScriptContext(context));
313
+ database.onAutoRemoveIndex = (name, uid) => {
314
+ this.databases.get('system').table(dbName + '_indexes').delete(uid);
315
+ };
316
+ this.initializingDatabase = database;
317
+ await database.start(this.config);
318
+ this.initializingDatabase = undefined;
319
+ return database;
320
+ }
321
+ async doSaveMetadata() {
322
+ if (this.config.temporary)
323
+ return;
324
+ //console.log("SAVE METADATA\n"+JSON.stringify(this.metadata, null, " "))
325
+ const normalMetadataPath = path.resolve(this.config.dbRoot, 'metadata.json');
326
+ const backupMetadataPath = path.resolve(this.config.dbRoot, 'metadata.json.bak');
327
+ this.metadata.timestamp = Date.now();
328
+ await fs.promises.writeFile(normalMetadataPath, JSON.stringify(this.metadata, null, " "));
329
+ await fs.promises.writeFile(backupMetadataPath, JSON.stringify(this.metadata, null, " "));
330
+ }
331
+ async saveMetadata() {
332
+ if (this.metadataSavePromise)
333
+ await this.metadataSavePromise;
334
+ this.metadataSavePromise = this.doSaveMetadata();
335
+ await this.metadataSavePromise;
336
+ }
337
+ async getHttp() {
338
+ if (this.http)
339
+ return this.http;
340
+ if (this.httpPromise)
341
+ return this.httpPromise;
342
+ this.httpPromise = (async () => {
343
+ const app = express();
344
+ const sockJsServer = sockjs.createServer({ prefix: '/api/sockjs' });
345
+ sockJsServer.on('connection', (conn) => {
346
+ debug("SOCKJS connection");
347
+ this.apiServer.handleConnection(conn);
348
+ });
349
+ const server = http.createServer(app);
350
+ let wsServer = new WebSocketServer({
351
+ httpServer: server,
352
+ autoAcceptConnections: false,
353
+ maxReceivedFrameSize: 1024 * 1024, // 1 MiB
354
+ maxReceivedMessageSize: 10 * 1024 * 1024, // 10 MiB
355
+ });
356
+ wsServer.on("request", (request) => {
357
+ debug("WS URI", request.httpRequest.url, "FROM", request.remoteAddress);
358
+ if (request.httpRequest.url !== "/api/ws")
359
+ return request.reject();
360
+ let serverConnection = new ReactiveDaoWebsocketServer(request);
361
+ this.apiServer.handleConnection(serverConnection);
362
+ });
363
+ sockJsServer.attach(server);
364
+ this.http = {
365
+ app,
366
+ sockJsServer,
367
+ wsServer,
368
+ server
369
+ };
370
+ return this.http;
371
+ })();
372
+ return this.httpPromise;
373
+ }
374
+ async listen(...args) {
375
+ (await this.getHttp()).server.listen(...args);
376
+ }
377
+ async close() {
378
+ if (this.http) {
379
+ this.http.server.close();
380
+ }
381
+ for (const db of this.databaseStores.values())
382
+ db.close();
383
+ }
384
+ handleUnhandledRejectionInQuery(reason, promise) {
385
+ console.error("ERROR IN USER CODE:", reason.stack, "REASON:", reason, "PROMISE:", promise);
386
+ let rest = reason.stack;
387
+ while (true) {
388
+ const match = rest.match(/\s(userCode:([a-z0-9_.\/-]+):([0-9]+):([0-9]+))\n/i);
389
+ if (match) {
390
+ const path = match[2];
391
+ const line = match[3];
392
+ const column = match[4];
393
+ const pathParts = path.split('/');
394
+ const databaseName = pathParts[0];
395
+ const objectDir = pathParts[1];
396
+ if (objectDir === 'query.js') {
397
+ console.error("error in query to database", databaseName);
398
+ return;
399
+ }
400
+ const database = this.databases.get(databaseName);
401
+ if (objectDir !== 'indexes') {
402
+ console.error(`unknown object dir ${objectDir}, something is wrong, exiting...`);
403
+ process.exit(1);
404
+ }
405
+ const indexName = pathParts[2];
406
+ if (!database) {
407
+ if (this.initializingDatabase.name !== databaseName) {
408
+ console.error('error in non existing database?!', databaseName);
409
+ process.exit(1);
410
+ }
411
+ console.error('error when initializing database', databaseName);
412
+ this.initializingDatabase.handleUnhandledRejectionInIndex(indexName, reason, promise);
413
+ }
414
+ else {
415
+ database.handleUnhandledRejectionInIndex(indexName, reason, promise);
416
+ }
417
+ }
418
+ else
419
+ break;
420
+ }
421
+ }
422
+ }
423
+ export default Server;
@@ -0,0 +1,142 @@
1
+ export default createBackend;
2
+ declare function createBackend({ name, url, maxDbs, mapSize }: {
3
+ name: any;
4
+ url: any;
5
+ maxDbs: any;
6
+ mapSize: any;
7
+ }): {
8
+ levelup: any;
9
+ leveldown: any;
10
+ subleveldown: any;
11
+ encoding: any;
12
+ Store: typeof import("@live-change/db-store-level");
13
+ createDb(path: any, options: any): any;
14
+ closeDb(db: any): void;
15
+ deleteDb(db: any): Promise<void>;
16
+ createStore(db: any, name: any, options: any): any;
17
+ closeStore(store: any): void;
18
+ deleteStore(store: any): Promise<void>;
19
+ rocksdb?: undefined;
20
+ memdown?: undefined;
21
+ lmdb?: undefined;
22
+ connection?: undefined;
23
+ } | {
24
+ levelup: any;
25
+ rocksdb: any;
26
+ subleveldown: any;
27
+ encoding: any;
28
+ Store: typeof import("@live-change/db-store-level");
29
+ createDb(path: any, options: any): any;
30
+ closeDb(db: any): void;
31
+ deleteDb(db: any): Promise<void>;
32
+ createStore(db: any, name: any, options: any): any;
33
+ closeStore(store: any): void;
34
+ deleteStore(store: any): Promise<void>;
35
+ leveldown?: undefined;
36
+ memdown?: undefined;
37
+ lmdb?: undefined;
38
+ connection?: undefined;
39
+ } | {
40
+ levelup: any;
41
+ memdown: any;
42
+ subleveldown: any;
43
+ encoding: any;
44
+ Store: typeof import("@live-change/db-store-level");
45
+ createDb(path: any, options: any): any;
46
+ closeDb(db: any): void;
47
+ deleteDb(db: any): Promise<void>;
48
+ createStore(db: any, name: any, options: any): any;
49
+ closeStore(store: any): void;
50
+ deleteStore(store: any): Promise<void>;
51
+ leveldown?: undefined;
52
+ rocksdb?: undefined;
53
+ lmdb?: undefined;
54
+ connection?: undefined;
55
+ } | {
56
+ Store: typeof rbTreeStore;
57
+ createDb(path: any, options: any): {
58
+ path: any;
59
+ };
60
+ closeDb(db: any): void;
61
+ deleteDb(db: any): Promise<void>;
62
+ createStore(db: any, name: any, options: any): rbTreeStore;
63
+ closeStore(store: any): void;
64
+ deleteStore(store: any): Promise<void>;
65
+ levelup?: undefined;
66
+ leveldown?: undefined;
67
+ subleveldown?: undefined;
68
+ encoding?: undefined;
69
+ rocksdb?: undefined;
70
+ memdown?: undefined;
71
+ lmdb?: undefined;
72
+ connection?: undefined;
73
+ } | {
74
+ lmdb: typeof lmdb;
75
+ Store: typeof lmdbStore;
76
+ createDb(path: any, options: any): lmdb.Env;
77
+ closeDb(db: any): void;
78
+ deleteDb(db: any): Promise<void>;
79
+ createStore(db: any, name: any, options: any): lmdbStore;
80
+ closeStore(store: any): void;
81
+ deleteStore(store: any): Promise<void>;
82
+ levelup?: undefined;
83
+ leveldown?: undefined;
84
+ subleveldown?: undefined;
85
+ encoding?: undefined;
86
+ rocksdb?: undefined;
87
+ memdown?: undefined;
88
+ connection?: undefined;
89
+ } | {
90
+ Store: typeof import("@live-change/db-store-observable-db/lib/Store.js");
91
+ connection: {
92
+ url: any;
93
+ creatingStores: Map<any, any>;
94
+ openingStores: Map<any, any>;
95
+ openStores: any[];
96
+ websocket: import("ws");
97
+ finished: boolean;
98
+ lastRequestId: number;
99
+ waitingRequests: Map<any, any>;
100
+ connect(): void;
101
+ openedStores: any[];
102
+ close(): void;
103
+ addRequest(request: any): void;
104
+ requestOkError(requestPacket: any): Promise<any>;
105
+ request(requestPacket: any, handle: any): Promise<any>;
106
+ rawRequest(requestPacket: any, handle: any): number;
107
+ requestSingleResult(requestPacket: any): Promise<any>;
108
+ createDatabase(databaseName: any, settings: any): Promise<any>;
109
+ deleteDatabase(databaseName: any): Promise<any>;
110
+ createStore(databaseName: any, storeName: any): Promise<any>;
111
+ deleteStore(databaseName: any, storeName: any): Promise<any>;
112
+ openStore(databaseName: any, storeName: any): Promise<any>;
113
+ closeStore(databaseName: any, storeName: any): Promise<any>;
114
+ put(databaseName: any, storeName: any, key: any, value: any): Promise<any>;
115
+ delete(databaseName: any, storeName: any, key: any): Promise<any>;
116
+ get(databaseName: any, storeName: any, key: any, value: any): Promise<any>;
117
+ rangePacket(storeId: any, op: any, range: any): Buffer;
118
+ getRange(databaseName: any, storeName: any, range: any): Promise<any>;
119
+ getCount(databaseName: any, storeName: any, range: any): Promise<any>;
120
+ deleteRange(databaseName: any, storeName: any, range: any): Promise<any>;
121
+ observe(databaseName: any, storeName: any, key: any, onValue: any, onError: any): Promise<number>;
122
+ observeRange(databaseName: any, storeName: any, range: any, onResultPut: any, onChanges: any, onError: any): Promise<number>;
123
+ observeCount(databaseName: any, storeName: any, range: any, onCount: any, onError: any): Promise<number>;
124
+ unobserve(requestId: any): Promise<void>;
125
+ };
126
+ createDb(path: any, options: any): any;
127
+ closeDb(db: any): void;
128
+ deleteDb(db: any): Promise<any>;
129
+ createStore(db: any, name: any, options: any): import("@live-change/db-store-observable-db/lib/Store.js");
130
+ closeStore(store: any): any;
131
+ deleteStore(store: any): Promise<any>;
132
+ levelup?: undefined;
133
+ leveldown?: undefined;
134
+ subleveldown?: undefined;
135
+ encoding?: undefined;
136
+ rocksdb?: undefined;
137
+ memdown?: undefined;
138
+ lmdb?: undefined;
139
+ };
140
+ import rbTreeStore from '@live-change/db-store-rbtree';
141
+ import lmdb from 'node-lmdb';
142
+ import lmdbStore from '@live-change/db-store-lmdb';