Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce the Context to the backend again
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
- Loading branch information
Showing
12 changed files
with
689 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@backstage/backend-common': patch | ||
--- | ||
|
||
Added a Context class for the backend, that handles aborting, timeouts, api resolution etc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* 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 { 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('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), | ||
); | ||
|
||
expect(ctx.value('a')).toBe(2); | ||
expect(ctx.value('b')).toBe(3); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* | ||
* 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 { AbortSignal } from 'node-abort-controller'; | ||
import { | ||
abortManually, | ||
abortOnTimeout, | ||
ContextAbortState, | ||
} from './features/abort'; | ||
import { | ||
ContextValues, | ||
findInContextValues, | ||
unshiftContextValues, | ||
} from './features/values'; | ||
import { Context, ContextDecorator } from './types'; | ||
|
||
// The context value key used for holding abort related state | ||
const abortKey = Symbol('Context.abort'); | ||
|
||
/** | ||
* 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 | ||
*/ | ||
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(), | ||
); | ||
} | ||
|
||
/** | ||
* {@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); | ||
} | ||
} |
Oops, something went wrong.