remove the middleware part of context composition
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -151,23 +151,23 @@ export interface ContainerRunner {
|
||||
export interface Context {
|
||||
readonly abortSignal: AbortSignal_2;
|
||||
readonly deadline: Date | undefined;
|
||||
use(...decorators: ContextDecorator[]): Context;
|
||||
value<T = unknown>(key: string): T | undefined;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ContextDecorator = (ctx: Context) => Context;
|
||||
|
||||
// @public
|
||||
export class Contexts {
|
||||
static root(): Context;
|
||||
static setAbort(source: AbortController_2 | AbortSignal_2): ContextDecorator;
|
||||
static setTimeoutDuration(timeout: Duration): ContextDecorator;
|
||||
static setTimeoutMillis(timeout: number): ContextDecorator;
|
||||
static setValue(
|
||||
static withAbort(
|
||||
parentCtx: Context,
|
||||
source: AbortController_2 | AbortSignal_2,
|
||||
): Context;
|
||||
static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context;
|
||||
static withTimeoutMillis(parentCtx: Context, timeout: number): Context;
|
||||
static withValue(
|
||||
parentCtx: Context,
|
||||
key: string,
|
||||
value: unknown | ((previous: unknown | undefined) => unknown),
|
||||
): ContextDecorator;
|
||||
): Context;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { AbortController } from 'node-abort-controller';
|
||||
import { AbortContext } from './AbortContext';
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
|
||||
describe('AbortContext', () => {
|
||||
@@ -343,15 +342,4 @@ describe('AbortContext', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { AbortController, AbortSignal } from 'node-abort-controller';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
import { Context } from './types';
|
||||
|
||||
/**
|
||||
* A context that implements various abort related functionality.
|
||||
@@ -130,8 +130,4 @@ export class AbortContext implements Context {
|
||||
value<T = unknown>(key: string): T | undefined {
|
||||
return this.parent.value(key);
|
||||
}
|
||||
|
||||
use(...items: ContextDecorator[]): Context {
|
||||
return items.reduce((prev, curr) => curr(prev), this as Context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('Contexts', () => {
|
||||
it('works for controllers', () => {
|
||||
const controller = new AbortController();
|
||||
const parent = Contexts.root();
|
||||
const child = parent.use(Contexts.setAbort(controller));
|
||||
const child = Contexts.withAbort(parent, controller);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
controller.abort();
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
@@ -44,7 +44,7 @@ describe('Contexts', () => {
|
||||
it('works for signals', () => {
|
||||
const controller = new AbortController();
|
||||
const parent = Contexts.root();
|
||||
const child = parent.use(Contexts.setAbort(controller.signal));
|
||||
const child = Contexts.withAbort(parent, controller.signal);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
controller.abort();
|
||||
expect(child.abortSignal.aborted).toBe(true);
|
||||
@@ -55,8 +55,9 @@ describe('Contexts', () => {
|
||||
it('works', () => {
|
||||
jest.useFakeTimers();
|
||||
const parent = Contexts.root();
|
||||
const child = parent.use(
|
||||
Contexts.setTimeoutDuration(Duration.fromMillis(200)),
|
||||
const child = Contexts.withTimeoutDuration(
|
||||
parent,
|
||||
Duration.fromMillis(200),
|
||||
);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
jest.advanceTimersByTime(100);
|
||||
@@ -70,7 +71,7 @@ describe('Contexts', () => {
|
||||
it('works', () => {
|
||||
jest.useFakeTimers();
|
||||
const parent = Contexts.root();
|
||||
const child = parent.use(Contexts.setTimeoutMillis(200));
|
||||
const child = Contexts.withTimeoutMillis(parent, 200);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(child.abortSignal.aborted).toBe(false);
|
||||
@@ -82,7 +83,7 @@ describe('Contexts', () => {
|
||||
describe('setValue', () => {
|
||||
it('works', () => {
|
||||
const parent = Contexts.root();
|
||||
const child = parent.use(Contexts.setValue('k', 'v'));
|
||||
const child = Contexts.withValue(parent, 'k', 'v');
|
||||
expect(child.value('k')).toBe('v');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Duration } from 'luxon';
|
||||
import { AbortController, AbortSignal } from 'node-abort-controller';
|
||||
import { AbortContext } from './AbortContext';
|
||||
import { RootContext } from './RootContext';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
import { Context } from './types';
|
||||
import { ValueContext } from './ValueContext';
|
||||
|
||||
/**
|
||||
@@ -51,15 +51,18 @@ export class Contexts {
|
||||
* If the given source was already aborted, then a new already-aborted context
|
||||
* is returned.
|
||||
*
|
||||
* @param parentCtx - A parent context that shall be used as a base
|
||||
* @param source - An abort controller or signal that you intend to perhaps
|
||||
* trigger at some later point in time.
|
||||
* @returns A decorator that can be passed to {@link Context.use}
|
||||
* @returns A new {@link Context}
|
||||
*/
|
||||
static setAbort(source: AbortController | AbortSignal): ContextDecorator {
|
||||
return ctx =>
|
||||
'aborted' in source
|
||||
? AbortContext.forSignal(ctx, source)
|
||||
: AbortContext.forController(ctx, source);
|
||||
static withAbort(
|
||||
parentCtx: Context,
|
||||
source: AbortController | AbortSignal,
|
||||
): Context {
|
||||
return 'aborted' in source
|
||||
? AbortContext.forSignal(parentCtx, source)
|
||||
: AbortContext.forController(parentCtx, source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,13 +70,13 @@ export class Contexts {
|
||||
* any parent context signals, or when the given amount of time has passed.
|
||||
* This may affect the deadline.
|
||||
*
|
||||
* @param parentCtx - A parent context that shall be used as a base
|
||||
* @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}
|
||||
* @returns A new {@link Context}
|
||||
*/
|
||||
static setTimeoutDuration(timeout: Duration): ContextDecorator {
|
||||
return ctx =>
|
||||
AbortContext.forTimeoutMillis(ctx, timeout.as('milliseconds'));
|
||||
static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context {
|
||||
return AbortContext.forTimeoutMillis(parentCtx, timeout.as('milliseconds'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,30 +84,31 @@ export class Contexts {
|
||||
* any parent context signals, or when the given amount of time has passed.
|
||||
* This may affect the deadline.
|
||||
*
|
||||
* @param parentCtx - A parent context that shall be used as a base
|
||||
* @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}
|
||||
* @returns A new {@link Context}
|
||||
*/
|
||||
static setTimeoutMillis(timeout: number): ContextDecorator {
|
||||
return ctx => AbortContext.forTimeoutMillis(ctx, timeout);
|
||||
static withTimeoutMillis(parentCtx: Context, timeout: number): Context {
|
||||
return AbortContext.forTimeoutMillis(parentCtx, 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 parentCtx - A parent context that shall be used as a base
|
||||
* @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}
|
||||
* @returns A new {@link Context}
|
||||
*/
|
||||
static setValue(
|
||||
static withValue(
|
||||
parentCtx: Context,
|
||||
key: string,
|
||||
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);
|
||||
};
|
||||
): Context {
|
||||
const v = typeof value === 'function' ? value(parentCtx.value(key)) : value;
|
||||
return ValueContext.forConstantValue(parentCtx, key, v);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,23 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
|
||||
describe('RootContext', () => {
|
||||
it('returns empty values', async () => {
|
||||
const ctx = new RootContext();
|
||||
expect(ctx.abortSignal).toBeDefined();
|
||||
expect(ctx.abortSignal.aborted).toBe(false);
|
||||
expect(ctx.deadline).toBeUndefined();
|
||||
expect(ctx.value('a')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can decorate', () => {
|
||||
const parent = new RootContext();
|
||||
const child = parent.use(
|
||||
Contexts.setValue('a', 2),
|
||||
Contexts.setValue('a', 3),
|
||||
);
|
||||
expect(child.value('a')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,21 +14,32 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AbortController } from 'node-abort-controller';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import { Context } from './types';
|
||||
|
||||
/**
|
||||
* Since the root context can never abort, and since nobody is every meant to
|
||||
* dispatch events through it, we can use a static dummy instance for
|
||||
* efficiency.
|
||||
*/
|
||||
const dummyAbortSignal: AbortSignal = Object.freeze({
|
||||
aborted: false,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dispatchEvent() {
|
||||
return true;
|
||||
},
|
||||
onabort: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* An empty root context.
|
||||
*/
|
||||
export class RootContext implements Context {
|
||||
readonly abortSignal = new AbortController().signal;
|
||||
readonly abortSignal = dummyAbortSignal;
|
||||
readonly deadline = undefined;
|
||||
|
||||
value<T = unknown>(_key: string): T | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
use(...items: ContextDecorator[]): Context {
|
||||
return items.reduce((prev, curr) => curr(prev), this as Context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Contexts } from './Contexts';
|
||||
import { RootContext } from './RootContext';
|
||||
import { ValueContext } from './ValueContext';
|
||||
|
||||
@@ -42,14 +41,4 @@ describe('ValueContext', () => {
|
||||
expect(d.value('b')).toBe(4);
|
||||
expect(d.value('x')).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
import { Context, ContextDecorator } from './types';
|
||||
import { Context } from './types';
|
||||
|
||||
/**
|
||||
* A context that just holds a single value, and delegates the rest to its
|
||||
@@ -43,8 +43,4 @@ export class ValueContext implements Context {
|
||||
value<T = unknown>(key: string): 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { Contexts } from './Contexts';
|
||||
export type { Context, ContextDecorator } from './types';
|
||||
export type { Context } from './types';
|
||||
|
||||
@@ -16,14 +16,6 @@
|
||||
|
||||
import { AbortSignal } from 'node-abort-controller';
|
||||
|
||||
/**
|
||||
* A function that accepts a context and produces a new, derived context from,
|
||||
* decorated with some specific behavior.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ContextDecorator = (ctx: Context) => Context;
|
||||
|
||||
/**
|
||||
* A context that is meant to be passed as a ctx variable down the call chain,
|
||||
* to pass along scoped information and abort signals.
|
||||
@@ -50,16 +42,4 @@ export interface Context {
|
||||
* @returns The associated value, or undefined if not set
|
||||
*/
|
||||
value<T = unknown>(key: string): T | undefined;
|
||||
|
||||
/**
|
||||
* Decorates this context with one or more behaviors.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* The decorators are applied in the order that they are given.
|
||||
*
|
||||
* @param decorators - The decorators to apply
|
||||
* @returns A derived context with the relevant behaviors
|
||||
*/
|
||||
use(...decorators: ContextDecorator[]): Context;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user