From f82b0fef26782f6a871da036a6bd9a6670444509 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 27 Jul 2022 15:51:00 -0400 Subject: fixed document leak related to capturedVariables in scripts. changed capturedVariables to be a string encoding the captured variables instead of a Doc. --- src/client/util/Scripting.ts | 65 ++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 32 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 3791bec73..ea2bf6551 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -5,12 +5,11 @@ // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import * as ts from "typescript"; -import { Doc, Field } from "../../fields/Doc"; -import { scriptingGlobals, ScriptingGlobals } from "./ScriptingGlobals"; +import * as ts from 'typescript'; +import { Doc, Field } from '../../fields/Doc'; +import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; export { ts }; - export interface ScriptSuccess { success: true; result: any; @@ -46,7 +45,6 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is return false; } - function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); if ((options.typecheck !== false && errors.length) || !script) { @@ -63,7 +61,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; for (const name of customParams) { - if (name === "this") { + if (name === 'this') { continue; } if (name in args) { @@ -86,11 +84,10 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an return { success: true, result }; } catch (error) { - if (batch) { batch.end(); } - onError?.(script + " " + error); + onError?.(script + ' ' + error); return { success: false, error, result: errorVal }; } }; @@ -151,16 +148,16 @@ class ScriptingCompilerHost { } export type Traverser = (node: ts.Node, indentation: string) => boolean | void; -export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser }; +export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { - transformer: ts.TransformerFactory, - getVars?: () => { capturedVariables: { [name: string]: Field } } + transformer: ts.TransformerFactory; + getVars?: () => { capturedVariables: { [name: string]: Field } }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value - addReturn?: boolean; // does the compiler automatically add a return statement + addReturn?: boolean; // does the compiler automatically add a return statement params?: { [name: string]: string }; // list of function parameters and their types - capturedVariables?: { [name: string]: Field }; // list of captured variables + capturedVariables?: { [name: string]: Doc | number | string | boolean }; // list of captured variables typecheck?: boolean; // should the compiler perform typechecking editable?: boolean; // can the script edit Docs traverser?: TraverserParam; @@ -169,24 +166,28 @@ export interface ScriptOptions { } // function forEachNode(node:ts.Node, fn:(node:any) => void); -function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") { - return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { - forEachNode(n, onEnter, onExit, indentation + " "); - }) || (onExit && onExit(node, indentation)); +function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = '') { + return ( + onEnter(node, indentation) || + ts.forEachChild(node, (n: any) => { + forEachNode(n, onEnter, onExit, indentation + ' '); + }) || + (onExit && onExit(node, indentation)) + ); } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { - const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; + const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; if (options.params && !options.params.this) options.params.this = Doc.name; if (options.params && !options.params.self) options.params.self = Doc.name; if (options.globals) { ScriptingGlobals.setScriptingGlobals(options.globals); } - const host = new ScriptingCompilerHost; + const host = new ScriptingCompilerHost(); if (options.traverser) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); - const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; - const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; + const onEnter = typeof options.traverser === 'object' ? options.traverser.onEnter : options.traverser; + const onLeave = typeof options.traverser === 'object' ? options.traverser.onLeave : undefined; forEachNode(sourceFile, onEnter, onLeave); } if (options.transformer) { @@ -199,17 +200,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp } const transformed = result.transformed; const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed + newLine: ts.NewLineKind.LineFeed, }); script = printer.printFile(transformed[0]); result.dispose(); } const paramNames: string[] = []; - if ("this" in params || "this" in capturedVariables) { - paramNames.push("this"); + if ('this' in params || 'this' in capturedVariables) { + paramNames.push('this'); } for (const key in params) { - if (key === "this") continue; + if (key === 'this') continue; paramNames.push(key); } const paramList = paramNames.map(key => { @@ -217,21 +218,21 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp return `${key}: ${val}`; }); for (const key in capturedVariables) { - if (key === "this") continue; + if (key === 'this') continue; const val = capturedVariables[key]; paramNames.push(key); - paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`); + paramList.push(`${key}: ${typeof val === 'object' ? Object.getPrototypeOf(val).constructor.name : typeof val}`); } - const paramString = paramList.join(", "); - const body = addReturn && !script.startsWith("{ return") ? `return ${script};` : script; + const paramString = paramList.join(', '); + const body = addReturn && !script.startsWith('{ return') ? `return ${script};` : script; const reqTypes = requiredType ? `: ${requiredType}` : ''; const funcScript = `(function(${paramString})${reqTypes} { ${body} })`; - host.writeFile("file.ts", funcScript); + host.writeFile('file.ts', funcScript); if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); - const program = ts.createProgram(["file.ts"], {}, host); + const program = ts.createProgram(['file.ts'], {}, host); const testResult = program.emit(); - const outputText = host.readFile("file.js"); + const outputText = host.readFile('file.js'); const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); -- cgit v1.2.3-70-g09d2 From 5dcf35a986bb1d9dbe6c26d0a7056ac43ceed007 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 30 Aug 2022 12:11:49 -0400 Subject: made script caching more robust to avoid compiles and speed up startup. --- src/client/util/Scripting.ts | 8 ++++++++ .../collectionFreeForm/CollectionFreeFormView.tsx | 6 +++--- src/client/views/pdf/Annotation.tsx | 3 ++- src/fields/ScriptField.ts | 12 +++++------- 4 files changed, 18 insertions(+), 11 deletions(-) (limited to 'src/client/util/Scripting.ts') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index ea2bf6551..6dcdcb71b 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,6 +7,8 @@ import * as typescriptlib from '!!raw-loader!./type_decls.d'; import * as ts from 'typescript'; import { Doc, Field } from '../../fields/Doc'; +import { ObjectField } from '../../fields/ObjectField'; +import { ScriptField } from '../../fields/ScriptField'; import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; export { ts }; @@ -177,6 +179,10 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { + const captured = options.capturedVariables ?? {}; + const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, ''); + const found = ScriptField.GetScriptFieldCache(script + ':' + signature); + if (found) return found as CompiledScript; const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; if (options.params && !options.params.this) options.params.this = Doc.name; if (options.params && !options.params.self) options.params.self = Doc.name; @@ -241,6 +247,8 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if (options.globals) { ScriptingGlobals.resetScriptingGlobals(); } + !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript); + //console.log('COMPILED: ' + script + ':' + signature); return result; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index c2bd7de54..0c4de681a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1203,8 +1203,8 @@ export class CollectionFreeFormView extends CollectionSubView - {this.props.Document.title?.toString()} + {this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()} ); } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 44f815336..ee418a02f 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -107,7 +107,8 @@ class RegionAnnotation extends React.Component { pointerEvents: this.props.pointerEvents?.() as any, outline: brushed === Doc.DocBrushStatus.linkHighlighted ? 'solid 1px lightBlue' : undefined, backgroundColor: brushed === Doc.DocBrushStatus.highlighted ? 'orange' : StrCast(this.props.document.backgroundColor), - }}> + }} + /> ); } } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index d38a019b3..3cb50a4c0 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc'; import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; -import { Cast, NumCast, StrCast } from './Types'; +import { Cast, StrCast } from './Types'; import { Plugins } from './util'; function optional(propSchema: PropSchema) { @@ -43,12 +43,11 @@ const scriptSchema = createSimpleSchema({ originalScript: true, }); -function finalizeScript(script: ScriptField, captures: boolean) { +function finalizeScript(script: ScriptField) { const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } - !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp); if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); if (!compset.compiled) { @@ -72,9 +71,9 @@ async function deserializeScript(script: ScriptField) { else if (!isNaN(Number(val))) captured[key] = Number(val); else captured[key] = val; }) - ).then(() => ((script as any).script = finalizeScript(script, true))); + ).then(() => ((script as any).script = finalizeScript(script))); } else { - (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script, false); + (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); } } @@ -143,7 +142,7 @@ export class ScriptField extends ObjectField { return this.script.originalScript; } public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { - const compiled = CompileScript(script, { + return CompileScript(script, { params: { this: Doc?.name || 'Doc', // this is the doc that executes the script self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script @@ -156,7 +155,6 @@ export class ScriptField extends ObjectField { addReturn: addReturn, capturedVariables, }); - return compiled; } public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); -- cgit v1.2.3-70-g09d2