mirror of
https://github.com/zadam/trilium.git
synced 2025-12-29 19:49:58 +01:00
Revert "feat(search): try to decrease complexity"
This reverts commit 5b79e0d71e.
This commit is contained in:
@@ -15,75 +15,6 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
// Resource manager for proper cleanup
|
||||
class ResourceManager {
|
||||
private resources: Array<{ name: string; cleanup: () => void | Promise<void> }> = [];
|
||||
private cleanedUp = false;
|
||||
|
||||
register(name: string, cleanup: () => void | Promise<void>): void {
|
||||
console.log(`[ResourceManager] Registered resource: ${name}`);
|
||||
this.resources.push({ name, cleanup });
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.cleanedUp) {
|
||||
console.log('[ResourceManager] Already cleaned up, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ResourceManager] Starting cleanup...');
|
||||
this.cleanedUp = true;
|
||||
|
||||
// Cleanup in reverse order of registration
|
||||
for (let i = this.resources.length - 1; i >= 0; i--) {
|
||||
const resource = this.resources[i];
|
||||
try {
|
||||
console.log(`[ResourceManager] Cleaning up: ${resource.name}`);
|
||||
await resource.cleanup();
|
||||
console.log(`[ResourceManager] Successfully cleaned up: ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error(`[ResourceManager] Error cleaning up ${resource.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.resources = [];
|
||||
console.log('[ResourceManager] Cleanup completed');
|
||||
}
|
||||
}
|
||||
|
||||
// Global resource manager
|
||||
const resourceManager = new ResourceManager();
|
||||
|
||||
// Setup process exit handlers
|
||||
process.on('exit', (code) => {
|
||||
console.log(`[Process] Exiting with code: ${code}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[Process] Received SIGINT, cleaning up...');
|
||||
await resourceManager.cleanup();
|
||||
process.exit(130); // Standard exit code for SIGINT
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\n[Process] Received SIGTERM, cleaning up...');
|
||||
await resourceManager.cleanup();
|
||||
process.exit(143); // Standard exit code for SIGTERM
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('[Process] Uncaught exception:', error);
|
||||
await resourceManager.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
console.error('[Process] Unhandled rejection at:', promise, 'reason:', reason);
|
||||
await resourceManager.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
const noteCount = parseInt(process.argv[2]);
|
||||
const batchSize = parseInt(process.argv[3]) || 100;
|
||||
|
||||
@@ -110,6 +41,15 @@ console.log(` Batch size: ${batchSize.toLocaleString()}`);
|
||||
console.log(` Database: ${DB_PATH}`);
|
||||
console.log(`============================================\n`);
|
||||
|
||||
// Open database
|
||||
const db = new Database(DB_PATH);
|
||||
|
||||
// Enable optimizations
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('cache_size = 10000');
|
||||
db.pragma('temp_store = MEMORY');
|
||||
|
||||
// Helper functions that mimic Trilium's ID generation
|
||||
function newEntityId(prefix: string = ''): string {
|
||||
return prefix + randomBytes(12).toString('base64').replace(/[+/=]/g, '').substring(0, 12);
|
||||
@@ -185,18 +125,15 @@ function generateContent(): string {
|
||||
}
|
||||
|
||||
// Native-style service functions
|
||||
function createNote(
|
||||
db: Database.Database,
|
||||
params: {
|
||||
noteId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
mime?: string;
|
||||
isProtected?: boolean;
|
||||
parentNoteId?: string;
|
||||
}
|
||||
) {
|
||||
function createNote(params: {
|
||||
noteId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
type: string;
|
||||
mime?: string;
|
||||
isProtected?: boolean;
|
||||
parentNoteId?: string;
|
||||
}) {
|
||||
const currentDateTime = utcNowDateTime();
|
||||
const noteStmt = db.prepare(`
|
||||
INSERT INTO notes (noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId,
|
||||
@@ -258,16 +195,13 @@ function createNote(
|
||||
return params.noteId;
|
||||
}
|
||||
|
||||
function createAttribute(
|
||||
db: Database.Database,
|
||||
params: {
|
||||
noteId: string;
|
||||
type: 'label' | 'relation';
|
||||
name: string;
|
||||
value: string;
|
||||
isInheritable?: boolean;
|
||||
}
|
||||
) {
|
||||
function createAttribute(params: {
|
||||
noteId: string;
|
||||
type: 'label' | 'relation';
|
||||
name: string;
|
||||
value: string;
|
||||
isInheritable?: boolean;
|
||||
}) {
|
||||
const currentDateTime = utcNowDateTime();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO attributes (attributeId, noteId, type, name, value, position,
|
||||
@@ -289,212 +223,148 @@ function createAttribute(
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let db: Database.Database | null = null;
|
||||
let exitCode = 0;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const allNoteIds: string[] = ['root'];
|
||||
let notesCreated = 0;
|
||||
let attributesCreated = 0;
|
||||
|
||||
console.log('Opening database connection...');
|
||||
|
||||
// Open database with proper error handling
|
||||
try {
|
||||
db = new Database(DB_PATH);
|
||||
resourceManager.register('Database Connection', () => {
|
||||
if (db && db.open) {
|
||||
console.log('Closing database connection...');
|
||||
db.close();
|
||||
console.log('Database connection closed');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to open database:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Enable optimizations
|
||||
console.log('Configuring database optimizations...');
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('cache_size = 10000');
|
||||
db.pragma('temp_store = MEMORY');
|
||||
|
||||
console.log('Starting note generation...\n');
|
||||
|
||||
// Create container note
|
||||
const containerNoteId = newEntityId();
|
||||
const containerTransaction = db.transaction(() => {
|
||||
createNote(db!, {
|
||||
noteId: containerNoteId,
|
||||
title: `Stress Test ${new Date().toISOString()}`,
|
||||
content: `<p>Container for stress test with ${noteCount} notes</p>`,
|
||||
type: 'text',
|
||||
parentNoteId: 'root'
|
||||
});
|
||||
async function main() {
|
||||
const startTime = Date.now();
|
||||
const allNoteIds: string[] = ['root'];
|
||||
let notesCreated = 0;
|
||||
let attributesCreated = 0;
|
||||
|
||||
console.log('Starting note generation...\n');
|
||||
|
||||
// Create container note
|
||||
const containerNoteId = newEntityId();
|
||||
const containerTransaction = db.transaction(() => {
|
||||
createNote({
|
||||
noteId: containerNoteId,
|
||||
title: `Stress Test ${new Date().toISOString()}`,
|
||||
content: `<p>Container for stress test with ${noteCount} notes</p>`,
|
||||
type: 'text',
|
||||
parentNoteId: 'root'
|
||||
});
|
||||
});
|
||||
containerTransaction();
|
||||
|
||||
console.log(`Created container note: ${containerNoteId}`);
|
||||
allNoteIds.push(containerNoteId);
|
||||
|
||||
// Process in batches
|
||||
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
|
||||
const batchStart = batch * batchSize;
|
||||
const batchEnd = Math.min(batchStart + batchSize, noteCount);
|
||||
const batchNoteCount = batchEnd - batchStart;
|
||||
|
||||
try {
|
||||
containerTransaction();
|
||||
console.log(`Created container note: ${containerNoteId}`);
|
||||
allNoteIds.push(containerNoteId);
|
||||
} catch (error) {
|
||||
console.error('Failed to create container note:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Process in batches
|
||||
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
|
||||
const batchStart = batch * batchSize;
|
||||
const batchEnd = Math.min(batchStart + batchSize, noteCount);
|
||||
const batchNoteCount = batchEnd - batchStart;
|
||||
|
||||
const batchTransaction = db.transaction(() => {
|
||||
for (let i = 0; i < batchNoteCount; i++) {
|
||||
const noteId = newEntityId();
|
||||
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
|
||||
const batchTransaction = db.transaction(() => {
|
||||
for (let i = 0; i < batchNoteCount; i++) {
|
||||
const noteId = newEntityId();
|
||||
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
|
||||
|
||||
// Decide parent - either container or random existing note
|
||||
let parentNoteId = containerNoteId;
|
||||
if (allNoteIds.length > 10 && Math.random() < 0.3) {
|
||||
parentNoteId = allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 100))];
|
||||
}
|
||||
|
||||
// Create note
|
||||
createNote({
|
||||
noteId,
|
||||
title: generateTitle(),
|
||||
content: generateContent(),
|
||||
type,
|
||||
parentNoteId,
|
||||
isProtected: Math.random() < 0.05
|
||||
});
|
||||
|
||||
notesCreated++;
|
||||
allNoteIds.push(noteId);
|
||||
|
||||
// Add attributes
|
||||
const attributeCount = Math.floor(Math.random() * 5);
|
||||
for (let a = 0; a < attributeCount; a++) {
|
||||
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
|
||||
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
|
||||
|
||||
// Decide parent - either container or random existing note
|
||||
let parentNoteId = containerNoteId;
|
||||
if (allNoteIds.length > 10 && Math.random() < 0.3) {
|
||||
parentNoteId = allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 100))];
|
||||
}
|
||||
|
||||
// Create note
|
||||
createNote(db!, {
|
||||
noteId,
|
||||
title: generateTitle(),
|
||||
content: generateContent(),
|
||||
type,
|
||||
parentNoteId,
|
||||
isProtected: Math.random() < 0.05
|
||||
});
|
||||
|
||||
notesCreated++;
|
||||
allNoteIds.push(noteId);
|
||||
|
||||
// Add attributes
|
||||
const attributeCount = Math.floor(Math.random() * 5);
|
||||
for (let a = 0; a < attributeCount; a++) {
|
||||
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
|
||||
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
|
||||
|
||||
try {
|
||||
createAttribute(db!, {
|
||||
noteId,
|
||||
type: attrType as 'label' | 'relation',
|
||||
name: attrName,
|
||||
value: attrType === 'relation'
|
||||
? allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 50))]
|
||||
: getRandomWord(),
|
||||
isInheritable: Math.random() < 0.2
|
||||
});
|
||||
attributesCreated++;
|
||||
} catch (e) {
|
||||
// Ignore duplicate errors, but log unexpected ones
|
||||
if (!(e instanceof Error) || !e.message.includes('UNIQUE')) {
|
||||
console.warn(`Unexpected attribute error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep memory in check
|
||||
if (allNoteIds.length > 500) {
|
||||
allNoteIds.splice(1, allNoteIds.length - 500);
|
||||
try {
|
||||
createAttribute({
|
||||
noteId,
|
||||
type: attrType,
|
||||
name: attrName,
|
||||
value: attrType === 'relation'
|
||||
? allNoteIds[Math.floor(Math.random() * Math.min(allNoteIds.length, 50))]
|
||||
: getRandomWord(),
|
||||
isInheritable: Math.random() < 0.2
|
||||
});
|
||||
attributesCreated++;
|
||||
} catch (e) {
|
||||
// Ignore duplicate errors
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
batchTransaction();
|
||||
|
||||
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const rate = Math.round(notesCreated / elapsed);
|
||||
|
||||
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attributes: ${attributesCreated}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to process batch ${batch + 1}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Add entity changes
|
||||
console.log('\nAdding entity changes...');
|
||||
const entityTransaction = db.transaction(() => {
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO entity_changes
|
||||
(entityName, entityId, hash, isErased, changeId, componentId, instanceId, isSynced, utcDateChanged)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (let i = 0; i < Math.min(100, allNoteIds.length); i++) {
|
||||
stmt.run(
|
||||
'notes',
|
||||
allNoteIds[i],
|
||||
randomBytes(16).toString('hex'),
|
||||
0,
|
||||
newEntityId(),
|
||||
'stress_test',
|
||||
'stress_test_instance',
|
||||
1,
|
||||
utcNowDateTime()
|
||||
);
|
||||
// Keep memory in check
|
||||
if (allNoteIds.length > 500) {
|
||||
allNoteIds.splice(1, allNoteIds.length - 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
entityTransaction();
|
||||
} catch (error) {
|
||||
console.error('Failed to add entity changes:', error);
|
||||
// Non-critical error, continue
|
||||
}
|
||||
batchTransaction();
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const rate = Math.round(notesCreated / elapsed);
|
||||
|
||||
// Get statistics
|
||||
console.log('\nGathering database statistics...');
|
||||
const stats = {
|
||||
notes: db.prepare('SELECT COUNT(*) as count FROM notes').get() as any,
|
||||
branches: db.prepare('SELECT COUNT(*) as count FROM branches').get() as any,
|
||||
attributes: db.prepare('SELECT COUNT(*) as count FROM attributes').get() as any,
|
||||
blobs: db.prepare('SELECT COUNT(*) as count FROM blobs').get() as any
|
||||
};
|
||||
|
||||
console.log('\n✅ Native-style stress test completed successfully!\n');
|
||||
console.log('Database Statistics:');
|
||||
console.log(` • Total notes: ${stats.notes.count.toLocaleString()}`);
|
||||
console.log(` • Total branches: ${stats.branches.count.toLocaleString()}`);
|
||||
console.log(` • Total attributes: ${stats.attributes.count.toLocaleString()}`);
|
||||
console.log(` • Total blobs: ${stats.blobs.count.toLocaleString()}`);
|
||||
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
|
||||
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
|
||||
console.log(` • Container note ID: ${containerNoteId}\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Stress test failed with error:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Error stack:', error.stack);
|
||||
}
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
// Ensure cleanup happens
|
||||
console.log('\nPerforming final cleanup...');
|
||||
await resourceManager.cleanup();
|
||||
|
||||
// Exit with appropriate code
|
||||
console.log(`Exiting with code: ${exitCode}`);
|
||||
process.exit(exitCode);
|
||||
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attributes: ${attributesCreated}`);
|
||||
}
|
||||
|
||||
// Add entity changes
|
||||
console.log('\nAdding entity changes...');
|
||||
const entityTransaction = db.transaction(() => {
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO entity_changes
|
||||
(entityName, entityId, hash, isErased, changeId, componentId, instanceId, isSynced, utcDateChanged)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (let i = 0; i < Math.min(100, allNoteIds.length); i++) {
|
||||
stmt.run(
|
||||
'notes',
|
||||
allNoteIds[i],
|
||||
randomBytes(16).toString('hex'),
|
||||
0,
|
||||
newEntityId(),
|
||||
'stress_test',
|
||||
'stress_test_instance',
|
||||
1,
|
||||
utcNowDateTime()
|
||||
);
|
||||
}
|
||||
});
|
||||
entityTransaction();
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
// Get statistics
|
||||
const stats = {
|
||||
notes: db.prepare('SELECT COUNT(*) as count FROM notes').get() as any,
|
||||
branches: db.prepare('SELECT COUNT(*) as count FROM branches').get() as any,
|
||||
attributes: db.prepare('SELECT COUNT(*) as count FROM attributes').get() as any,
|
||||
blobs: db.prepare('SELECT COUNT(*) as count FROM blobs').get() as any
|
||||
};
|
||||
|
||||
console.log('\n✅ Native-style stress test completed successfully!\n');
|
||||
console.log('Database Statistics:');
|
||||
console.log(` • Total notes: ${stats.notes.count.toLocaleString()}`);
|
||||
console.log(` • Total branches: ${stats.branches.count.toLocaleString()}`);
|
||||
console.log(` • Total attributes: ${stats.attributes.count.toLocaleString()}`);
|
||||
console.log(` • Total blobs: ${stats.blobs.count.toLocaleString()}`);
|
||||
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
|
||||
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
|
||||
console.log(` • Container note ID: ${containerNoteId}\n`);
|
||||
|
||||
db.close();
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch(async (error) => {
|
||||
console.error('Fatal error in main:', error);
|
||||
await resourceManager.cleanup();
|
||||
main().catch((error) => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -15,75 +15,6 @@
|
||||
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
|
||||
process.env.DATA_DIR = process.env.DATA_DIR || './data';
|
||||
|
||||
// Resource manager for proper cleanup
|
||||
class ResourceManager {
|
||||
private resources: Array<{ name: string; cleanup: () => void | Promise<void> }> = [];
|
||||
private cleanedUp = false;
|
||||
|
||||
register(name: string, cleanup: () => void | Promise<void>): void {
|
||||
console.log(`[ResourceManager] Registered resource: ${name}`);
|
||||
this.resources.push({ name, cleanup });
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
if (this.cleanedUp) {
|
||||
console.log('[ResourceManager] Already cleaned up, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ResourceManager] Starting cleanup...');
|
||||
this.cleanedUp = true;
|
||||
|
||||
// Cleanup in reverse order of registration
|
||||
for (let i = this.resources.length - 1; i >= 0; i--) {
|
||||
const resource = this.resources[i];
|
||||
try {
|
||||
console.log(`[ResourceManager] Cleaning up: ${resource.name}`);
|
||||
await resource.cleanup();
|
||||
console.log(`[ResourceManager] Successfully cleaned up: ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error(`[ResourceManager] Error cleaning up ${resource.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.resources = [];
|
||||
console.log('[ResourceManager] Cleanup completed');
|
||||
}
|
||||
}
|
||||
|
||||
// Global resource manager
|
||||
const resourceManager = new ResourceManager();
|
||||
|
||||
// Setup process exit handlers
|
||||
process.on('exit', (code) => {
|
||||
console.log(`[Process] Exiting with code: ${code}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n[Process] Received SIGINT, cleaning up...');
|
||||
await resourceManager.cleanup();
|
||||
process.exit(130); // Standard exit code for SIGINT
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\n[Process] Received SIGTERM, cleaning up...');
|
||||
await resourceManager.cleanup();
|
||||
process.exit(143); // Standard exit code for SIGTERM
|
||||
});
|
||||
|
||||
process.on('uncaughtException', async (error) => {
|
||||
console.error('[Process] Uncaught exception:', error);
|
||||
await resourceManager.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
console.error('[Process] Unhandled rejection at:', promise, 'reason:', reason);
|
||||
await resourceManager.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Import Trilium services after setting up environment and handlers
|
||||
import './src/becca/entity_constructor.js';
|
||||
import sqlInit from './src/services/sql_init.js';
|
||||
import noteService from './src/services/notes.js';
|
||||
@@ -95,7 +26,6 @@ import becca from './src/becca/becca.js';
|
||||
import entityChangesService from './src/services/entity_changes.js';
|
||||
import type BNote from './src/becca/entities/bnote.js';
|
||||
|
||||
// Parse command line arguments
|
||||
const noteCount = parseInt(process.argv[2]);
|
||||
const batchSize = parseInt(process.argv[3]) || 100;
|
||||
|
||||
@@ -229,8 +159,7 @@ function generateSentence(): string {
|
||||
return wordList.join(' ');
|
||||
}
|
||||
|
||||
async function runStressTest(): Promise<void> {
|
||||
let exitCode = 0;
|
||||
async function start() {
|
||||
const startTime = Date.now();
|
||||
const allNotes: BNote[] = [];
|
||||
let notesCreated = 0;
|
||||
@@ -238,343 +167,255 @@ async function runStressTest(): Promise<void> {
|
||||
let clonesCreated = 0;
|
||||
let revisionsCreated = 0;
|
||||
|
||||
try {
|
||||
console.log('Starting note generation using native Trilium services...\n');
|
||||
|
||||
// Find root note
|
||||
const rootNote = becca.getNote('root');
|
||||
if (!rootNote) {
|
||||
throw new Error('Root note not found! Database might not be initialized properly.');
|
||||
}
|
||||
|
||||
// Create a container note for our stress test
|
||||
console.log('Creating container note...');
|
||||
const { note: containerNote } = noteService.createNewNote({
|
||||
parentNoteId: 'root',
|
||||
title: `Stress Test ${new Date().toISOString()}`,
|
||||
content: `<p>Container for stress test with ${noteCount} notes</p>`,
|
||||
type: 'text',
|
||||
isProtected: false
|
||||
});
|
||||
|
||||
console.log(`Created container note: ${containerNote.title} (${containerNote.noteId})`);
|
||||
allNotes.push(containerNote);
|
||||
|
||||
// Process in batches for better control
|
||||
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
|
||||
const batchStart = batch * batchSize;
|
||||
const batchEnd = Math.min(batchStart + batchSize, noteCount);
|
||||
const batchNoteCount = batchEnd - batchStart;
|
||||
|
||||
try {
|
||||
sql.transactional(() => {
|
||||
for (let i = 0; i < batchNoteCount; i++) {
|
||||
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
|
||||
let content = '';
|
||||
let mime = undefined;
|
||||
|
||||
// Generate content based on type
|
||||
switch (type) {
|
||||
case 'code':
|
||||
content = generateCodeContent();
|
||||
mime = 'text/plain';
|
||||
break;
|
||||
case 'mermaid':
|
||||
content = generateMermaidContent();
|
||||
mime = 'text/plain';
|
||||
break;
|
||||
case 'canvas':
|
||||
content = JSON.stringify({
|
||||
elements: [],
|
||||
appState: { viewBackgroundColor: "#ffffff" },
|
||||
files: {}
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
case 'search':
|
||||
content = JSON.stringify({
|
||||
searchString: `#${getRandomWord()} OR #${getRandomWord()}`
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
case 'relationMap':
|
||||
content = JSON.stringify({
|
||||
notes: [],
|
||||
zoom: 1
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
default:
|
||||
content = generateContent();
|
||||
mime = 'text/html';
|
||||
}
|
||||
|
||||
// Decide parent - either container or random existing note for complex hierarchy
|
||||
let parentNoteId = containerNote.noteId;
|
||||
if (allNotes.length > 10 && Math.random() < 0.3) {
|
||||
// 30% chance to attach to random existing note
|
||||
parentNoteId = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 100))].noteId;
|
||||
}
|
||||
|
||||
// Create the note using native service
|
||||
const { note, branch } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title: generateTitle(),
|
||||
content,
|
||||
type,
|
||||
mime,
|
||||
isProtected: Math.random() < 0.05 // 5% protected notes
|
||||
});
|
||||
|
||||
notesCreated++;
|
||||
allNotes.push(note);
|
||||
|
||||
// Add attributes using native service
|
||||
const attributeCount = Math.floor(Math.random() * 8);
|
||||
for (let a = 0; a < attributeCount; a++) {
|
||||
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
|
||||
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
|
||||
|
||||
try {
|
||||
if (attrType === 'label') {
|
||||
attributeService.createLabel(
|
||||
note.noteId,
|
||||
attrName,
|
||||
Math.random() < 0.5 ? getRandomWord() : ''
|
||||
);
|
||||
attributesCreated++;
|
||||
} else if (allNotes.length > 1) {
|
||||
const targetNote = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 50))];
|
||||
attributeService.createRelation(
|
||||
note.noteId,
|
||||
attrName,
|
||||
targetNote.noteId
|
||||
);
|
||||
attributesCreated++;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore attribute creation errors (e.g., duplicates)
|
||||
if (e instanceof Error && !e.message.includes('duplicate') && !e.message.includes('already exists')) {
|
||||
console.warn(`Unexpected attribute error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update note content occasionally to trigger revisions
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
note.setContent(content + `\n<p>Updated at ${new Date().toISOString()}</p>`);
|
||||
note.save();
|
||||
|
||||
// Save revision
|
||||
if (Math.random() < 0.5) {
|
||||
try {
|
||||
note.saveRevision();
|
||||
revisionsCreated++;
|
||||
} catch (e) {
|
||||
// Ignore revision errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create clones occasionally for complex relationships
|
||||
if (allNotes.length > 20 && Math.random() < 0.05) { // 5% chance
|
||||
try {
|
||||
const targetParent = allNotes[Math.floor(Math.random() * allNotes.length)];
|
||||
const result = cloningService.cloneNoteToBranch(
|
||||
note.noteId,
|
||||
targetParent.noteId,
|
||||
Math.random() < 0.2 ? 'clone' : ''
|
||||
);
|
||||
if (result.success) {
|
||||
clonesCreated++;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cloning errors (e.g., circular dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
// Add note to recent notes occasionally
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
try {
|
||||
sql.execute(
|
||||
"INSERT OR IGNORE INTO recent_notes (noteId, notePath, utcDateCreated) VALUES (?, ?, ?)",
|
||||
[note.noteId, note.getBestNotePath()?.path || 'root', note.utcDateCreated]
|
||||
);
|
||||
} catch (e) {
|
||||
// Table might not exist in all versions
|
||||
}
|
||||
}
|
||||
|
||||
// Keep memory usage in check
|
||||
if (allNotes.length > 500) {
|
||||
allNotes.splice(0, allNotes.length - 500);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const rate = Math.round(notesCreated / elapsed);
|
||||
|
||||
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attrs: ${attributesCreated} | Clones: ${clonesCreated} | Revisions: ${revisionsCreated}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to process batch ${batch + 1}:`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Force entity changes sync (non-critical)
|
||||
try {
|
||||
entityChangesService.putNoteReorderingEntityChange(containerNote.noteId);
|
||||
} catch (e) {
|
||||
// Ignore entity change errors
|
||||
}
|
||||
}
|
||||
|
||||
// Create some advanced structures
|
||||
console.log('\nCreating advanced relationships...');
|
||||
|
||||
try {
|
||||
// Create template notes
|
||||
const templateNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Template: ' + generateTitle(),
|
||||
content: '<p>This is a template note</p>',
|
||||
type: 'text',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(templateNote.noteId, 'template', '');
|
||||
|
||||
// Apply template to some notes
|
||||
for (let i = 0; i < Math.min(10, allNotes.length); i++) {
|
||||
const targetNote = allNotes[Math.floor(Math.random() * allNotes.length)];
|
||||
try {
|
||||
attributeService.createRelation(targetNote.noteId, 'template', templateNote.noteId);
|
||||
} catch (e) {
|
||||
// Ignore relation errors
|
||||
}
|
||||
}
|
||||
|
||||
// Create some CSS notes
|
||||
const cssNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Custom CSS',
|
||||
content: `.custom-class { color: #${Math.floor(Math.random()*16777215).toString(16)}; }`,
|
||||
type: 'code',
|
||||
mime: 'text/css',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(cssNote.noteId, 'appCss', '');
|
||||
|
||||
// Create widget notes
|
||||
const widgetNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Custom Widget',
|
||||
content: `<div>Widget content: ${generateSentence()}</div>`,
|
||||
type: 'code',
|
||||
mime: 'text/html',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(widgetNote.noteId, 'widget', '');
|
||||
} catch (error) {
|
||||
console.warn('Failed to create some advanced structures:', error);
|
||||
// Non-critical, continue
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
// Get final statistics
|
||||
console.log('\nGathering database statistics...');
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats.notes = sql.getValue('SELECT COUNT(*) FROM notes');
|
||||
stats.branches = sql.getValue('SELECT COUNT(*) FROM branches');
|
||||
stats.attributes = sql.getValue('SELECT COUNT(*) FROM attributes');
|
||||
stats.revisions = sql.getValue('SELECT COUNT(*) FROM revisions');
|
||||
stats.attachments = sql.getValue('SELECT COUNT(*) FROM attachments');
|
||||
stats.recentNotes = sql.getValue('SELECT COUNT(*) FROM recent_notes');
|
||||
} catch (error) {
|
||||
console.warn('Failed to get some statistics:', error);
|
||||
}
|
||||
|
||||
console.log('\n✅ Native API stress test completed successfully!\n');
|
||||
console.log('Database Statistics:');
|
||||
console.log(` • Total notes: ${stats.notes?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Total branches: ${stats.branches?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Total attributes: ${stats.attributes?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Total revisions: ${stats.revisions?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Total attachments: ${stats.attachments?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Recent notes: ${stats.recentNotes?.toLocaleString() || 'N/A'}`);
|
||||
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
|
||||
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
|
||||
console.log(` • Container note ID: ${containerNote.noteId}\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Stress test failed with error:', error);
|
||||
if (error instanceof Error) {
|
||||
console.error('Error stack:', error.stack);
|
||||
}
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
// Cleanup database connections and resources
|
||||
console.log('\nCleaning up database resources...');
|
||||
try {
|
||||
// Close any open database connections
|
||||
if (sql && typeof sql.execute === 'function') {
|
||||
// Try to checkpoint WAL if possible
|
||||
try {
|
||||
sql.execute('PRAGMA wal_checkpoint(TRUNCATE)');
|
||||
console.log('WAL checkpoint completed');
|
||||
} catch (e) {
|
||||
// Ignore checkpoint errors
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error during database cleanup:', error);
|
||||
}
|
||||
|
||||
// Perform final resource cleanup
|
||||
await resourceManager.cleanup();
|
||||
|
||||
// Exit with appropriate code
|
||||
console.log(`Exiting with code: ${exitCode}`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
async function start(): Promise<void> {
|
||||
try {
|
||||
// Register database cleanup
|
||||
resourceManager.register('Database Connection', async () => {
|
||||
try {
|
||||
if (sql && typeof sql.execute === 'function') {
|
||||
console.log('Closing database connections...');
|
||||
// Attempt to close any open transactions
|
||||
sql.execute('ROLLBACK');
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
});
|
||||
|
||||
// Run the stress test
|
||||
await runStressTest();
|
||||
} catch (error) {
|
||||
console.error('Fatal error during startup:', error);
|
||||
await resourceManager.cleanup();
|
||||
console.log('Starting note generation using native Trilium services...\n');
|
||||
|
||||
// Find root note
|
||||
const rootNote = becca.getNote('root');
|
||||
if (!rootNote) {
|
||||
console.error('Root note not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create a container note for our stress test
|
||||
const { note: containerNote } = noteService.createNewNote({
|
||||
parentNoteId: 'root',
|
||||
title: `Stress Test ${new Date().toISOString()}`,
|
||||
content: `<p>Container for stress test with ${noteCount} notes</p>`,
|
||||
type: 'text',
|
||||
isProtected: false
|
||||
});
|
||||
|
||||
console.log(`Created container note: ${containerNote.title} (${containerNote.noteId})`);
|
||||
allNotes.push(containerNote);
|
||||
|
||||
// Process in batches for better control
|
||||
for (let batch = 0; batch < Math.ceil(noteCount / batchSize); batch++) {
|
||||
const batchStart = batch * batchSize;
|
||||
const batchEnd = Math.min(batchStart + batchSize, noteCount);
|
||||
const batchNoteCount = batchEnd - batchStart;
|
||||
|
||||
sql.transactional(() => {
|
||||
for (let i = 0; i < batchNoteCount; i++) {
|
||||
const type = noteTypes[Math.floor(Math.random() * noteTypes.length)];
|
||||
let content = '';
|
||||
let mime = undefined;
|
||||
|
||||
// Generate content based on type
|
||||
switch (type) {
|
||||
case 'code':
|
||||
content = generateCodeContent();
|
||||
mime = 'text/plain';
|
||||
break;
|
||||
case 'mermaid':
|
||||
content = generateMermaidContent();
|
||||
mime = 'text/plain';
|
||||
break;
|
||||
case 'canvas':
|
||||
content = JSON.stringify({
|
||||
elements: [],
|
||||
appState: { viewBackgroundColor: "#ffffff" },
|
||||
files: {}
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
case 'search':
|
||||
content = JSON.stringify({
|
||||
searchString: `#${getRandomWord()} OR #${getRandomWord()}`
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
case 'relationMap':
|
||||
content = JSON.stringify({
|
||||
notes: [],
|
||||
zoom: 1
|
||||
});
|
||||
mime = 'application/json';
|
||||
break;
|
||||
default:
|
||||
content = generateContent();
|
||||
mime = 'text/html';
|
||||
}
|
||||
|
||||
// Decide parent - either container or random existing note for complex hierarchy
|
||||
let parentNoteId = containerNote.noteId;
|
||||
if (allNotes.length > 10 && Math.random() < 0.3) {
|
||||
// 30% chance to attach to random existing note
|
||||
parentNoteId = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 100))].noteId;
|
||||
}
|
||||
|
||||
// Create the note using native service
|
||||
const { note, branch } = noteService.createNewNote({
|
||||
parentNoteId,
|
||||
title: generateTitle(),
|
||||
content,
|
||||
type,
|
||||
mime,
|
||||
isProtected: Math.random() < 0.05 // 5% protected notes
|
||||
});
|
||||
|
||||
notesCreated++;
|
||||
allNotes.push(note);
|
||||
|
||||
// Add attributes using native service
|
||||
const attributeCount = Math.floor(Math.random() * 8);
|
||||
for (let a = 0; a < attributeCount; a++) {
|
||||
const attrType = Math.random() < 0.7 ? 'label' : 'relation';
|
||||
const attrName = attributeNames[Math.floor(Math.random() * attributeNames.length)];
|
||||
|
||||
try {
|
||||
if (attrType === 'label') {
|
||||
attributeService.createLabel(
|
||||
note.noteId,
|
||||
attrName,
|
||||
Math.random() < 0.5 ? getRandomWord() : ''
|
||||
);
|
||||
attributesCreated++;
|
||||
} else if (allNotes.length > 1) {
|
||||
const targetNote = allNotes[Math.floor(Math.random() * Math.min(allNotes.length, 50))];
|
||||
attributeService.createRelation(
|
||||
note.noteId,
|
||||
attrName,
|
||||
targetNote.noteId
|
||||
);
|
||||
attributesCreated++;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore attribute creation errors (e.g., duplicates)
|
||||
}
|
||||
}
|
||||
|
||||
// Update note content occasionally to trigger revisions
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
note.setContent(content + `\n<p>Updated at ${new Date().toISOString()}</p>`);
|
||||
note.save();
|
||||
|
||||
// Save revision
|
||||
if (Math.random() < 0.5) {
|
||||
note.saveRevision();
|
||||
revisionsCreated++;
|
||||
}
|
||||
}
|
||||
|
||||
// Create clones occasionally for complex relationships
|
||||
if (allNotes.length > 20 && Math.random() < 0.05) { // 5% chance
|
||||
try {
|
||||
const targetParent = allNotes[Math.floor(Math.random() * allNotes.length)];
|
||||
const result = cloningService.cloneNoteToBranch(
|
||||
note.noteId,
|
||||
targetParent.noteId,
|
||||
Math.random() < 0.2 ? 'clone' : ''
|
||||
);
|
||||
if (result.success) {
|
||||
clonesCreated++;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cloning errors (e.g., circular dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
// Add note to recent notes occasionally
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
try {
|
||||
sql.execute(
|
||||
"INSERT OR IGNORE INTO recent_notes (noteId, notePath, utcDateCreated) VALUES (?, ?, ?)",
|
||||
[note.noteId, note.getBestNotePath()?.path || 'root', note.utcDateCreated]
|
||||
);
|
||||
} catch (e) {
|
||||
// Table might not exist in all versions
|
||||
}
|
||||
}
|
||||
|
||||
// Keep memory usage in check
|
||||
if (allNotes.length > 500) {
|
||||
allNotes.splice(0, allNotes.length - 500);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const progress = Math.round(((batch + 1) / Math.ceil(noteCount / batchSize)) * 100);
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const rate = Math.round(notesCreated / elapsed);
|
||||
|
||||
console.log(`Progress: ${progress}% | Notes: ${notesCreated}/${noteCount} | Rate: ${rate}/sec | Attrs: ${attributesCreated} | Clones: ${clonesCreated} | Revisions: ${revisionsCreated}`);
|
||||
|
||||
// Force entity changes sync
|
||||
entityChangesService.putNoteReorderingEntityChange(containerNote.noteId);
|
||||
}
|
||||
|
||||
// Create some advanced structures
|
||||
console.log('\nCreating advanced relationships...');
|
||||
|
||||
// Create template notes
|
||||
const templateNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Template: ' + generateTitle(),
|
||||
content: '<p>This is a template note</p>',
|
||||
type: 'text',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(templateNote.noteId, 'template', '');
|
||||
|
||||
// Apply template to some notes
|
||||
for (let i = 0; i < Math.min(10, allNotes.length); i++) {
|
||||
const targetNote = allNotes[Math.floor(Math.random() * allNotes.length)];
|
||||
attributeService.createRelation(targetNote.noteId, 'template', templateNote.noteId);
|
||||
}
|
||||
|
||||
// Create some CSS notes
|
||||
const cssNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Custom CSS',
|
||||
content: `.custom-class { color: #${Math.floor(Math.random()*16777215).toString(16)}; }`,
|
||||
type: 'code',
|
||||
mime: 'text/css',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(cssNote.noteId, 'appCss', '');
|
||||
|
||||
// Create widget notes
|
||||
const widgetNote = noteService.createNewNote({
|
||||
parentNoteId: containerNote.noteId,
|
||||
title: 'Custom Widget',
|
||||
content: `<div>Widget content: ${generateSentence()}</div>`,
|
||||
type: 'code',
|
||||
mime: 'text/html',
|
||||
isProtected: false
|
||||
}).note;
|
||||
|
||||
attributeService.createLabel(widgetNote.noteId, 'widget', '');
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = (endTime - startTime) / 1000;
|
||||
|
||||
// Get final statistics
|
||||
const stats = {
|
||||
notes: sql.getValue('SELECT COUNT(*) FROM notes'),
|
||||
branches: sql.getValue('SELECT COUNT(*) FROM branches'),
|
||||
attributes: sql.getValue('SELECT COUNT(*) FROM attributes'),
|
||||
revisions: sql.getValue('SELECT COUNT(*) FROM revisions'),
|
||||
attachments: sql.getValue('SELECT COUNT(*) FROM attachments'),
|
||||
recentNotes: sql.getValue('SELECT COUNT(*) FROM recent_notes')
|
||||
};
|
||||
|
||||
console.log('\n✅ Native API stress test completed successfully!\n');
|
||||
console.log('Database Statistics:');
|
||||
console.log(` • Total notes: ${stats.notes?.toLocaleString()}`);
|
||||
console.log(` • Total branches: ${stats.branches?.toLocaleString()}`);
|
||||
console.log(` • Total attributes: ${stats.attributes?.toLocaleString()}`);
|
||||
console.log(` • Total revisions: ${stats.revisions?.toLocaleString()}`);
|
||||
console.log(` • Total attachments: ${stats.attachments?.toLocaleString()}`);
|
||||
console.log(` • Recent notes: ${stats.recentNotes?.toLocaleString()}`);
|
||||
console.log(` • Time taken: ${duration.toFixed(2)} seconds`);
|
||||
console.log(` • Average rate: ${Math.round(noteCount / duration).toLocaleString()} notes/second`);
|
||||
console.log(` • Container note ID: ${containerNote.noteId}\n`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Initialize database and run stress test
|
||||
sqlInit.dbReady
|
||||
.then(() => cls.wrap(start)())
|
||||
.catch(async (err) => {
|
||||
console.error('Failed to initialize database:', err);
|
||||
await resourceManager.cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => {
|
||||
console.error('Error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user