From 3f6cdd787ce09fd884eef39e28672b516a33b2b6 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 14 Nov 2022 18:00:20 +0000 Subject: [PATCH] Replacing CLS with local-storage, simplified usage which should remove the memory leak permenantly. --- packages/backend-core/db.js | 7 +- packages/backend-core/src/clshooked/index.js | 650 ------------------ .../src/context/FunctionContext.ts | 47 -- .../backend-core/src/context/constants.ts | 4 - packages/backend-core/src/context/index.ts | 38 +- .../backend-core/src/context/localStorage.ts | 18 + packages/backend-core/src/db/couch/pouchDB.ts | 2 +- packages/backend-core/src/db/db.ts | 50 ++ packages/backend-core/src/db/index.ts | 58 +- packages/backend-core/src/db/utils.ts | 2 +- packages/backend-core/src/index.ts | 2 +- packages/backend-core/src/pkg/db.ts | 6 - 12 files changed, 98 insertions(+), 786 deletions(-) delete mode 100644 packages/backend-core/src/clshooked/index.js delete mode 100644 packages/backend-core/src/context/FunctionContext.ts create mode 100644 packages/backend-core/src/context/localStorage.ts create mode 100644 packages/backend-core/src/db/db.ts delete mode 100644 packages/backend-core/src/pkg/db.ts diff --git a/packages/backend-core/db.js b/packages/backend-core/db.js index d2adf6c092..f7004972d5 100644 --- a/packages/backend-core/db.js +++ b/packages/backend-core/db.js @@ -1,6 +1 @@ -module.exports = { - ...require("./src/db/utils"), - ...require("./src/db/constants"), - ...require("./src/db"), - ...require("./src/db/views"), -} +module.exports = require("./src/db") diff --git a/packages/backend-core/src/clshooked/index.js b/packages/backend-core/src/clshooked/index.js deleted file mode 100644 index d69ffdd914..0000000000 --- a/packages/backend-core/src/clshooked/index.js +++ /dev/null @@ -1,650 +0,0 @@ -const util = require("util") -const assert = require("assert") -const wrapEmitter = require("emitter-listener") -const async_hooks = require("async_hooks") - -const CONTEXTS_SYMBOL = "cls@contexts" -const ERROR_SYMBOL = "error@context" - -const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED - -let currentUid = -1 - -module.exports = { - getNamespace: getNamespace, - createNamespace: createNamespace, - destroyNamespace: destroyNamespace, - reset: reset, - ERROR_SYMBOL: ERROR_SYMBOL, -} - -function Namespace(name) { - this.name = name - // changed in 2.7: no default context - this.active = null - this._set = [] - this.id = null - this._contexts = new Map() - this._indent = 0 - this._hook = null -} - -Namespace.prototype.set = function set(key, value) { - if (!this.active) { - throw new Error( - "No context available. ns.run() or ns.bind() must be called first." - ) - } - - this.active[key] = value - - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - indentStr + - "CONTEXT-SET KEY:" + - key + - "=" + - value + - " in ns:" + - this.name + - " currentUid:" + - currentUid + - " active:" + - util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) - ) - } - - return value -} - -Namespace.prototype.get = function get(key) { - if (!this.active) { - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.currentId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}` - ) - } - return undefined - } - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - indentStr + - "CONTEXT-GETTING KEY:" + - key + - "=" + - this.active[key] + - " (" + - this.name + - ") currentUid:" + - currentUid + - " active:" + - util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) - ) - debug2( - `${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${ - this.active[key] - } currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ - this._set.length - } active:${util.inspect(this.active)}` - ) - } - return this.active[key] -} - -Namespace.prototype.createContext = function createContext() { - // Prototype inherit existing context if created a new child context within existing context. - let context = Object.create(this.active ? this.active : Object.prototype) - context._ns_name = this.name - context.id = currentUid - - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-CREATED Context: (${ - this.name - }) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ - this._set.length - } context:${util.inspect(context, { - showHidden: true, - depth: 2, - colors: true, - })}` - ) - } - - return context -} - -Namespace.prototype.run = function run(fn) { - let context = this.createContext() - this.enter(context) - - try { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-RUN BEGIN: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } context:${util.inspect(context)}` - ) - } - fn(context) - return context - } catch (exception) { - if (exception) { - exception[ERROR_SYMBOL] = context - } - throw exception - } finally { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-RUN END: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - this.exit(context) - } -} - -Namespace.prototype.runAndReturn = function runAndReturn(fn) { - let value - this.run(function (context) { - value = fn(context) - }) - return value -} - -/** - * Uses global Promise and assumes Promise is cls friendly or wrapped already. - * @param {function} fn - * @returns {*} - */ -Namespace.prototype.runPromise = function runPromise(fn) { - let context = this.createContext() - this.enter(context) - - let promise = fn(context) - if (!promise || !promise.then || !promise.catch) { - throw new Error("fn must return a promise.") - } - - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise BEFORE: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - - return promise - .then(result => { - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise AFTER then: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - this.exit(context) - return result - }) - .catch(err => { - err[ERROR_SYMBOL] = context - if (DEBUG_CLS_HOOKED) { - debug2( - "CONTEXT-runPromise AFTER catch: (" + - this.name + - ") currentUid:" + - currentUid + - " len:" + - this._set.length + - " " + - util.inspect(context) - ) - } - this.exit(context) - throw err - }) -} - -Namespace.prototype.bind = function bindFactory(fn, context) { - if (!context) { - if (!this.active) { - context = this.createContext() - } else { - context = this.active - } - } - - let self = this - return function clsBind() { - self.enter(context) - try { - return fn.apply(this, arguments) - } catch (exception) { - if (exception) { - exception[ERROR_SYMBOL] = context - } - throw exception - } finally { - self.exit(context) - } - } -} - -Namespace.prototype.enter = function enter(context) { - assert.ok(context, "context must be provided for entering") - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-ENTER: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - - this._set.push(this.active) - this.active = context -} - -Namespace.prototype.exit = function exit(context) { - assert.ok(context, "context must be provided for exiting") - if (DEBUG_CLS_HOOKED) { - const asyncHooksCurrentId = async_hooks.executionAsyncId() - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) - debug2( - `${indentStr}CONTEXT-EXIT: (${ - this.name - }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ - this._set.length - } ${util.inspect(context)}` - ) - } - - // Fast path for most exits that are at the top of the stack - if (this.active === context) { - assert.ok(this._set.length, "can't remove top context") - this.active = this._set.pop() - return - } - - // Fast search in the stack using lastIndexOf - let index = this._set.lastIndexOf(context) - - if (index < 0) { - if (DEBUG_CLS_HOOKED) { - debug2( - "??ERROR?? context exiting but not entered - ignoring: " + - util.inspect(context) - ) - } - assert.ok( - index >= 0, - "context not currently entered; can't exit. \n" + - util.inspect(this) + - "\n" + - util.inspect(context) - ) - } else { - assert.ok(index, "can't remove top context") - this._set.splice(index, 1) - } -} - -Namespace.prototype.bindEmitter = function bindEmitter(emitter) { - assert.ok( - emitter.on && emitter.addListener && emitter.emit, - "can only bind real EEs" - ) - - let namespace = this - let thisSymbol = "context@" + this.name - - // Capture the context active at the time the emitter is bound. - function attach(listener) { - if (!listener) { - return - } - if (!listener[CONTEXTS_SYMBOL]) { - listener[CONTEXTS_SYMBOL] = Object.create(null) - } - - listener[CONTEXTS_SYMBOL][thisSymbol] = { - namespace: namespace, - context: namespace.active, - } - } - - // At emit time, bind the listener within the correct context. - function bind(unwrapped) { - if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { - return unwrapped - } - - let wrapped = unwrapped - let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL] - Object.keys(unwrappedContexts).forEach(function (name) { - let thunk = unwrappedContexts[name] - wrapped = thunk.namespace.bind(wrapped, thunk.context) - }) - return wrapped - } - - wrapEmitter(emitter, attach, bind) -} - -/** - * If an error comes out of a namespace, it will have a context attached to it. - * This function knows how to find it. - * - * @param {Error} exception Possibly annotated error. - */ -Namespace.prototype.fromException = function fromException(exception) { - return exception[ERROR_SYMBOL] -} - -function getNamespace(name) { - return process.namespaces[name] -} - -function createNamespace(name) { - assert.ok(name, "namespace must be given a name.") - - if (DEBUG_CLS_HOOKED) { - debug2(`NS-CREATING NAMESPACE (${name})`) - } - let namespace = new Namespace(name) - namespace.id = currentUid - - const hook = async_hooks.createHook({ - init(asyncId, type, triggerId, resource) { - currentUid = async_hooks.executionAsyncId() - - //CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec - // let initContext = namespace.active; - // if(!initContext && triggerId) { - // let parentContext = namespace._contexts.get(triggerId); - // if (parentContext) { - // namespace.active = parentContext; - // namespace._contexts.set(currentUid, parentContext); - // if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // } else if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // }else { - // namespace._contexts.set(currentUid, namespace.active); - // if (DEBUG_CLS_HOOKED) { - // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); - // debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); - // } - // } - if (namespace.active) { - namespace._contexts.set(asyncId, namespace.active) - - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } else if (currentUid === 0) { - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - const triggerId = async_hooks.triggerAsyncId() - const triggerIdContext = namespace._contexts.get(triggerId) - if (triggerIdContext) { - namespace._contexts.set(asyncId, triggerIdContext) - if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } else if (DEBUG_CLS_HOOKED) { - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - } - - if (DEBUG_CLS_HOOKED && type === "PROMISE") { - debug2(util.inspect(resource, { showHidden: true })) - const parentId = resource.parentId - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} resource:${resource}` - ) - } - }, - before(asyncId) { - currentUid = async_hooks.executionAsyncId() - let context - - /* - if(currentUid === 0){ - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - //const triggerId = async_hooks.triggerAsyncId(); - context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); - }else{ - context = namespace._contexts.get(currentUid); - } - */ - - //HACK to work with promises until they are fixed in node > 8.1.1 - context = - namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) - - if (context) { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - namespace._indent += 2 - } - - namespace.enter(context) - } else if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} namespace._contexts:${util.inspect(namespace._contexts, { - showHidden: true, - depth: 2, - colors: true, - })}` - ) - namespace._indent += 2 - } - }, - after(asyncId) { - currentUid = async_hooks.executionAsyncId() - let context // = namespace._contexts.get(currentUid); - /* - if(currentUid === 0){ - // CurrentId will be 0 when triggered from C++. Promise events - // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid - //const triggerId = async_hooks.triggerAsyncId(); - context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); - }else{ - context = namespace._contexts.get(currentUid); - } - */ - //HACK to work with promises until they are fixed in node > 8.1.1 - context = - namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) - - if (context) { - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - namespace._indent -= 2 - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - } - - namespace.exit(context) - } else if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - namespace._indent -= 2 - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(context)}` - ) - } - }, - destroy(asyncId) { - currentUid = async_hooks.executionAsyncId() - if (DEBUG_CLS_HOOKED) { - const triggerId = async_hooks.triggerAsyncId() - const indentStr = " ".repeat( - namespace._indent < 0 ? 0 : namespace._indent - ) - debug2( - `${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect( - namespace.active, - { showHidden: true, depth: 2, colors: true } - )} context:${util.inspect(namespace._contexts.get(currentUid))}` - ) - } - - namespace._contexts.delete(asyncId) - }, - }) - - hook.enable() - namespace._hook = hook - - process.namespaces[name] = namespace - return namespace -} - -function destroyNamespace(name) { - let namespace = getNamespace(name) - - assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"') - assert.ok( - namespace.id, - "don't assign to process.namespaces directly! " + util.inspect(namespace) - ) - - namespace._hook.disable() - namespace._contexts = null - process.namespaces[name] = null -} - -function reset() { - // must unregister async listeners - if (process.namespaces) { - Object.keys(process.namespaces).forEach(function (name) { - destroyNamespace(name) - }) - } - process.namespaces = Object.create(null) -} - -process.namespaces = process.namespaces || {} - -//const fs = require('fs'); -function debug2(...args) { - if (DEBUG_CLS_HOOKED) { - //fs.writeSync(1, `${util.format(...args)}\n`); - process._rawDebug(`${util.format(...args)}`) - } -} - -/*function getFunctionName(fn) { - if (!fn) { - return fn; - } - if (typeof fn === 'function') { - if (fn.name) { - return fn.name; - } - return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; - } else if (fn.constructor && fn.constructor.name) { - return fn.constructor.name; - } -}*/ diff --git a/packages/backend-core/src/context/FunctionContext.ts b/packages/backend-core/src/context/FunctionContext.ts deleted file mode 100644 index 1010f585ef..0000000000 --- a/packages/backend-core/src/context/FunctionContext.ts +++ /dev/null @@ -1,47 +0,0 @@ -import cls from "../clshooked" -import { newid } from "../hashing" - -const REQUEST_ID_KEY = "requestId" -const MAIN_CTX = cls.createNamespace("main") - -function getContextStorage(namespace: any) { - if (namespace && namespace.active) { - let contextData = namespace.active - delete contextData.id - delete contextData._ns_name - return contextData - } - return {} -} - -class FunctionContext { - static run(callback: any) { - return MAIN_CTX.runAndReturn(async () => { - const namespaceId = newid() - MAIN_CTX.set(REQUEST_ID_KEY, namespaceId) - const namespace = cls.createNamespace(namespaceId) - let response = await namespace.runAndReturn(callback) - cls.destroyNamespace(namespaceId) - return response - }) - } - - static setOnContext(key: string, value: any) { - const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) - const namespace = cls.getNamespace(namespaceId) - namespace.set(key, value) - } - - static getFromContext(key: string) { - const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) - const namespace = cls.getNamespace(namespaceId) - const context = getContextStorage(namespace) - if (context) { - return context[key] - } else { - return null - } - } -} - -export = FunctionContext diff --git a/packages/backend-core/src/context/constants.ts b/packages/backend-core/src/context/constants.ts index eb156c357b..64fdb45dec 100644 --- a/packages/backend-core/src/context/constants.ts +++ b/packages/backend-core/src/context/constants.ts @@ -1,9 +1,5 @@ import { IdentityContext } from "@budibase/types" -export enum ContextKey { - MAIN = "main", -} - export type ContextMap = { tenantId?: string appId?: string diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts index 34e6603ef7..910bd192b4 100644 --- a/packages/backend-core/src/context/index.ts +++ b/packages/backend-core/src/context/index.ts @@ -1,13 +1,16 @@ import env from "../environment" -import { SEPARATOR, DocumentType } from "../db/constants" -import cls from "./FunctionContext" -import { baseGlobalDBName } from "../db/tenancy" +import { + SEPARATOR, + DocumentType, + getDevelopmentAppID, + getProdAppID, + baseGlobalDBName, + PouchLike, +} from "../db" +import { Context } from "./localStorage" import { IdentityContext } from "@budibase/types" import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" -import { ContextMap, ContextKey } from "./constants" -import { PouchLike } from "../db" -import { getDevelopmentAppID, getProdAppID } from "../db/conversions" - +import { ContextMap } from "./constants" export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID // some test cases call functions directly, need to @@ -19,7 +22,7 @@ export function isMultiTenant() { } export function isTenantIdSet() { - const context = cls.getFromContext(ContextKey.MAIN) as ContextMap + const context = Context.get() return !!context?.tenantId } @@ -53,7 +56,7 @@ export function getTenantIDFromAppID(appId: string) { function updateContext(updates: ContextMap) { let context: ContextMap try { - context = cls.getFromContext(ContextKey.MAIN) + context = Context.get() } catch (err) { // no context, start empty context = {} @@ -68,10 +71,7 @@ function updateContext(updates: ContextMap) { async function newContext(updates: ContextMap, task: any) { // see if there already is a context setup let context: ContextMap = updateContext(updates) - return cls.run(async () => { - cls.setOnContext(ContextKey.MAIN, context) - return await task() - }) + return Context.run(context, task) } export async function doInContext(appId: string, task: any): Promise { @@ -132,7 +132,7 @@ export async function doInIdentityContext( export function getIdentity(): IdentityContext | undefined { try { - const context = cls.getFromContext(ContextKey.MAIN) as ContextMap + const context = Context.get() return context?.identity } catch (e) { // do nothing - identity is not in context @@ -143,7 +143,7 @@ export function getTenantId(): string { if (!isMultiTenant()) { return DEFAULT_TENANT_ID } - const context = cls.getFromContext(ContextKey.MAIN) as ContextMap + const context = Context.get() const tenantId = context?.tenantId if (!tenantId) { throw new Error("Tenant id not found") @@ -152,7 +152,7 @@ export function getTenantId(): string { } export function getAppId(): string | undefined { - const context = cls.getFromContext(ContextKey.MAIN) as ContextMap + const context = Context.get() const foundId = context?.appId if (!foundId && env.isTest() && TEST_APP_ID) { return TEST_APP_ID @@ -165,7 +165,7 @@ export function updateTenantId(tenantId?: string) { let context: ContextMap = updateContext({ tenantId, }) - cls.setOnContext(ContextKey.MAIN, context) + Context.set(context) } export function updateAppId(appId: string) { @@ -173,7 +173,7 @@ export function updateAppId(appId: string) { appId, }) try { - cls.setOnContext(ContextKey.MAIN, context) + Context.set(context) } catch (err) { if (env.isTest()) { TEST_APP_ID = appId @@ -184,7 +184,7 @@ export function updateAppId(appId: string) { } export function getGlobalDB(): PouchLike { - const context = cls.getFromContext(ContextKey.MAIN) as ContextMap + const context = Context.get() return new PouchLike(baseGlobalDBName(context?.tenantId)) } diff --git a/packages/backend-core/src/context/localStorage.ts b/packages/backend-core/src/context/localStorage.ts new file mode 100644 index 0000000000..fd761d10bc --- /dev/null +++ b/packages/backend-core/src/context/localStorage.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "async_hooks" +import { ContextMap } from "./constants" + +export class Context { + static storage = new AsyncLocalStorage() + + static run(context: ContextMap, func: any) { + return Context.storage.run(context, () => func()) + } + + static get(): ContextMap { + return Context.storage.getStore() as ContextMap + } + + static set(context: ContextMap) { + Context.storage.enterWith(context) + } +} diff --git a/packages/backend-core/src/db/couch/pouchDB.ts b/packages/backend-core/src/db/couch/pouchDB.ts index eead0e4d6f..07f7b6c90d 100644 --- a/packages/backend-core/src/db/couch/pouchDB.ts +++ b/packages/backend-core/src/db/couch/pouchDB.ts @@ -53,7 +53,7 @@ export const getPouch = (opts: any = {}) => { return PouchDB.defaults(POUCH_DB_DEFAULTS) } -export async function init(opts?: PouchOptions) { +export function init(opts?: PouchOptions) { Pouch = getPouch(opts) initialised = true } diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts new file mode 100644 index 0000000000..497b747b80 --- /dev/null +++ b/packages/backend-core/src/db/db.ts @@ -0,0 +1,50 @@ +import env from "../environment" +import { directCouchQuery, PouchLike } from "./couch" +import { CouchFindOptions } from "@budibase/types" + +let initialised = false +const dbList = new Set() + +const checkInitialised = () => { + if (!initialised) { + throw new Error("init has not been called") + } +} + +export function getDB(dbName: string, opts?: any): PouchLike { + if (env.isTest()) { + dbList.add(dbName) + } + return new PouchLike(dbName, opts) +} + +// we have to use a callback for this so that we can close +// the DB when we're done, without this manual requests would +// need to close the database when done with it to avoid memory leaks +export async function doWithDB(dbName: string, cb: any, opts = {}) { + const db = getDB(dbName, opts) + // need this to be async so that we can correctly close DB after all + // async operations have been completed + return await cb(db) +} + +export function allDbs() { + if (!env.isTest()) { + throw new Error("Cannot be used outside test environment.") + } + checkInitialised() + return [...dbList] +} + +export async function directCouchAllDbs(queryString?: string) { + let couchPath = "/_all_dbs" + if (queryString) { + couchPath += `?${queryString}` + } + return await directCouchQuery(couchPath) +} + +export async function directCouchFind(dbName: string, opts: CouchFindOptions) { + const json = await directCouchQuery(`${dbName}/_find`, "POST", opts) + return { rows: json.docs, bookmark: json.bookmark } +} diff --git a/packages/backend-core/src/db/index.ts b/packages/backend-core/src/db/index.ts index 7cc49eae40..7269aa8f92 100644 --- a/packages/backend-core/src/db/index.ts +++ b/packages/backend-core/src/db/index.ts @@ -1,51 +1,7 @@ -import env from "../environment" -import { CouchFindOptions } from "@budibase/types" -import { directCouchQuery, PouchLike } from "./couch" -export { init, PouchLike, getPouch, getPouchDB } from "./couch" - -let initialised = false -const dbList = new Set() - -const checkInitialised = () => { - if (!initialised) { - throw new Error("init has not been called") - } -} - -export function getDB(dbName: string, opts?: any): PouchLike { - if (env.isTest()) { - dbList.add(dbName) - } - return new PouchLike(dbName, opts) -} - -// we have to use a callback for this so that we can close -// the DB when we're done, without this manual requests would -// need to close the database when done with it to avoid memory leaks -export async function doWithDB(dbName: string, cb: any, opts = {}) { - const db = getDB(dbName, opts) - // need this to be async so that we can correctly close DB after all - // async operations have been completed - return await cb(db) -} - -export function allDbs() { - if (!env.isTest()) { - throw new Error("Cannot be used outside test environment.") - } - checkInitialised() - return [...dbList] -} - -export async function directCouchAllDbs(queryString?: string) { - let couchPath = "/_all_dbs" - if (queryString) { - couchPath += `?${queryString}` - } - return await directCouchQuery(couchPath) -} - -export async function directCouchFind(dbName: string, opts: CouchFindOptions) { - const json = await directCouchQuery(`${dbName}/_find`, "POST", opts) - return { rows: json.docs, bookmark: json.bookmark } -} +export * from "./couch" +export * from "./db" +export * from "./utils" +export * from "./views" +export * from "./constants" +export * from "./conversions" +export * from "./tenancy" diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 9a4ed98418..fc67a2c49b 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -10,7 +10,7 @@ import { } from "./constants" import { getTenantId, getGlobalDB } from "../context" import { getGlobalDBName } from "./tenancy" -import { doWithDB, allDbs, directCouchAllDbs } from "./index" +import { doWithDB, allDbs, directCouchAllDbs } from "./db" import { getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "./conversions" import { APP_PREFIX } from "./constants" diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index b45248abc2..a2cfaaa9c6 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -24,7 +24,7 @@ import * as queue from "./queue" import * as types from "./types" // mimic the outer package exports -import * as db from "./pkg/db" +import * as db from "./db" import * as objectStore from "./pkg/objectStore" import * as utils from "./pkg/utils" import redis from "./pkg/redis" diff --git a/packages/backend-core/src/pkg/db.ts b/packages/backend-core/src/pkg/db.ts deleted file mode 100644 index d2bc474786..0000000000 --- a/packages/backend-core/src/pkg/db.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Mimic the outer package export for usage in index.ts -// The outer exports can't be used as they now reference dist directly -export * from "../db" -export * from "../db/utils" -export * from "../db/views" -export * from "../db/constants"