Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lifecycle hooks #86

Merged
merged 3 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
});
Loading