Skip to content

Commit

Permalink
Lifecycle hooks (#86)
Browse files Browse the repository at this point in the history
* increase delayMs in generate.json for better demos

* initial commit

* bump version
  • Loading branch information
geoffhendrey authored Oct 8, 2024
1 parent cd841bd commit 6225553
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 5 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1795,7 +1795,7 @@ with 10 ms temporal separation.
```json
> .init -f example/generate.json
{
"delayMs": 10,
"delayMs": 250,
"generated":"${[1..10]~>$generate(delayMs)}"
}
```
Expand Down
2 changes: 1 addition & 1 deletion example/generate.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"delayMs": 10,
"delayMs": 250,
"generated":"${[1..10]~>$generate(delayMs)}"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
67 changes: 67 additions & 0 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
@@ -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<void>` indicating the asynchronous operation is complete.
*/
export type LifecycleCallback = (state: LifecycleState, templateProcessor: TemplateProcessor) => Promise<void>;

/**
* 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;
}


72 changes: 72 additions & 0 deletions src/LifecycleManager.ts
Original file line number Diff line number Diff line change
@@ -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<LifecycleState, Set<LifecycleCallback>>;
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();
}
}
15 changes: 13 additions & 2 deletions src/TemplateProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string,() => Promise<void>|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<void> = async () =>{};
public readonly lifecycleManager:LifecycleOwner = new LifecycleManager(this);

public executionStatus: ExecutionStatus;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -492,21 +498,26 @@ 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;
}
}

async close():Promise<void>{
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) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

81 changes: 81 additions & 0 deletions src/test/TemplateProcessor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
});

0 comments on commit 6225553

Please sign in to comment.