diff --git a/package.json b/package.json index 295d2a97..66adcece 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stated-js", - "version": "0.1.54", + "version": "0.1.55", "license": "Apache-2.0", "description": "JSONata embedded in JSON", "main": "./dist/src/index.js", diff --git a/src/ParallelExecutionPlanDefault.ts b/src/ParallelExecutionPlanDefault.ts index 6523f5c8..3765b1ea 100644 --- a/src/ParallelExecutionPlanDefault.ts +++ b/src/ParallelExecutionPlanDefault.ts @@ -17,8 +17,10 @@ export class ParallelExecutionPlanDefault implements ParallelExecutionPlan { didUpdate: boolean = false; restore?: boolean = false; circular?:boolean; + tp:TemplateProcessor constructor(tp: TemplateProcessor, parallelSteps: ParallelExecutionPlan[] = [], vals?: Partial | null) { + this.tp = tp; this.output = tp.output; this.parallel = parallelSteps; // Copy properties from `vals` into `this`, while preserving existing defaults @@ -91,6 +93,15 @@ export class ParallelExecutionPlanDefault implements ParallelExecutionPlan { return Array.from(nodeSet); } + reinitialize(): void { + const walkTree = (node: ParallelExecutionPlan) => { + node.completed = false; + node.output = this.tp.output; + node.parallel.forEach(child => walkTree(child)); + }; + walkTree(this); + } + /** * make a shell of an ExecutionPlan that exist just to carry the jsonPtr, with op set to "noop" */ diff --git a/src/ParallelPlanner.ts b/src/ParallelPlanner.ts index c38db93f..5a27dced 100644 --- a/src/ParallelPlanner.ts +++ b/src/ParallelPlanner.ts @@ -35,7 +35,16 @@ export class ParallelPlanner implements Planner{ //remember, initialization plan is not always for "/" because we can be initializing an imported template getInitializationPlan(jsonPtr:JsonPointerString): ExecutionPlan { - return this.makeInitializationPlan({jsonPtr}); + const cached = this.planCache.get(jsonPtr); + let initPlan: ExecutionPlan; + if(cached) { + [initPlan] = cached; + (initPlan as ParallelExecutionPlanDefault).reinitialize(); //allow this plan to be re-run + } else { + initPlan = this.makeInitializationPlan({jsonPtr}); + this.planCache.set("/", [initPlan, ['/']]); + } + return initPlan; } diff --git a/src/TemplateProcessor.ts b/src/TemplateProcessor.ts index f6110d54..57da5888 100644 --- a/src/TemplateProcessor.ts +++ b/src/TemplateProcessor.ts @@ -228,6 +228,7 @@ export default class TemplateProcessor { public static NOOP = Symbol('NOOP'); private isExecutingPlan: boolean = false; + private providedContext: any; //the context provided by the owner of the template before we merge it into internal context /** * Loads a template and initializes a new template processor instance. @@ -410,6 +411,7 @@ export default class TemplateProcessor { } constructor(template={}, context = {}, options={}) { + this.providedContext = context; this.timerManager = new TimerManager(this); //prevent leaks from $setTimeout and $setInterval this.generatorManager = new GeneratorManager(this); this.uniqueId = crypto.randomUUID(); @@ -472,6 +474,18 @@ export default class TemplateProcessor { } + public async reInitializeContext(userProvidedContext:object){ + this.output = JSON.parse(JSON.stringify(this.input)); //initial output is input template + for(const key in this.providedContext){ //at this point, providedContext is the OLD previously provided context + delete this.context[key]; //remove the previous user-provided context to clear out 'old' state + } + Object.assign(this.context, userProvidedContext); //spread the *new* context onto the existing context object + this.providedContext = userProvidedContext; //update providedContext + const plan:ExecutionPlan = this.planner.getInitializationPlan("/"); //will get cached initialization plan + this.executionQueue.push(plan); + await this.drainExecutionQueue(false); + } + /** * Template processor initialize can be called from 2 major use cases * 1. initialize a new importedSubtemplate processor importedSubtemplate diff --git a/src/test/TemplateProcessor.test.js b/src/test/TemplateProcessor.test.js index 2e0c0f5e..3ac0517d 100644 --- a/src/test/TemplateProcessor.test.js +++ b/src/test/TemplateProcessor.test.js @@ -3442,7 +3442,7 @@ test("forked homeworlds", async () => { // Ensure the array is exactly the same length as the expected array expect(homeworlds).toHaveLength(expectedHomeworlds.length); expect(savedForkIds.size).toEqual(6); //5 names + 1 initialization of null name - },5000); + },10000); test("performance test with 100 data injections", async () => { @@ -3699,7 +3699,7 @@ test("repetitive snapshots stopped in random execution time", async () => { expect(savedState.plans.length).toBeLessThanOrEqual(5); } -}, 60000); +}, 120000); test("output-only snapshot example", async () => { const __filename = fileURLToPath(import.meta.url); @@ -5780,6 +5780,40 @@ test("change with defaultVal", async () => { } }); +test("reInitialize", async () => { + let template = { + a: 42, + b: "${a}", + c: "${$contextMsg & $string(b)}" + }; + let tp = new TemplateProcessor(template, {contextMsg: "the answer is: ", random: "foo"}); + try { + const initStart = process.hrtime.bigint(); + await tp.initialize(); + const initEnd = process.hrtime.bigint(); + const initElapsedNs = initEnd - initStart; + console.log(`initialize took ${initElapsedNs} nanoseconds (${Number(initElapsedNs) / 1_000_000} ms)`); + expect(tp.output.c).toBe("the answer is: 42"); + let totalNs = 0n; + const iterations = 100; + for (let i = 0; i < iterations; i++) { + const start = process.hrtime.bigint(); + await tp.reInitializeContext( {contextMsg: "the answer is STILL: ", nonRandom: "bar"}); + const end = process.hrtime.bigint(); + totalNs += (end - start); + } + const avgNs = totalNs / BigInt(iterations); + console.log(`reInitializeContext avg over ${iterations} iterations: ${avgNs} nanoseconds (${Number(avgNs) / 1_000_000} ms)`); + expect(tp.output).toStrictEqual({ + "a": 42, + "b": 42, + "c": "the answer is STILL: 42" + }); + }finally{ + await tp.close(); + } +}); +