import { computedFn } from 'mobx-utils'; import { createSimpleSchema, custom, map, object, primitive, PropSchema, serializable, SKIP } from 'serializr'; import { DocServer } from '../client/DocServer'; import { CompiledScript, CompileScript, ScriptOptions } from '../client/util/Scripting'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; import { Doc, Field, Opt } from './Doc'; import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; import { Plugins } from './util'; function optional(propSchema: PropSchema) { return custom( value => { if (value !== undefined) { return propSchema.serializer(value); } return SKIP; }, (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => { if (jsonValue !== undefined) { return propSchema.deserializer(jsonValue, callback, context, oldValue); } return SKIP; } ); } const optionsSchema = createSimpleSchema({ requiredType: true, addReturn: true, typecheck: true, editable: true, readonly: true, params: optional(map(primitive())), }); const scriptSchema = createSimpleSchema({ options: object(optionsSchema), originalScript: true, }); function finalizeScript(script: ScriptField) { const comp = CompileScript(script.script.originalScript, script.script.options); if (!comp.compiled) { throw new Error("Couldn't compile loaded script"); } if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); if (!compset.compiled) { throw new Error("Couldn't compile setter script"); } (script as any).setterscript = compset; } return comp; } async function deserializeScript(script: ScriptField) { if (script.captures) { const captured: any = {}; (script.script.options as ScriptOptions).capturedVariables = captured; Promise.all( script.captures.map(async capture => { const key = capture.split(':')[0]; const val = capture.split(':')[1]; if (val === 'true') captured[key] = true; else if (val === 'false') captured[key] = false; else if (val.startsWith('ID->')) captured[key] = await DocServer.GetRefField(val.replace('ID->', '')); else if (!isNaN(Number(val))) captured[key] = Number(val); else captured[key] = val; }) ).then(() => ((script as any).script = finalizeScript(script))); } else { (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script); } } @scriptingGlobal @Deserializable('script', deserializeScript) export class ScriptField extends ObjectField { @serializable readonly rawscript: string | undefined; @serializable(object(scriptSchema)) readonly script: CompiledScript; @serializable(object(scriptSchema)) readonly setterscript: CompiledScript | undefined; @serializable(autoObject()) captures?: List; public static _scriptFieldCache: Map> = new Map(); public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } constructor(script: CompiledScript | undefined, setterscript?: CompiledScript, rawscript?: string) { super(); const captured = script?.options.capturedVariables; if (captured) { this.captures = new List(Object.keys(captured).map(key => key + ':' + (captured[key] instanceof Doc ? 'ID->' + (captured[key] as Doc)[Id] : captured[key].toString()))); } this.rawscript = rawscript; this.setterscript = setterscript; this.script = script ?? (CompileScript('false') as CompiledScript); } // init(callback: (res: Field) => any) { // const options = this.options!; // const keys = Object.keys(options.options.capturedIds); // Server.GetFields(keys).then(fields => { // let captured: { [name: string]: Field } = {}; // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]); // const opts: ScriptOptions = { // addReturn: options.options.addReturn, // params: options.options.params, // requiredType: options.options.requiredType, // capturedVariables: captured // }; // const script = CompileScript(options.script, opts); // if (!script.compiled) { // throw new Error("Can't compile script"); // } // this._script = script; // callback(this); // }); // } [Copy](): ObjectField { return new ScriptField(this.script, this.setterscript, this.rawscript); } toString() { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } [ToScriptString]() { return 'script field'; } [ToString]() { return this.script.originalScript; } public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { 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 _last_: 'any', // _last_ is the previous value of a computed field when it is being triggered to re-run. _readOnly_: 'boolean', // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) ...params, }, typecheck: false, editable: true, addReturn: addReturn, capturedVariables, }); } public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, false, capturedVariables); return compiled.compiled ? new ScriptField(compiled) : undefined; } } @scriptingGlobal @Deserializable('computed', deserializeScript) export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ComputedField(compiled) : undefined; } public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt) { if (!doc[`${fieldKey}-indexed`]) { const flist = new List(numberRange(curTimecode + 1).map(i => undefined) as any as number[]); flist[curTimecode] = Cast(doc[fieldKey], 'number', null); doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, ${defaultVal})`, {}, true, {}); const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (!doc[`${fieldKey}-indexed`]) { const flist = new List(numberRange(curTimecode + 1).map(i => undefined) as any as string[]); flist[curTimecode] = StrCast(doc[fieldKey]); doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (!doc[`${fieldKey}-indexed`]) { const flist = new List(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]); flist[curTimecode] = Field.Copy(doc[fieldKey]); doc[`${fieldKey}-indexed`] = flist; } const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } export namespace ComputedField { let useComputed = true; export function DisableComputedFields() { useComputed = false; } export function EnableComputedFields() { useComputed = true; } export const undefined = '__undefined'; export function WithoutComputed(fn: () => T) { DisableComputedFields(); try { return fn(); } finally { EnableComputedFields(); } } export function initPlugin() { Plugins.addGetterPlugin((doc, _, value) => { if (useComputed && value instanceof ComputedField) { return { value: value._valueOutsideReaction(doc), shouldReturn: true }; } }); } } ScriptingGlobals.add( function setIndexVal(list: any[], index: number, value: any) { while (list.length <= index) list.push(undefined); list[index] = value; }, 'sets the value at a given index of a list', '(list: any[], index: number, value: any)' ); ScriptingGlobals.add( function getIndexVal(list: any[], index: number, defaultVal: Opt = undefined) { return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal); }, 'returns the value at a given index of a list', '(list: any[], index: number)' ); ScriptingGlobals.add( function makeScript(script: string) { return ScriptField.MakeScript(script); }, 'returns the value at a given index of a list', '(list: any[], index: number)' );