Use the simpler abortSignal + deadline + value structure instead
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added a Context class for the backend, that handles aborting, timeouts, api resolution etc
|
||||
Added a `Context` type for the backend, that can propagate an abort signal, a deadline, and contextual values through the call stack. The main entrypoint is the `Contexts` utility class that provides a root context creator and commonly used decorators.
|
||||
|
||||
@@ -12,7 +12,6 @@ import { AzureIntegration } from '@backstage/integration';
|
||||
import { BitbucketIntegration } from '@backstage/integration';
|
||||
import { Config } from '@backstage/config';
|
||||
import cors from 'cors';
|
||||
import { DateTime } from 'luxon';
|
||||
import Docker from 'dockerode';
|
||||
import { Duration } from 'luxon';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
@@ -149,25 +148,27 @@ export interface ContainerRunner {
|
||||
|
||||
// @public
|
||||
export interface Context {
|
||||
readonly abortPromise: Promise<void>;
|
||||
readonly abortSignal: AbortSignal;
|
||||
readonly deadline: DateTime | undefined;
|
||||
readonly deadline: Date | undefined;
|
||||
use(...decorators: ContextDecorator[]): Context;
|
||||
value<T = unknown>(key: string | symbol): T | undefined;
|
||||
with(...decorators: ContextDecorator[]): Context;
|
||||
withAbort(): {
|
||||
ctx: Context;
|
||||
abort: () => void;
|
||||
};
|
||||
withTimeout(timeout: Duration): Context;
|
||||
withValue<T = unknown>(
|
||||
key: string | symbol,
|
||||
value: T | ((previous: T | undefined) => T),
|
||||
): Context;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ContextDecorator = (ctx: Context) => Context;
|
||||
|
||||
// @public
|
||||
export class Contexts {
|
||||
static root(): Context;
|
||||
static setAbort(signal: AbortSignal_2): ContextDecorator;
|
||||
static setTimeoutDuration(timeout: Duration): ContextDecorator;
|
||||
static setTimeoutMillis(timeout: number): ContextDecorator;
|
||||
static setValue(
|
||||
key: string | symbol,
|
||||
value: unknown | ((previous: unknown | undefined) => unknown),
|
||||
): ContextDecorator;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export const createDatabase: typeof createDatabaseClient;
|
||||
|
||||
@@ -479,25 +480,6 @@ export function resolvePackagePath(name: string, ...paths: string[]): string;
|
||||
// @public
|
||||
export function resolveSafeChildPath(base: string, path: string): string;
|
||||
|
||||
// @public
|
||||
export class RootContext implements Context {
|
||||
get abortPromise(): Promise<void>;
|
||||
get abortSignal(): AbortSignal_2;
|
||||
static create(): Context;
|
||||
get deadline(): DateTime | undefined;
|
||||
value<T = unknown>(key: string | symbol): T | undefined;
|
||||
with(...items: ContextDecorator[]): Context;
|
||||
withAbort(): {
|
||||
ctx: Context;
|
||||
abort: () => void;
|
||||
};
|
||||
withTimeout(timeout: Duration): Context;
|
||||
withValue<T = unknown>(
|
||||
key: string | symbol,
|
||||
value: T | ((previous: T | undefined) => T),
|
||||
): Context;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type RunContainerOptions = {
|
||||
imageName: string;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AbortController } from 'node-abort-controller';
|
||||
import { AbortContext } from './AbortContext';
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
|
||||
describe('AbortContext', () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('forTimeoutMillis', () => {
|
||||
it('can abort on a timeout', async () => {
|
||||
jest.useFakeTimers();
|
||||
const timeout = 200;
|
||||
const deadline = Date.now() + timeout;
|
||||
|
||||
const root = new RootContext();
|
||||
|
||||
const child = AbortContext.forTimeoutMillis(root, timeout);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
expect(Math.abs(+child.deadline! - deadline)).toBeLessThan(50);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
jest.advanceTimersByTime(timeout + 1);
|
||||
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('results in minimum deadline when parent triggers sooner', async () => {
|
||||
jest.useFakeTimers();
|
||||
const parentTimeout = 200;
|
||||
const childTimeout = 300;
|
||||
const parentDeadline = Date.now() + parentTimeout;
|
||||
const childDeadline = parentDeadline; // clamped
|
||||
|
||||
const root = new RootContext();
|
||||
|
||||
const parent = AbortContext.forTimeoutMillis(root, parentTimeout);
|
||||
const parentListener = jest.fn();
|
||||
parent.abortSignal.addEventListener('abort', parentListener);
|
||||
|
||||
const child = AbortContext.forTimeoutMillis(parent, childTimeout);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
|
||||
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
jest.advanceTimersByTime(parentTimeout + 1);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(true);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(parentListener).toBeCalledTimes(1);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('results in minimum deadline when child triggers sooner', async () => {
|
||||
jest.useFakeTimers();
|
||||
const parentTimeout = 300;
|
||||
const childTimeout = 200;
|
||||
const parentDeadline = Date.now() + parentTimeout;
|
||||
const childDeadline = Date.now() + childTimeout;
|
||||
|
||||
const root = new RootContext();
|
||||
|
||||
const parent = AbortContext.forTimeoutMillis(root, parentTimeout);
|
||||
const parentListener = jest.fn();
|
||||
parent.abortSignal.addEventListener('abort', parentListener);
|
||||
|
||||
const child = AbortContext.forTimeoutMillis(parent, childTimeout);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50);
|
||||
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
jest.advanceTimersByTime(childTimeout + 1);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
|
||||
jest.advanceTimersByTime(parentTimeout - childTimeout + 1);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(true);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(parentListener).toBeCalledTimes(1);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('child carries over parent signal state if parent was already aborted and had no deadline', async () => {
|
||||
jest.useFakeTimers();
|
||||
const childTimeout = 200;
|
||||
const childDeadline = Date.now() + childTimeout;
|
||||
|
||||
const root = new RootContext();
|
||||
|
||||
const parentController = new AbortController();
|
||||
const parent = AbortContext.forSignal(root, parentController.signal);
|
||||
|
||||
parentController.abort();
|
||||
|
||||
const child = AbortContext.forTimeoutMillis(parent, childTimeout);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50);
|
||||
|
||||
jest.advanceTimersByTime(childTimeout + 1);
|
||||
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(0); // still
|
||||
});
|
||||
|
||||
it('child carries over parent signal state if parent was already aborted and had a deadline', async () => {
|
||||
jest.useFakeTimers();
|
||||
const first = new RootContext();
|
||||
|
||||
const secondController = new AbortController();
|
||||
const second = AbortContext.forSignal(first, secondController.signal);
|
||||
secondController.abort();
|
||||
|
||||
const third = AbortContext.forTimeoutMillis(second, 200);
|
||||
const fourth = AbortContext.forTimeoutMillis(third, 300);
|
||||
|
||||
expect(third.abortSignal.aborted).toBe(true);
|
||||
expect(fourth.abortSignal.aborted).toBe(true);
|
||||
expect(Math.abs(+fourth.deadline! - Date.now() - 200)).toBeLessThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forSignal', () => {
|
||||
it('signals child when parent is aborted', async () => {
|
||||
const root = new RootContext();
|
||||
|
||||
const parentController = new AbortController();
|
||||
const parent = AbortContext.forSignal(root, parentController.signal);
|
||||
const parentListener = jest.fn();
|
||||
parent.abortSignal.addEventListener('abort', parentListener);
|
||||
|
||||
const childController = new AbortController();
|
||||
const child = AbortContext.forSignal(parent, childController.signal);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
parentController.abort();
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(true);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(parentListener).toBeCalledTimes(1);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not signal parent when child is aborted', async () => {
|
||||
const root = new RootContext();
|
||||
|
||||
const parentController = new AbortController();
|
||||
const parent = AbortContext.forSignal(root, parentController.signal);
|
||||
const parentListener = jest.fn();
|
||||
parent.abortSignal.addEventListener('abort', parentListener);
|
||||
|
||||
const childController = new AbortController();
|
||||
const child = AbortContext.forSignal(parent, childController.signal);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
childController.abort();
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(false);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(parentListener).toBeCalledTimes(0);
|
||||
expect(childListener).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('child carries over parent signal state if parent was already aborted', async () => {
|
||||
const root = new RootContext();
|
||||
|
||||
const parentController = new AbortController();
|
||||
const parent = AbortContext.forSignal(root, parentController.signal);
|
||||
|
||||
parentController.abort();
|
||||
|
||||
const childController = new AbortController();
|
||||
const child = AbortContext.forSignal(parent, childController.signal);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(true);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
|
||||
childController.abort();
|
||||
|
||||
expect(parent.abortSignal.aborted).toBe(true);
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('child carries over given signal state if it was already aborted', async () => {
|
||||
const root = new RootContext();
|
||||
|
||||
const childController = new AbortController();
|
||||
childController.abort();
|
||||
|
||||
const child = AbortContext.forSignal(root, childController.signal);
|
||||
const childListener = jest.fn();
|
||||
child.abortSignal.addEventListener('abort', childListener);
|
||||
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
expect(childListener).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('can decorate', () => {
|
||||
const root = new RootContext();
|
||||
const controller = new AbortController();
|
||||
const parent = AbortContext.forSignal(root, controller.signal);
|
||||
const child = parent.use(
|
||||
Contexts.setValue('a', 2),
|
||||
Contexts.setValue('a', 3),
|
||||
);
|
||||
expect(child.value('a')).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
|
||||
/**
|
||||
* A context that implements various abort related functionality.
|
||||
*/
|
||||
export class AbortContext implements Context {
|
||||
/**
|
||||
* Abort either when the parent aborts, or after the given timeout has
|
||||
* expired.
|
||||
*/
|
||||
static forTimeoutMillis(ctx: Context, timeout: number): Context {
|
||||
const desiredDeadline = new Date(Date.now() + timeout);
|
||||
const actualDeadline =
|
||||
ctx.deadline && ctx.deadline < desiredDeadline
|
||||
? ctx.deadline
|
||||
: desiredDeadline;
|
||||
|
||||
if (ctx.abortSignal.aborted) {
|
||||
if (ctx.deadline && desiredDeadline === actualDeadline) {
|
||||
return ctx;
|
||||
}
|
||||
return new AbortContext(ctx, ctx.abortSignal, actualDeadline);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const abort = () => {
|
||||
ctx.abortSignal.removeEventListener('abort', abort);
|
||||
clearTimeout(timeoutHandle);
|
||||
controller.abort();
|
||||
};
|
||||
|
||||
ctx.abortSignal.addEventListener('abort', abort);
|
||||
|
||||
return new AbortContext(ctx, controller.signal, actualDeadline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort either when the parent aborts, or when the given signal is triggered.
|
||||
*/
|
||||
static forSignal(ctx: Context, signal: AbortSignal): Context {
|
||||
// If the parent context was already aborted, it is fine to reuse as-is
|
||||
if (ctx.abortSignal.aborted) {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const abort = controller.abort.bind(controller);
|
||||
|
||||
// If the incoming signal was already aborted, let's trigger the new one as
|
||||
// well
|
||||
if (signal.aborted) {
|
||||
abort();
|
||||
} else {
|
||||
ctx.abortSignal.addEventListener('abort', abort);
|
||||
signal.addEventListener('abort', abort);
|
||||
}
|
||||
|
||||
return new AbortContext(ctx, controller.signal, ctx.deadline);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly parent: Context,
|
||||
readonly abortSignal: AbortSignal,
|
||||
readonly deadline: Date | undefined,
|
||||
) {}
|
||||
|
||||
value<T = unknown>(key: string | symbol): T | undefined {
|
||||
return this.parent.value(key);
|
||||
}
|
||||
|
||||
use(...items: ContextDecorator[]): Context {
|
||||
return items.reduce((prev, curr) => curr(prev), this as Context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Duration } from 'luxon';
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import { AbortContext } from './AbortContext';
|
||||
import { RootContext } from './RootContext';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
import { ValueContext } from './ValueContext';
|
||||
|
||||
/**
|
||||
* Common context decorators.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Contexts {
|
||||
/**
|
||||
* Creates a root context.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This should normally only be called near the root of an application. The
|
||||
* created context is meant to be passed down into deeper levels, which may or
|
||||
* may not make derived contexts out of it.
|
||||
*/
|
||||
static root(): Context {
|
||||
return new RootContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a derived context, which signals to abort operations either when
|
||||
* any parent context signals, or when the given controller is aborted.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If the parent context was already aborted, then it is returned as-is.
|
||||
*
|
||||
* If the given signal was already aborted, then a new already-aborted context
|
||||
* is returned.
|
||||
*
|
||||
* @param signal - An abort signal that you intend to perhaps trigger at some
|
||||
* later point in time.
|
||||
* @returns A decorator that can be passed to {@link Context.use}
|
||||
*/
|
||||
static setAbort(signal: AbortSignal): ContextDecorator {
|
||||
return ctx => AbortContext.forSignal(ctx, signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a derived context, which signals to abort operations either when
|
||||
* any parent context signals, or when the given amount of time has passed.
|
||||
* This may affect the deadline.
|
||||
*
|
||||
* @param timeout - The duration of time, after which the derived context will
|
||||
* signal to abort.
|
||||
* @returns A decorator that can be passed to {@link Context.use}
|
||||
*/
|
||||
static setTimeoutDuration(timeout: Duration): ContextDecorator {
|
||||
return ctx =>
|
||||
AbortContext.forTimeoutMillis(ctx, timeout.as('milliseconds'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a derived context, which signals to abort operations either when
|
||||
* any parent context signals, or when the given amount of time has passed.
|
||||
* This may affect the deadline.
|
||||
*
|
||||
* @param timeout - The number of milliseconds, after which the derived
|
||||
* context will signal to abort.
|
||||
* @returns A decorator that can be passed to {@link Context.use}
|
||||
*/
|
||||
static setTimeoutMillis(timeout: number): ContextDecorator {
|
||||
return ctx => AbortContext.forTimeoutMillis(ctx, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a derived context, which has a specific key-value pair set as well
|
||||
* as all key-value pairs that were set in the original context.
|
||||
*
|
||||
* @param key - The key of the value to set
|
||||
* @param value - The value, or a function that accepts the previous value (or
|
||||
* undefined if not set yet) and computes the new value
|
||||
* @returns A decorator that can be passed to {@link Context.use}
|
||||
*/
|
||||
static setValue(
|
||||
key: string | symbol,
|
||||
value: unknown | ((previous: unknown | undefined) => unknown),
|
||||
): ContextDecorator {
|
||||
return ctx => {
|
||||
const v = typeof value === 'function' ? value(ctx.value(key)) : value;
|
||||
return ValueContext.forConstantValue(ctx, key, v);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,47 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Duration } from 'luxon';
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
|
||||
describe('RootContext', () => {
|
||||
it('can perform a manual abort', async () => {
|
||||
const { ctx, abort } = RootContext.create().withAbort();
|
||||
|
||||
const cb = jest.fn();
|
||||
ctx.abortSignal.addEventListener('abort', cb);
|
||||
ctx.abortPromise.then(cb);
|
||||
|
||||
abort();
|
||||
|
||||
await ctx.abortPromise;
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
it('returns empty values', async () => {
|
||||
const ctx = new RootContext();
|
||||
expect(ctx.abortSignal).toBeDefined();
|
||||
expect(ctx.deadline).toBeUndefined();
|
||||
expect(ctx.value('a')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can abort on a timeout', async () => {
|
||||
const ctx = RootContext.create().withTimeout(Duration.fromMillis(200));
|
||||
const start = Date.now();
|
||||
|
||||
const cb = jest.fn();
|
||||
ctx.abortSignal.addEventListener('abort', cb);
|
||||
ctx.abortPromise.then(cb);
|
||||
|
||||
await ctx.abortPromise;
|
||||
const delta = Date.now() - start;
|
||||
|
||||
expect(delta).toBeGreaterThan(100);
|
||||
expect(delta).toBeLessThan(300);
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('can apply behaviors', () => {
|
||||
const ctx = RootContext.create().with(
|
||||
c => c.withValue('a', 1),
|
||||
c => c.withValue<number>('a', p => p! + 1),
|
||||
c => c.withValue('b', 3),
|
||||
it('can decorate', () => {
|
||||
const parent = new RootContext();
|
||||
const child = parent.use(
|
||||
Contexts.setValue('a', 2),
|
||||
Contexts.setValue('a', 3),
|
||||
);
|
||||
|
||||
expect(ctx.value('a')).toBe(2);
|
||||
expect(ctx.value('b')).toBe(3);
|
||||
expect(child.value('a')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,110 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import {
|
||||
abortManually,
|
||||
abortOnTimeout,
|
||||
ContextAbortState,
|
||||
} from './features/abort';
|
||||
import {
|
||||
ContextValues,
|
||||
findInContextValues,
|
||||
unshiftContextValues,
|
||||
} from './features/values';
|
||||
import { AbortController } from 'node-abort-controller';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
|
||||
// The context value key used for holding abort related state
|
||||
const abortKey = Symbol('Context.abort');
|
||||
const neverAborts = new AbortController().signal;
|
||||
|
||||
/**
|
||||
* A context that is meant to be passed as a ctx variable down the call chain,
|
||||
* to pass along scoped information and abort signals.
|
||||
*
|
||||
* @public
|
||||
* An empty root context.
|
||||
*/
|
||||
export class RootContext implements Context {
|
||||
/**
|
||||
* Creates a root context.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This should normally only be called near the root of an application. The
|
||||
* created context is meant to be passed down into deeper levels, which may
|
||||
* or may not make derived contexts out of it.
|
||||
*/
|
||||
static create() {
|
||||
return new RootContext(undefined).withValue<ContextAbortState>(
|
||||
abortKey,
|
||||
abortManually(),
|
||||
);
|
||||
readonly abortSignal = neverAborts;
|
||||
readonly deadline = undefined;
|
||||
|
||||
value<T = unknown>(_key: string | symbol): T | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.abortSignal}
|
||||
*/
|
||||
public get abortSignal(): AbortSignal {
|
||||
return this.value<ContextAbortState>(abortKey)!.signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.abortPromise}
|
||||
*/
|
||||
public get abortPromise(): Promise<void> {
|
||||
return this.value<ContextAbortState>(abortKey)!.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.deadline}
|
||||
*/
|
||||
public get deadline(): DateTime | undefined {
|
||||
return this.value<ContextAbortState>(abortKey)!.deadline;
|
||||
}
|
||||
|
||||
private constructor(private readonly values: ContextValues) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.withAbort}
|
||||
*/
|
||||
withAbort(): { ctx: Context; abort: () => void } {
|
||||
const state = abortManually(this.value<ContextAbortState>(abortKey));
|
||||
return {
|
||||
ctx: this.withValue(abortKey, state),
|
||||
abort: state.abort,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.withTimeout}
|
||||
*/
|
||||
withTimeout(timeout: Duration): Context {
|
||||
return this.withValue<ContextAbortState>(abortKey, previous =>
|
||||
abortOnTimeout(timeout, previous),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.with}
|
||||
*/
|
||||
with(...items: ContextDecorator[]): Context {
|
||||
return items.reduce<Context>((prev, curr) => curr(prev), this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.withValue}
|
||||
*/
|
||||
withValue<T = unknown>(
|
||||
key: string | symbol,
|
||||
value: T | ((previous: T | undefined) => T),
|
||||
): Context {
|
||||
return new RootContext(unshiftContextValues(this.values, key, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc Context.value}
|
||||
*/
|
||||
value<T = unknown>(key: string | symbol): T | undefined {
|
||||
return findInContextValues<T>(this.values, key);
|
||||
use(...items: ContextDecorator[]): Context {
|
||||
return items.reduce((prev, curr) => curr(prev), this as Context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
import { ValueContext } from './ValueContext';
|
||||
|
||||
const s = Symbol();
|
||||
|
||||
describe('ValueContext', () => {
|
||||
it('returns its own values, or delegates to the parent', async () => {
|
||||
const root = new RootContext();
|
||||
const a = ValueContext.forConstantValue(root, 'a', 1);
|
||||
const b = ValueContext.forConstantValue(a, s, 2);
|
||||
const c = ValueContext.forConstantValue(b, 'a', 3);
|
||||
const d = ValueContext.forConstantValue(c, 'b', 4);
|
||||
|
||||
expect(a.value('a')).toBe(1);
|
||||
expect(a.value('b')).toBeUndefined();
|
||||
expect(a.value(s)).toBeUndefined();
|
||||
|
||||
expect(b.value('a')).toBe(1);
|
||||
expect(b.value('b')).toBeUndefined();
|
||||
expect(b.value(s)).toBe(2);
|
||||
|
||||
expect(c.value('a')).toBe(3);
|
||||
expect(c.value('b')).toBeUndefined();
|
||||
expect(c.value(s)).toBe(2);
|
||||
|
||||
expect(d.value('a')).toBe(3);
|
||||
expect(d.value('b')).toBe(4);
|
||||
expect(d.value(s)).toBe(2);
|
||||
});
|
||||
|
||||
it('can decorate', () => {
|
||||
const root = new RootContext();
|
||||
const parent = ValueContext.forConstantValue(root, 'a', 1);
|
||||
const child = parent.use(
|
||||
Contexts.setValue('a', 2),
|
||||
Contexts.setValue('a', 3),
|
||||
);
|
||||
expect(child.value('a')).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Context, ContextDecorator } from './types';
|
||||
|
||||
/**
|
||||
* A context that just holds a single value, and delegates the rest to its
|
||||
* parent.
|
||||
*/
|
||||
export class ValueContext implements Context {
|
||||
static forConstantValue(
|
||||
ctx: Context,
|
||||
key: string | symbol,
|
||||
value: unknown,
|
||||
): Context {
|
||||
return new ValueContext(ctx, key, value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _parent: Context,
|
||||
private readonly _key: string | symbol,
|
||||
private readonly _value: unknown,
|
||||
) {}
|
||||
|
||||
get abortSignal(): AbortSignal {
|
||||
return this._parent.abortSignal;
|
||||
}
|
||||
|
||||
get deadline(): Date | undefined {
|
||||
return this._parent.deadline;
|
||||
}
|
||||
|
||||
value<T = unknown>(key: string | symbol): T | undefined {
|
||||
return key === this._key ? (this._value as T) : this._parent.value(key);
|
||||
}
|
||||
|
||||
use(...items: ContextDecorator[]): Context {
|
||||
return items.reduce((prev, curr) => curr(prev), this as Context);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Duration } from 'luxon';
|
||||
import { abortManually, abortOnTimeout } from './abort';
|
||||
|
||||
describe('ContextAbortState', () => {
|
||||
describe('abortManually', () => {
|
||||
it('can perform a manual abort', async () => {
|
||||
const state = abortManually();
|
||||
|
||||
const cb = jest.fn();
|
||||
state.signal.addEventListener('abort', cb);
|
||||
state.promise.then(cb);
|
||||
|
||||
state.abort();
|
||||
|
||||
await state.promise;
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('triggers child when parent is aborted', async () => {
|
||||
const parent = abortManually();
|
||||
const child = abortManually(parent);
|
||||
|
||||
const parentCb = jest.fn();
|
||||
parent.signal.addEventListener('abort', parentCb);
|
||||
parent.promise.then(parentCb);
|
||||
|
||||
const childCb = jest.fn();
|
||||
child.signal.addEventListener('abort', childCb);
|
||||
child.promise.then(childCb);
|
||||
|
||||
parent.abort();
|
||||
|
||||
await child.promise;
|
||||
expect(parentCb).toBeCalledTimes(2);
|
||||
expect(childCb).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not trigger parent when child is aborted', async () => {
|
||||
const parent = abortManually();
|
||||
const child = abortManually(parent);
|
||||
|
||||
const parentCb = jest.fn();
|
||||
parent.signal.addEventListener('abort', parentCb);
|
||||
parent.promise.then(parentCb);
|
||||
|
||||
const childCb = jest.fn();
|
||||
child.signal.addEventListener('abort', childCb);
|
||||
child.promise.then(childCb);
|
||||
|
||||
child.abort();
|
||||
|
||||
await child.promise;
|
||||
expect(parentCb).toBeCalledTimes(0);
|
||||
expect(childCb).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('only triggers once', async () => {
|
||||
const state = abortManually();
|
||||
|
||||
const cb = jest.fn();
|
||||
state.signal.addEventListener('abort', cb);
|
||||
state.promise.then(cb);
|
||||
|
||||
state.abort();
|
||||
|
||||
await state.promise;
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
|
||||
state.abort();
|
||||
|
||||
await state.promise;
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('abortOnTimeout', () => {
|
||||
it('can abort on a timeout', async () => {
|
||||
const state = abortOnTimeout(Duration.fromMillis(200));
|
||||
const start = Date.now();
|
||||
|
||||
const cb = jest.fn();
|
||||
state.signal.addEventListener('abort', cb);
|
||||
state.promise.then(cb);
|
||||
|
||||
await state.promise;
|
||||
const delta = Date.now() - start;
|
||||
|
||||
expect(delta).toBeGreaterThan(100);
|
||||
expect(delta).toBeLessThan(300);
|
||||
expect(cb).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('aborts early if parent triggers first', async () => {
|
||||
const parent = abortManually();
|
||||
const child = abortOnTimeout(Duration.fromMillis(200), parent);
|
||||
|
||||
const parentCb = jest.fn();
|
||||
parent.signal.addEventListener('abort', parentCb);
|
||||
parent.promise.then(parentCb);
|
||||
|
||||
const childCb = jest.fn();
|
||||
child.signal.addEventListener('abort', childCb);
|
||||
child.promise.then(childCb);
|
||||
|
||||
expect(parentCb).toBeCalledTimes(0);
|
||||
expect(childCb).toBeCalledTimes(0);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
parent.abort();
|
||||
|
||||
await child.promise;
|
||||
expect(parentCb).toBeCalledTimes(2);
|
||||
expect(childCb).toBeCalledTimes(2);
|
||||
expect(Date.now() - start).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { AbortController, AbortSignal } from 'node-abort-controller';
|
||||
|
||||
export type ContextAbortState = {
|
||||
signal: AbortSignal;
|
||||
promise: Promise<void>;
|
||||
deadline: DateTime | undefined;
|
||||
abort: () => void;
|
||||
};
|
||||
|
||||
export function abortManually(
|
||||
previous?: ContextAbortState | undefined,
|
||||
): ContextAbortState {
|
||||
const controller = new AbortController();
|
||||
const abort = controller.abort.bind(controller);
|
||||
previous?.signal.addEventListener('abort', abort);
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
promise: new Promise<void>(resolve => {
|
||||
controller.signal.addEventListener('abort', resolve);
|
||||
}),
|
||||
deadline: previous?.deadline,
|
||||
abort,
|
||||
};
|
||||
}
|
||||
|
||||
export function abortOnTimeout(
|
||||
timeout: Duration,
|
||||
previous?: ContextAbortState | undefined,
|
||||
): ContextAbortState {
|
||||
const deadline = DateTime.now().plus(timeout);
|
||||
if (previous?.deadline && deadline > previous.deadline) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, timeout.as('milliseconds'));
|
||||
|
||||
const abort = () => {
|
||||
previous?.signal.removeEventListener('abort', abort);
|
||||
clearTimeout(timeoutHandle);
|
||||
controller.abort();
|
||||
};
|
||||
|
||||
previous?.signal.addEventListener('abort', abort);
|
||||
|
||||
return {
|
||||
signal: controller.signal,
|
||||
promise: new Promise<void>(resolve => {
|
||||
controller.signal.addEventListener('abort', resolve);
|
||||
}),
|
||||
deadline,
|
||||
abort,
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ContextValues,
|
||||
findInContextValues,
|
||||
unshiftContextValues,
|
||||
} from './values';
|
||||
|
||||
describe('ContextValues', () => {
|
||||
it('can start from the empty list', () => {
|
||||
let list: ContextValues = undefined;
|
||||
expect(findInContextValues(list, 'a')).toBeUndefined();
|
||||
list = unshiftContextValues(list, 'a', 'b');
|
||||
expect(findInContextValues(list, 'a')).toBe('b');
|
||||
expect(findInContextValues(list, 'x')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('always fetches the most recent value', () => {
|
||||
let list: ContextValues = undefined;
|
||||
expect(findInContextValues(list, 'a')).toBeUndefined();
|
||||
list = unshiftContextValues(list, 'a', 1);
|
||||
expect(findInContextValues(list, 'a')).toBe(1);
|
||||
list = unshiftContextValues(list, 'a', 2);
|
||||
expect(findInContextValues(list, 'a')).toBe(2);
|
||||
});
|
||||
|
||||
it('handles all key types', () => {
|
||||
let list: ContextValues = undefined;
|
||||
const symbol1 = Symbol('str');
|
||||
const symbol2 = Symbol('str');
|
||||
list = unshiftContextValues(list, 'str', 'str');
|
||||
list = unshiftContextValues(list, symbol1, 'sym');
|
||||
expect(findInContextValues(list, 'str')).toBe('str');
|
||||
expect(findInContextValues(list, symbol1)).toBe('sym');
|
||||
expect(findInContextValues(list, 'blah')).toBeUndefined();
|
||||
expect(findInContextValues(list, symbol2)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An immutable key-value list. The only operations possible are to add a new
|
||||
* node at the start forming a new list, and to find by key from the start and
|
||||
* backwards through the list.
|
||||
*/
|
||||
export type ContextValues = ContextValueNode | undefined;
|
||||
|
||||
type ContextValueNode = {
|
||||
key: string | symbol;
|
||||
value: unknown;
|
||||
next: ContextValueNode | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new list with the given key-value pair as its first element.
|
||||
*
|
||||
* @param list - The original list
|
||||
* @param key - The key of the pair
|
||||
* @param value - The value of the pair, or a function that accepts the
|
||||
* previously stored value (or undefined if not found) and
|
||||
* computes the new value
|
||||
* @returns A new list with this pair as its first element
|
||||
*/
|
||||
export function unshiftContextValues(
|
||||
list: ContextValues,
|
||||
key: string | symbol,
|
||||
value: unknown | ((previous: unknown | undefined) => unknown),
|
||||
): ContextValues {
|
||||
return {
|
||||
key,
|
||||
value:
|
||||
typeof value === 'function'
|
||||
? value(findInContextValues(list, key))
|
||||
: value,
|
||||
next: list,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to find the value associated with a given key, starting from the
|
||||
* most recently added element.
|
||||
*
|
||||
* @param list - The list
|
||||
* @param key - The key to search for
|
||||
* @returns The first such value, or undefined if no match was found
|
||||
*/
|
||||
export function findInContextValues<T = unknown>(
|
||||
list: ContextValues,
|
||||
key: string | symbol,
|
||||
): T | undefined {
|
||||
for (let current = list; current; current = current.next) {
|
||||
if (key === current.key) {
|
||||
return current.value as T;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -14,5 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { RootContext } from './RootContext';
|
||||
export { Contexts } from './Contexts';
|
||||
export type { Context, ContextDecorator } from './types';
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
|
||||
/**
|
||||
* A function that accepts a context and produces a new, derived context from,
|
||||
* decorated with some specific behavior.
|
||||
@@ -37,37 +35,19 @@ export interface Context {
|
||||
*/
|
||||
readonly abortSignal: AbortSignal;
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the current context or any of its
|
||||
* parents signal to abort.
|
||||
*/
|
||||
readonly abortPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* The point in time when the current context shall time out and abort, if
|
||||
* applicable.
|
||||
*/
|
||||
readonly deadline: DateTime | undefined;
|
||||
readonly deadline: Date | undefined;
|
||||
|
||||
/**
|
||||
* Creates a derived context, which signals to abort operations either when
|
||||
* any parent context signals, or when the current layer calls the returned
|
||||
* abort function.
|
||||
* Attempts to get a stored value by key from the context.
|
||||
*
|
||||
* @returns A derived context, and the function that triggers it to abort.
|
||||
* @param key - The key of the value to get
|
||||
* @returns The associated value, or undefined if not set
|
||||
*/
|
||||
withAbort(): { ctx: Context; abort: () => void };
|
||||
|
||||
/**
|
||||
* Creates a derived context, which signals to abort operations either when
|
||||
* any parent context signals, or when the given amount of time has passed.
|
||||
* This may affect the deadline.
|
||||
*
|
||||
* @param timeout - The duration of time, after which the derived context
|
||||
* will signal to abort.
|
||||
* @returns A derived context with an updated deadline
|
||||
*/
|
||||
withTimeout(timeout: Duration): Context;
|
||||
value<T = unknown>(key: string | symbol): T | undefined;
|
||||
|
||||
/**
|
||||
* Decorates this context with one or more behaviors.
|
||||
@@ -79,26 +59,5 @@ export interface Context {
|
||||
* @param decorators - The decorators to apply
|
||||
* @returns A derived context with the relevant behaviors
|
||||
*/
|
||||
with(...decorators: ContextDecorator[]): Context;
|
||||
|
||||
/**
|
||||
* Creates a derived context, which has a specific key-value pair set as well
|
||||
* as all key-value pairs set in the original context.
|
||||
*
|
||||
* @param key - The key of the value to set
|
||||
* @param value - The value, or a function that accepts the previous value (or
|
||||
* undefined if not set yet) and computes the new value
|
||||
*/
|
||||
withValue<T = unknown>(
|
||||
key: string | symbol,
|
||||
value: T | ((previous: T | undefined) => T),
|
||||
): Context;
|
||||
|
||||
/**
|
||||
* Attempts to get a stored value by key from the context.
|
||||
*
|
||||
* @param key - The key of the value to get
|
||||
* @returns The associated value, or undefined if not set
|
||||
*/
|
||||
value<T = unknown>(key: string | symbol): T | undefined;
|
||||
use(...decorators: ContextDecorator[]): Context;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user