diff --git a/README.md b/README.md index 6488b46c..25e06cf2 100644 --- a/README.md +++ b/README.md @@ -1795,7 +1795,7 @@ with 10 ms temporal separation. ```json > .init -f example/generate.json { - "delayMs": 10, + "delayMs": 250, "generated":"${[1..10]~>$generate(delayMs)}" } ``` diff --git a/example/generate.json b/example/generate.json index 79fb4252..0862c6d0 100644 --- a/example/generate.json +++ b/example/generate.json @@ -1,4 +1,4 @@ { - "delayMs": 10, + "delayMs": 250, "generated":"${[1..10]~>$generate(delayMs)}" } \ No newline at end of file diff --git a/package.json b/package.json index fd809a0f..9fbf43f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stated-js", - "version": "0.1.39", + "version": "0.1.40", "license": "Apache-2.0", "description": "JSONata embedded in JSON", "main": "./dist/src/index.js", diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts new file mode 100644 index 00000000..51b22ab0 --- /dev/null +++ b/src/Lifecycle.ts @@ -0,0 +1,67 @@ +import TemplateProcessor from "./TemplateProcessor.js"; + +/** + * Enum representing the various states of the lifecycle. + * This is used to track different phases during the operation of the system. + */ +export enum LifecycleState { + /** + * The state representing the start of the initialization process. + */ + StartInitialize = 'StartInitialize', + + /** + * The state before temporary variables are removed from the system. + */ + PreTmpVarRemoval = 'PreTmpVarRemoval', + + /** + * The state when the system has been fully initialized and is ready for use. + */ + Initialized = 'Initialized', + + /** + * The state when the process to close the system begins. + */ + StartClose = 'StartClose', + + /** + * The state when the system has fully closed and is no longer operational. + */ + Closed = 'Closed', +} + +/** + * Callback type definition for functions that handle lifecycle transitions. + * + * This type represents an asynchronous function that will be called whenever the + * lifecycle state changes. It receives the new lifecycle state and a `TemplateProcessor` + * instance for processing. + * + * @param state - The new lifecycle state that the system has transitioned to. + * @param templateProcessor - The `TemplateProcessor` instance to be used for handling the state transition. + * + * @returns A `Promise` indicating the asynchronous operation is complete. + */ +export type LifecycleCallback = (state: LifecycleState, templateProcessor: TemplateProcessor) => Promise; + +/** + * Interface for managing lifecycle callbacks. + */ +export interface LifecycleOwner { + /** + * Registers a lifecycle callback for a specific lifecycle state. + * @param state The lifecycle state to register the callback for. + * @param cbFn The callback function to execute when the lifecycle state is triggered. + */ + setLifecycleCallback(state: LifecycleState, cbFn: LifecycleCallback): void; + + /** + * Removes a specific lifecycle callback or all callbacks for a lifecycle state. + * @param state The lifecycle state to remove the callback from. + * @param cbFn The specific callback function to remove. If not provided, all callbacks for the state will be removed. + */ + removeLifecycleCallback(state: LifecycleState, cbFn?: LifecycleCallback): void; +} + + diff --git a/src/LifecycleManager.ts b/src/LifecycleManager.ts new file mode 100644 index 00000000..fbec02db --- /dev/null +++ b/src/LifecycleManager.ts @@ -0,0 +1,72 @@ +import { LifecycleState, LifecycleCallback, LifecycleOwner } from './Lifecycle.js'; +import TemplateProcessor from "./TemplateProcessor.js"; + + +/** + * Class for managing lifecycle callbacks. + */ +export class LifecycleManager implements LifecycleOwner { + private lifecycleCallbacks: Map>; + private templateProcessor: TemplateProcessor; + + constructor(templateProcessor:TemplateProcessor) { + this.lifecycleCallbacks = new Map(); + this.templateProcessor = templateProcessor; + } + + /** + * Registers a lifecycle callback for a specific lifecycle state. + * @param state The lifecycle state to register the callback for. + * @param cbFn The callback function to execute when the lifecycle state is triggered. + */ + setLifecycleCallback(state: LifecycleState, cbFn: LifecycleCallback) { + this.templateProcessor.logger.debug(`Lifecycle callback set on state: ${state}`); + let callbacks = this.lifecycleCallbacks.get(state); + if (!callbacks) { + callbacks = new Set(); + this.lifecycleCallbacks.set(state, callbacks); + } + callbacks.add(cbFn); + } + + /** + * Removes a specific lifecycle callback or all callbacks for a lifecycle state. + * @param state The lifecycle state to remove the callback from. + * @param cbFn The specific callback function to remove. If not provided, all callbacks for the state will be removed. + */ + removeLifecycleCallback(state: LifecycleState, cbFn?: LifecycleCallback) { + this.templateProcessor.logger.debug(`Lifecycle callback removed from state: ${state}`); + if (cbFn) { + const callbacks = this.lifecycleCallbacks.get(state); + if (callbacks) { + callbacks.delete(cbFn); + } + } else { + this.lifecycleCallbacks.delete(state); + } + } + + /** + * Calls all lifecycle callbacks registered for a specific lifecycle state. + * @param state The lifecycle state to trigger callbacks for. + */ + async runCallbacks(state: LifecycleState) { + this.templateProcessor.logger.debug(`Calling lifecycle callbacks for state: ${state}`); + const callbacks = this.lifecycleCallbacks.get(state); + if (callbacks) { + const promises = Array.from(callbacks).map(cbFn => + Promise.resolve().then(() => cbFn(state, this.templateProcessor)) + ); + + try { + await Promise.all(promises); + } catch (error: any) { + this.templateProcessor.logger.error(`Error in lifecycle callback at state ${state}: ${error.message}`); + } + } + } + + clear(){ + this.lifecycleCallbacks.clear(); + } +} diff --git a/src/TemplateProcessor.ts b/src/TemplateProcessor.ts index a5e7cbec..8ec1181a 100644 --- a/src/TemplateProcessor.ts +++ b/src/TemplateProcessor.ts @@ -32,6 +32,8 @@ import {saferFetch} from "./utils/FetchWrapper.js"; import {env} from "./utils/env.js" import * as jsonata from "jsonata"; import {GeneratorManager} from "./utils/GeneratorManager.js"; +import {LifecycleOwner, LifecycleState} from "./Lifecycle.js"; +import {LifecycleManager} from "./LifecycleManager.js"; declare const BUILD_TARGET: string | undefined; @@ -304,15 +306,18 @@ export default class TemplateProcessor { private generatorManager:GeneratorManager; - /** Allows caller to set a callback to propagate initialization into their framework */ + /** Allows caller to set a callback to propagate initialization into their framework + * @deprecated use lifecycleManager instead + * */ public readonly onInitialize: Map Promise|void>; /** * Allows a caller to receive a callback after the template is evaluated, but before any temporary variables are * removed. This function is slated to be replaced with a map of functions like onInitialize - * @deprecated + * @deprecated use lifecycleManager instead */ public postInitialize: ()=> Promise = async () =>{}; + public readonly lifecycleManager:LifecycleOwner = new LifecycleManager(this); public executionStatus: ExecutionStatus; @@ -452,6 +457,7 @@ export default class TemplateProcessor { this.logger.debug(`Running onInitialize plugin '${name}'...`); await task(); } + await (this.lifecycleManager as LifecycleManager).runCallbacks(LifecycleState.StartInitialize); try { if (jsonPtr === "/") { this.errorReport = {}; //clear the error report when we initialize a root importedSubtemplate @@ -492,9 +498,11 @@ export default class TemplateProcessor { await this.executionStatus.restore(this); } await this.postInitialize(); + await (this.lifecycleManager as LifecycleManager).runCallbacks(LifecycleState.PreTmpVarRemoval); this.removeTemporaryVariables(this.tempVars, jsonPtr); this.logger.verbose("initialization complete..."); this.logOutput(this.output); + await (this.lifecycleManager as LifecycleManager).runCallbacks(LifecycleState.Initialized); }finally { this.isInitializing = false; } @@ -502,11 +510,14 @@ export default class TemplateProcessor { async close():Promise{ this.isClosed = true; + await (this.lifecycleManager as LifecycleManager).runCallbacks(LifecycleState.StartClose); this.executionQueue.length = 0; //nuke execution queue await this.drainExecutionQueue(); this.timerManager.clear(); this.changeCallbacks.clear(); this.executionStatus.clear(); + await (this.lifecycleManager as LifecycleManager).runCallbacks(LifecycleState.Closed); + (this.lifecycleManager as LifecycleManager).clear(); } private async evaluateInitialPlan(jsonPtr:JsonPointerString) { diff --git a/src/index.ts b/src/index.ts index 79009c01..2eef49c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,5 @@ export * as MetaInfoProducer from './MetaInfoProducer.js'; export {default as JsonPointer} from './JsonPointer.js' export {stringifyTemplateJSON} from './utils/stringify.js' export {CliCoreBase} from './CliCoreBase.js'; +export * from './Lifecycle.js'; diff --git a/src/test/TemplateProcessor.test.js b/src/test/TemplateProcessor.test.js index 463e64e5..bff75f18 100644 --- a/src/test/TemplateProcessor.test.js +++ b/src/test/TemplateProcessor.test.js @@ -24,6 +24,7 @@ import jsonata from "jsonata"; import { default as jp } from "../../dist/src/JsonPointer.js"; import StatedREPL from "../../dist/src/StatedREPL.js"; import { jest, expect, describe, beforeEach, afterEach, test} from '@jest/globals'; +import {LifecycleState} from "../../dist/src/Lifecycle.js"; if (typeof Bun !== 'undefined') { // Dynamically import Jest's globals if in Bun.js environment @@ -3139,3 +3140,83 @@ test("test generate", async () => { await tp.close(); } }); + + +test("test lifecycle manager", async () => { + const o = { + "a": "hello", + "tmp": "!${'remove me'}" + }; + + const callCount = 10; + + + + const tp = new TemplateProcessor(o); + + let resolve0; + const promise0 = new Promise((resolve) => { + resolve0 = resolve; + }) + tp.lifecycleManager.setLifecycleCallback(LifecycleState.StartInitialize, async (state, tp)=>{ + expect(state).toEqual(LifecycleState.StartInitialize); + expect(tp.output).toEqual({ + "a": "hello", + "tmp": "!${'remove me'}" + }); + resolve0(); + }); + let resolve1; + const promise1 = new Promise((resolve) => { + resolve1 = resolve; + }) + tp.lifecycleManager.setLifecycleCallback(LifecycleState.PreTmpVarRemoval, async (state)=>{ + expect(state).toEqual(LifecycleState.PreTmpVarRemoval); + expect(tp.output).toEqual({ + "a": "hello", + "tmp": "remove me" + }); + resolve1(); + }); + let resolve2; + const promise2 = new Promise((resolve) => { + resolve2 = resolve; + }) + tp.lifecycleManager.setLifecycleCallback(LifecycleState.Initialized, async (state)=>{ + expect(state).toEqual(LifecycleState.Initialized); + expect(tp.output).toEqual({ + "a": "hello", + }); + resolve2(); + }); + let resolve3; + const promise3 = new Promise((resolve) => { + resolve3 = resolve; + }) + tp.lifecycleManager.setLifecycleCallback(LifecycleState.StartClose, async (state)=>{ + expect(state).toEqual(LifecycleState.StartClose); + expect(tp.output).toEqual({ + "a": "hello", + }); + resolve3(); + }); + let resolve4; + const promise4 = new Promise((resolve) => { + resolve4 = resolve; + }) + tp.lifecycleManager.setLifecycleCallback(LifecycleState.Closed, async (state)=>{ + expect(state).toEqual(LifecycleState.Closed); + expect(tp.output).toEqual({ + "a": "hello", + }); + resolve4(); + }); + try { + await tp.initialize(); + await Promise.all([promise0, promise1]); + tp.close(); + await Promise.all([promise2, promise3, promise4]); + } finally { + await tp.close(); + } +}); \ No newline at end of file