mirror of
https://github.com/zadam/trilium.git
synced 2026-09-14 11:06:04 +05:00
cls-hooked installs its own async_hooks hook at import time and keeps a JS Map of contexts keyed by asyncId. Enabling a promiseResolve hook turns off V8's promise fast paths for the whole process, so the cost is not confined to code that uses the context. On Node 24 (the floor in .nvmrc) AsyncLocalStorage is backed by AsyncContextFrame instead, which is native and enables no hooks at all. Measured on Node v24.19.0. Creating 200k contexts each running a five-await chain: 890ms and 139MB RSS under cls-hooked, 80ms and 54MB under AsyncLocalStorage, against 50ms with no context propagation at all. Merely loading cls-hooked and creating a namespace, with no context ever entered, slows 500k unrelated awaits from 18ms to 215ms; the AsyncLocalStorage instance costs nothing measurable. The Electron main process paid that too, since it loads the same provider. Two behaviours the routes depend on carry over rather than being dropped. A nested init() inherits from the enclosing scope, which routes/api/llm.ts opens while its request is still on the stack and reads componentId and hoistedNoteId through; init() therefore runs the callback against a copy of the enclosing store, so writes shadow instead of reaching back. And set() still throws outside a context: a silent no-op there would drop an entity change from the sync queue without anything to notice. bindEmitter is reimplemented on AsyncResource.bind. AsyncLocalStorage alone covers a listener whose emit descends from the scope that added it but not one the socket raises on its own, which is what a client aborting mid-response does — and #customRequestHandler scripts are handed the Express response and invited by the User Guide to use it. Three details it has to get right: only addListener, on and prependListener are patched, because Node routes once() and prependOnceListener() through them and patching all five wraps twice; removal needs both a per-emitter WeakMap, so once()'s internal removeListener finds its own wrapper, and a `listener` property naming the innermost function, so off(originalFn) and listeners() still report what the caller passed; and the fallback branch is a `function` so the emitter arrives as `this`, which express's own listeners read. The class is renamed from ClsHookedExecutionContext at its import sites. The file keeps its name, since cls.* is still the vocabulary elsewhere. Verified against the contract specs added in the previous commit, which pass unchanged apart from reset() moving from a cls-hooked spy to an observable assertion. Stubbing the new bindEmitter to a no-op fails the same four tests as stubbing the old one did, so it is doing the work rather than AsyncLocalStorage covering it incidentally. 898 tests across the ETAPI and routes suites pass with cls-hooked removed from disk. Removing the dependency also drops async-hook-jl, emitter-listener and shimmer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
import { beforeAll } from "vitest";
|
|
import { readFileSync } from "fs";
|
|
import { join } from "path";
|
|
import { initializeCore, options } from "@triliumnext/core";
|
|
import { serverZipExportProviderFactory } from "../src/services/export/zip/factory.js";
|
|
import ServerBackupService from "../src/backup_provider.js";
|
|
import AsyncLocalStorageExecutionContext from "../src/cls_provider.js";
|
|
import NodejsCryptoProvider from "../src/crypto_provider.js";
|
|
import NodejsZipProvider from "../src/zip_provider.js";
|
|
import ServerPlatformProvider from "../src/platform_provider.js";
|
|
import BetterSqlite3Provider from "../src/sql_provider.js";
|
|
import NodejsInAppHelpProvider from "../src/in_app_help_provider.js";
|
|
import { initializeTranslationsWithParams } from "../src/services/i18n.js";
|
|
import ServerLogService from "../src/log_provider.js";
|
|
import { serverImageProvider } from "../src/services/image_provider.js";
|
|
|
|
// Initialize environment variables.
|
|
process.env.TRILIUM_DATA_DIR = join(__dirname, "db");
|
|
process.env.TRILIUM_RESOURCE_DIR = join(__dirname, "../src");
|
|
process.env.TRILIUM_INTEGRATION_TEST = "memory";
|
|
process.env.TRILIUM_ENV = "dev";
|
|
process.env.TRILIUM_PUBLIC_SERVER = "http://localhost:4200";
|
|
|
|
beforeAll(async () => {
|
|
// Load the integration test database into memory. The fixture at
|
|
// packages/trilium-core/src/test/fixtures/document.db is pre-seeded with
|
|
// the schema, demo content, and a known password ("demo1234") that the
|
|
// ETAPI tests log in with. Each test file runs in its own vitest fork
|
|
// (pool: "forks"), so each gets a fresh in-memory copy and mutations
|
|
// don't leak across files.
|
|
const dbProvider = new BetterSqlite3Provider();
|
|
dbProvider.loadFromBuffer(readFileSync(require.resolve("@triliumnext/core/src/test/fixtures/document.db")));
|
|
|
|
await initializeCore({
|
|
dbConfig: {
|
|
provider: dbProvider,
|
|
isReadOnly: false,
|
|
onTransactionCommit() {},
|
|
onTransactionRollback() {}
|
|
},
|
|
crypto: new NodejsCryptoProvider(),
|
|
zip: new NodejsZipProvider(),
|
|
zipExportProviderFactory: serverZipExportProviderFactory,
|
|
executionContext: new AsyncLocalStorageExecutionContext(),
|
|
schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
|
|
platform: new ServerPlatformProvider(),
|
|
translations: initializeTranslationsWithParams,
|
|
inAppHelp: new NodejsInAppHelpProvider(),
|
|
backup: new ServerBackupService(options),
|
|
log: new ServerLogService(),
|
|
image: serverImageProvider
|
|
});
|
|
});
|