feat: cron to clean require cache
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import {
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from '@nestjs/common'
|
||||
import { Interval } from '@nestjs/schedule'
|
||||
import { isURL } from 'class-validator'
|
||||
import fs, { mkdir, stat } from 'fs/promises'
|
||||
import { cloneDeep } from 'lodash'
|
||||
@@ -12,7 +17,7 @@ import { UniqueArray } from '~/ts-hepler/unique'
|
||||
import { safePathJoin } from '~/utils'
|
||||
import { safeEval } from '~/utils/safe-eval.util'
|
||||
import { isBuiltinModule } from '~/utils/sys.util'
|
||||
import type PKG from '../../../package.json'
|
||||
import PKG from '../../../package.json'
|
||||
import { SnippetModel } from '../snippet/snippet.model'
|
||||
import {
|
||||
FunctionContextRequest,
|
||||
@@ -78,8 +83,6 @@ export class ServerlessService {
|
||||
name: model.name,
|
||||
reference: model.reference,
|
||||
|
||||
// TODO
|
||||
// write file to asset
|
||||
writeAsset: async (
|
||||
path: string,
|
||||
data: any,
|
||||
@@ -91,7 +94,6 @@ export class ServerlessService {
|
||||
options,
|
||||
)
|
||||
},
|
||||
// read file to asset
|
||||
readAsset: async (
|
||||
path: string,
|
||||
options: Parameters<typeof fs.readFile>[1],
|
||||
@@ -99,8 +101,10 @@ export class ServerlessService {
|
||||
return await this.assetService.getAsset(safePathJoin(path), options)
|
||||
},
|
||||
},
|
||||
|
||||
// inject global
|
||||
__dirname: DATA_DIR,
|
||||
__filename: '',
|
||||
|
||||
// inject some zx utils
|
||||
fetch,
|
||||
@@ -112,108 +116,11 @@ export class ServerlessService {
|
||||
console: logger,
|
||||
logger,
|
||||
|
||||
require: (() => {
|
||||
const __require = (id: string) => {
|
||||
const module = require(id)
|
||||
require: this.inNewContextRequire(),
|
||||
get import() {
|
||||
return this.require
|
||||
},
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
const __requireNoCache = (id: string) => {
|
||||
delete require.cache[require.resolve(id)]
|
||||
const module = require(id)
|
||||
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
async function $require(
|
||||
this: ServerlessService,
|
||||
id: string,
|
||||
useCache = true,
|
||||
) {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('require id is not valid')
|
||||
}
|
||||
|
||||
// 1. if is remote module
|
||||
if (
|
||||
isURL(id, { protocols: ['http', 'https'], require_protocol: true })
|
||||
) {
|
||||
const text = await this.httpService.getAndCacheRequest(id)
|
||||
return await safeEval(`${text}; return module.exports`, {
|
||||
exports: {},
|
||||
module: {
|
||||
exports: null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 2. if application third part lib
|
||||
|
||||
const allowedThirdPartLibs: UniqueArray<
|
||||
(keyof typeof PKG.dependencies)[]
|
||||
> = [
|
||||
'algoliasearch',
|
||||
'axios-retry',
|
||||
'axios',
|
||||
'class-transformer',
|
||||
'class-validator',
|
||||
'dayjs',
|
||||
'ejs',
|
||||
'html-minifier',
|
||||
'image-size',
|
||||
'isbot',
|
||||
'js-yaml',
|
||||
'jsdom',
|
||||
'jszip',
|
||||
'lodash',
|
||||
'marked',
|
||||
'nanoid',
|
||||
'qs',
|
||||
'rxjs',
|
||||
'snakecase-keys',
|
||||
'ua-parser-js',
|
||||
'xss',
|
||||
]
|
||||
|
||||
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
|
||||
|
||||
if (
|
||||
allowedThirdPartLibs.includes(id as any) ||
|
||||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
|
||||
) {
|
||||
return useCache ? __require(id) : __requireNoCache(id)
|
||||
}
|
||||
|
||||
// 3. mock built-in module
|
||||
|
||||
const mockModules = {
|
||||
fs: {
|
||||
writeFile: globalContext.context.writeAsset,
|
||||
readFile: globalContext.context.readAsset,
|
||||
},
|
||||
}
|
||||
|
||||
if (Object.keys(mockModules).includes(id)) {
|
||||
return mockModules[id]
|
||||
}
|
||||
|
||||
// fin. is built-in module
|
||||
const module = isBuiltinModule(id, [
|
||||
'fs',
|
||||
'os',
|
||||
'child_process',
|
||||
'sys',
|
||||
])
|
||||
if (!module) {
|
||||
throw new Error(`cannot require ${id}`)
|
||||
} else {
|
||||
return __require(id)
|
||||
}
|
||||
}
|
||||
|
||||
return $require.bind(this)
|
||||
})(),
|
||||
process: {
|
||||
env: Object.freeze({ ...process.env }),
|
||||
nextTick: process.nextTick,
|
||||
@@ -226,6 +133,129 @@ export class ServerlessService {
|
||||
)
|
||||
}
|
||||
|
||||
private requireModuleIdSet = new Set<string>()
|
||||
|
||||
@Interval(5 * 60 * 1000)
|
||||
private cleanRequireCache() {
|
||||
Array.from(this.requireModuleIdSet.values()).forEach((id) => {
|
||||
delete require.cache[id]
|
||||
})
|
||||
|
||||
this.requireModuleIdSet.clear()
|
||||
}
|
||||
private inNewContextRequire() {
|
||||
const __require = (id: string) => {
|
||||
const module = require(id)
|
||||
// eslint-disable-next-line no-empty
|
||||
if (Object.keys(PKG.dependencies).includes(id) || isBuiltinModule(id)) {
|
||||
} else {
|
||||
this.requireModuleIdSet.add(require.resolve(id))
|
||||
}
|
||||
return cloneDeep(module)
|
||||
}
|
||||
|
||||
const __requireNoCache = (id: string) => {
|
||||
delete require.cache[require.resolve(id)]
|
||||
const clonedModule = __require(id)
|
||||
|
||||
return clonedModule
|
||||
}
|
||||
|
||||
async function $require(
|
||||
this: ServerlessService,
|
||||
id: string,
|
||||
useCache = true,
|
||||
) {
|
||||
if (!id || typeof id !== 'string') {
|
||||
throw new Error('require id is not valid')
|
||||
}
|
||||
|
||||
// 1. if is remote module
|
||||
if (isURL(id, { protocols: ['http', 'https'], require_protocol: true })) {
|
||||
let text: string
|
||||
|
||||
try {
|
||||
text = useCache
|
||||
? await this.httpService.getAndCacheRequest(id)
|
||||
: await this.httpService.axiosRef.get(id).then((res) => res.data)
|
||||
} catch (err) {
|
||||
throw new InternalServerErrorException(
|
||||
'Failed to fetch remote module',
|
||||
)
|
||||
}
|
||||
return await safeEval(
|
||||
`${text}; return module.exports ? module.exports : exports.default ? exports.default : exports`,
|
||||
{
|
||||
exports: {},
|
||||
module: {
|
||||
exports: null,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 2. if application third part lib
|
||||
|
||||
const allowedThirdPartLibs: UniqueArray<
|
||||
(keyof typeof PKG.dependencies)[]
|
||||
> = [
|
||||
'algoliasearch',
|
||||
'axios-retry',
|
||||
'axios',
|
||||
'class-transformer',
|
||||
'class-validator',
|
||||
'dayjs',
|
||||
'ejs',
|
||||
'html-minifier',
|
||||
'image-size',
|
||||
'isbot',
|
||||
'js-yaml',
|
||||
'jsdom',
|
||||
'jszip',
|
||||
'lodash',
|
||||
'marked',
|
||||
'nanoid',
|
||||
'qs',
|
||||
'rxjs',
|
||||
'snakecase-keys',
|
||||
'ua-parser-js',
|
||||
'xss',
|
||||
]
|
||||
|
||||
const trustPackagePrefixes = ['@innei/', '@mx-space/', 'mx-function-']
|
||||
|
||||
if (
|
||||
allowedThirdPartLibs.includes(id as any) ||
|
||||
trustPackagePrefixes.some((prefix) => id.startsWith(prefix))
|
||||
) {
|
||||
return useCache ? __require(id) : __requireNoCache(id)
|
||||
}
|
||||
|
||||
// 3. mock built-in module
|
||||
|
||||
// const mockModules = {
|
||||
// fs: {
|
||||
// writeFile: globalContext.context.writeAsset,
|
||||
// readFile: globalContext.context.readAsset,
|
||||
// },
|
||||
// }
|
||||
|
||||
// if (Object.keys(mockModules).includes(id)) {
|
||||
// return mockModules[id]
|
||||
// }
|
||||
|
||||
// fin. is built-in module
|
||||
const module = isBuiltinModule(id, ['fs', 'os', 'child_process', 'sys'])
|
||||
if (!module) {
|
||||
throw new Error(`cannot require ${id}`)
|
||||
} else {
|
||||
return __require(id)
|
||||
}
|
||||
}
|
||||
|
||||
return $require.bind(this)
|
||||
}
|
||||
|
||||
async isValidServerlessFunction(raw: string) {
|
||||
try {
|
||||
return safeEval(`
|
||||
|
||||
Reference in New Issue
Block a user