Skip to content
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
78 changes: 78 additions & 0 deletions modules/runtime/src/@types/scope.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
export interface ILayeredMap<T extends object> {
rootID: string;
addFrame(frameParentID: string): string;
removeFrame(frameID: string): void;
updateFrameKeyMap(frameID: string, keyMap: T): void;
projectFlatMap(frameID: string): T;
}

export interface IContextStack<T extends object> {
/** Unique ID for this stack (second frame in its chain) */
readonly contextID: string;

/** Read/write the global frame’s entire key-map */
contextGlobalKeyMap: T;

/** Read/write the current (top) frame’s key-map */
contextLocalKeyMap: T;

/** Enter a new local frame */
pushFrame(): void;

/** Exit the current local frame */
popFrame(): void;
}

// factory fnc
export interface IContextManager<T extends object> {
/** Create a new independent context stack */
createContextStack(): IContextStack<T>;

/** Fetch an existing stack by its ID */
getContextStack(contextStackID: string): IContextStack<T>;

/** Tear down an existing stack by its ID */
removeContextStack(contextStackID: string): void;

/** Read-only view of the global frame */
readonly contextGlobalKeyMap: T;

/**
* Overwrite the global frame’s key-map.
* @param commit if true, also update the “reset” baseline
*/
updateContextGlobalKeyMap(keyMap: T, commit?: boolean): void;

/** Reset everything back to the original global map */
reset(): void;
}

export interface IThreadContext<T extends object> {
/** The unique ID of this thread’s context stack */
readonly id: string;

/** Local frame CRUD */
getLocal<K extends keyof T>(key: K): T[K] | undefined;
setLocal<K extends keyof T>(key: K, value: T[K]): void;
deleteLocal(key: keyof T): void;

/** Scope push/pop */
pushScope(): void;
popScope(): void;

/** Global frame CRUD */
getGlobal<K extends keyof T>(key: K): T[K] | undefined;
setGlobal<K extends keyof T>(key: K, value: T[K]): void;
deleteGlobal(key: keyof T): void;
}

export interface IThreadManager<T extends object> {
/** Spawn a new thread (with its own local-stack) */
createThread(): IThreadContext<T>;

/** Look up an existing thread by ID */
getThread(threadID: string): IThreadContext<T>;

/** Tear down a thread by ID */
deleteThread(threadID: string): void;
}
246 changes: 246 additions & 0 deletions modules/runtime/src/execution/scope/context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { ContextManager } from './context';
import type { IContextStack } from '../../@types/scope';

type TContextDummy = { foo: number; bar: string };

// -------------------------------------------------------------------------------------------------

describe('Context module', () => {
const contextManager = new ContextManager<TContextDummy>({
foo: 4,
bar: 'red',
});

let contextStack0: IContextStack<TContextDummy>;
let contextStack1: IContextStack<TContextDummy>;

describe('Context stack', () => {
beforeAll(() => {
contextStack0 = contextManager.createContextStack();
});

it('returns global context', () => {
expect(
Object.entries(contextStack0.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-4');
});

it('updates local key-map', () => {
contextStack0.contextLocalKeyMap = {
...contextStack0.contextLocalKeyMap,
foo: 8,
};

expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-8');

// Global must stay unchanged
expect(
Object.entries(contextStack0.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-4');
});

it("pushes context frame, and updates new frame's key-map", () => {
contextStack0.pushFrame();
contextStack0.contextLocalKeyMap = {
...contextStack0.contextLocalKeyMap,
bar: 'blue',
};

expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-blue, foo-8');

expect(
Object.entries(contextStack0.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-4');
});

it('pops (previously pushed) context frames', () => {
contextStack0.popFrame();
expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-8');
});

it('throws error on popping root context frame', () => {
expect(() => {
contextStack0.popFrame();
}).toThrowError('InvalidOperationError: No context frame remaining to pop');
});
});

describe('Context Manager', () => {
beforeAll(() => {
contextStack1 = contextManager.createContextStack();
});

it('creates multiple context stacks having independent behavior', () => {
contextStack0.contextLocalKeyMap = {
...contextStack0.contextLocalKeyMap,
foo: 6,
};
contextStack1.contextLocalKeyMap = {
...contextStack0.contextLocalKeyMap,
foo: 7,
};

expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-6');
expect(
Object.entries(contextStack1.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-7');

contextStack0.pushFrame();
contextStack0.contextLocalKeyMap = {
...contextStack0.contextLocalKeyMap,
bar: 'yellow',
};
contextStack1.pushFrame();
contextStack1.contextLocalKeyMap = {
...contextStack1.contextLocalKeyMap,
bar: 'green',
};

expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-yellow, foo-6');
expect(
Object.entries(contextStack1.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-green, foo-7');

contextStack0.popFrame();
expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-6');
expect(
Object.entries(contextStack1.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-green, foo-7');

contextStack1.popFrame();
expect(
Object.entries(contextStack0.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-6');
expect(
Object.entries(contextStack1.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-7');
});

it('updates global context key-map from one context stack that reflects on another', () => {
contextStack0.contextGlobalKeyMap = {
foo: 5,
bar: 'purple',
};
expect(
Object.entries(contextStack1.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-purple, foo-5');

contextStack1.contextGlobalKeyMap = {
foo: 3,
bar: 'lime',
};
expect(
Object.entries(contextStack0.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-lime, foo-3');
});

it('fetches context stack using existing context stack ID', () => {
const cs = contextManager.getContextStack(contextStack0.contextID);
expect(
Object.entries(cs.contextLocalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-6');
});

it('throws error on fetching context stack using non-existing context stack ID', () => {
expect(() => {
contextManager.getContextStack('foobar');
}).toThrowError(`InvalidAccessError: Context stack with ID "foobar" doesn't exist`);
});

it('removes context stack using existing context stack ID', () => {
expect(() => {
contextManager.removeContextStack(contextStack1.contextID);
}).not.toThrowError();
});

it('throws error on removing context stack using non-existing context stack ID', () => {
expect(() => {
contextManager.removeContextStack(contextStack1.contextID);
}).toThrowError(
`InvalidAccessError: Context stack with ID "${contextStack1.contextID}" doesn't exist`,
);
});

it('updates global context key-map with commit', () => {
contextManager.reset();
expect(
Object.entries(contextManager.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-red, foo-4');

contextManager.updateContextGlobalKeyMap({ foo: 16, bar: 'coral' }, true);
contextManager.reset();
expect(
Object.entries(contextManager.contextGlobalKeyMap)
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}-${v}`)
.join(', '),
).toBe('bar-coral, foo-16');
});
});
});
Loading