Skip to content

Instantly share code, notes, and snippets.

@Jamesernator
Last active July 14, 2023 07:11
Show Gist options
  • Save Jamesernator/01b0ad45016d8e5658c76e6811449181 to your computer and use it in GitHub Desktop.
Save Jamesernator/01b0ad45016d8e5658c76e6811449181 to your computer and use it in GitHub Desktop.
AsyncContext with termination
import InternalAsyncContextState from "./InternalAsyncContextState.js";
import InternalVariable from "./InternalVariable.js";
namespace AsyncContext {
const state = new InternalAsyncContextState();
export class Snapshot {
readonly #internalSnapshot = state.currentSnapshot.clone();
discard(): void {
this.#internalSnapshot.discard();
}
run<Args extends ReadonlyArray<any>, Return>(
fn: (...args: Args) => Return,
...args: Args
): Return {
if (!this.#internalSnapshot.isAlive) {
throw new TypeError(`this snapshot has already been discarded`);
}
return state.run(this.#internalSnapshot, fn, ...args);
}
}
export type VariableInit<T> = {
name?: string | undefined,
defaultValue?: T | undefined,
};
export class Variable<T> {
readonly #internalVariable: InternalVariable<T>;
readonly #name: string;
constructor({ name = "", defaultValue }: VariableInit<T> = {}) {
this.#name = name;
this.#internalVariable = new InternalVariable(state, { name, defaultValue });
}
get name(): string {
return this.#name;
}
get(): T | undefined {
return this.#internalVariable.get();
}
run<Args extends ReadonlyArray<any>, Return>(
value: T,
fn: (...args: Args) => Return,
...args: Args
): Return {
return this.#internalVariable.run(value, fn, ...args);
}
runWithTermination<Args extends ReadonlyArray<any>, Return>(
value: T,
terminationHandler: () => void,
fn: (...args: Args) => Return,
...args: Args
): Return {
return this.#internalVariable.runWithTermination(
value,
terminationHandler,
fn,
...args,
);
}
}
}
export default AsyncContext;
import { createHook, executionAsyncId } from "node:async_hooks";
import InternalSnapshot from "./InternalSnapshot.js";
export default class InternalAsyncContextState {
readonly #initialSnapshot = new InternalSnapshot(this, new Map());
readonly #resourceMapping = new Map<number, InternalSnapshot>();
readonly #hook = createHook({
destroy: (asyncId) => {
const snapshot = this.#resourceMapping.get(asyncId);
snapshot?.discard();
},
init: (asyncId, type, triggerAsyncId, resource) => {
const previousSnapshot
= this.#resourceMapping.get(triggerAsyncId) ?? this.#initialSnapshot;
this.#resourceMapping.set(asyncId, previousSnapshot.clone());
},
});
constructor() {
this.#hook.enable();
}
disable(): void {
this.#hook.disable();
}
get currentSnapshot(): InternalSnapshot {
return this.#resourceMapping.get(executionAsyncId()) ?? this.#initialSnapshot;
}
run<Args extends ReadonlyArray<any>, Return>(
snapshot: InternalSnapshot,
fn: (...args: Args) => Return,
...args: Args
): Return {
const asyncId = executionAsyncId();
const previousSnapshot
= this.#resourceMapping.get(asyncId) ?? this.#initialSnapshot.clone();
this.#resourceMapping.set(asyncId, snapshot);
try {
return fn(...args);
} finally {
this.#resourceMapping.set(asyncId, previousSnapshot);
}
}
}
import type InternalAsyncContextState from "./InternalAsyncContextState.js";
import type InternalVariable from "./InternalVariable.js";
type VariableStateInit = {
value: any,
terminationHandler: () => void,
};
class InternalVariableState {
#value: {
value: any,
terminationHandler: () => void,
} | null;
#refCount: number = 0;
constructor({ value, terminationHandler }: VariableStateInit) {
this.#value = { value, terminationHandler };
}
[Symbol.for("nodejs.util.inspect.custom")](
depth: number,
options: import("node:util").InspectOptions,
inspect: typeof import("node:util").inspect,
) {
return inspect(
{
value: this.#value?.value,
terminationHandler: this.#value?.terminationHandler,
refCount: this.#refCount,
},
{ depth, ...options },
);
}
get value(): any {
if (this.#value === null) {
throw new RangeError(`value has been discarded`);
}
return this.#value?.value;
}
ref(): void {
this.#refCount += 1;
}
deref(): void {
this.#refCount -= 1;
if (this.#refCount === 0) {
if (this.#value === null) {
throw new RangeError(`this should be impossible`);
}
const { terminationHandler } = this.#value;
this.#value = null;
terminationHandler();
}
}
}
const finalizer = new FinalizationRegistry<Map<InternalVariable<any>, InternalVariableState>>(
(mapping) => {
for (const variableState of mapping.values()) {
variableState.deref();
}
},
);
export default class InternalSnapshot {
readonly #state: InternalAsyncContextState;
#maybeMapping: Map<InternalVariable<any>, InternalVariableState> | null;
constructor(
state: InternalAsyncContextState,
mapping: Map<InternalVariable<any>, InternalVariableState>,
) {
this.#state = state;
this.#maybeMapping = mapping;
for (const variableState of mapping.values()) {
variableState.ref();
}
finalizer.register(this, mapping, this);
}
[Symbol.for("nodejs.util.inspect.custom")](
depth: number,
options: import("node:util").InspectOptions,
inspect: typeof import("node:util").inspect,
) {
return `InternalSnapshot ${inspect(
{
"#state": this.#state,
"#mapping": this.#maybeMapping,
},
{ depth, ...options },
)}`;
}
get #mapping(): Map<InternalVariable<any>, InternalVariableState> {
if (this.#maybeMapping === null) {
throw new Error(`can't use InternalSnapshot after it has been discarded`);
}
return this.#maybeMapping;
}
get isAlive(): boolean {
return this.#maybeMapping !== null;
}
discard(): void {
finalizer.unregister(this);
if (this.#maybeMapping) {
for (const variableState of this.#mapping.values()) {
variableState.deref();
}
}
this.#maybeMapping = null;
}
get(variable: InternalVariable<any>): { value: any } | null {
const state = this.#mapping.get(variable);
if (state) {
return { value: state.value };
}
return null;
}
clone(): InternalSnapshot {
return new InternalSnapshot(this.#state, new Map([...this.#mapping]));
}
cloneWith(
variable: InternalVariable<any>,
value: any,
terminationHandler: () => void,
): InternalSnapshot {
const variableState = new InternalVariableState({ value, terminationHandler });
return new InternalSnapshot(
this.#state,
new Map([...this.#mapping, [variable, variableState]]),
);
}
run<Args extends ReadonlyArray<any>, Return>(
fn: (...args: Args) => Return,
...args: Args
): Return {
return this.#state.run(this, fn, ...args);
}
}
import type AsyncContextState from "./InternalAsyncContextState.js";
export type AsyncVariableOptions<T> = {
name?: string | undefined,
defaultValue?: T | undefined,
};
export default class InternalVariable<T> {
readonly #state: AsyncContextState;
readonly #name: string;
readonly #defaultValue: T | undefined;
constructor(
state: AsyncContextState,
{ name = "", defaultValue }: AsyncVariableOptions<T> = {},
) {
this.#state = state;
this.#name = name;
this.#defaultValue = defaultValue;
}
get name(): string {
return this.#name;
}
get(): T | undefined {
const v = this.#state.currentSnapshot.get(this);
if (v === null) {
return this.#defaultValue;
}
return v.value;
}
#run<Args extends ReadonlyArray<any>, Return>(
value: T,
terminationCb: () => void,
fn: (...args: Args) => Return,
...args: Args
): Return {
const modifiedSnapshot = this.#state.currentSnapshot.cloneWith(this, value, terminationCb);
try {
return this.#state.run(modifiedSnapshot, fn, ...args);
} finally {
modifiedSnapshot.discard();
}
}
run<Args extends ReadonlyArray<any>, Return>(
value: T,
fn: (...args: Args) => Return,
...args: Args
): Return {
return this.#run(value, () => {}, fn, ...args);
}
runWithTermination<Args extends ReadonlyArray<any>, Return>(
value: T,
terminationCb: () => void,
fn: (...args: Args) => Return,
...args: Args
): Return {
return this.#run(value, terminationCb, fn, ...args);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment