config-loader: MutableConfigSource tests + fixes
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2023 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 { ConfigSources } from './ConfigSources';
|
||||
import { MutableConfigSource } from './MutableConfigSource';
|
||||
import { ConfigSource, ConfigSourceData } from './types';
|
||||
|
||||
function isResolved(promise: Promise<unknown>): Promise<boolean> {
|
||||
return Promise.race([promise.then(() => true), Promise.resolve(false)]);
|
||||
}
|
||||
|
||||
async function readAll(
|
||||
source: ConfigSource,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ConfigSourceData[][]> {
|
||||
const results: ConfigSourceData[][] = [];
|
||||
|
||||
for await (const { data } of source.readConfigData({ signal })) {
|
||||
results.push(data);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('MutableConfigSource', () => {
|
||||
it('should be initialized with data', async () => {
|
||||
const source = MutableConfigSource.create({ data: { a: 1 } });
|
||||
const config = await ConfigSources.toConfig(source);
|
||||
expect(config.getNumber('a')).toEqual(1);
|
||||
config.close();
|
||||
});
|
||||
|
||||
it('should be created without data', async () => {
|
||||
const source = MutableConfigSource.create();
|
||||
const it = source.readConfigData();
|
||||
const first = it.next();
|
||||
await expect(isResolved(first)).resolves.toBe(false);
|
||||
source.setData({ a: 1 });
|
||||
await expect(first).resolves.toEqual({
|
||||
value: {
|
||||
data: [
|
||||
{
|
||||
data: { a: 1 },
|
||||
context: 'mutable-config',
|
||||
},
|
||||
],
|
||||
},
|
||||
done: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should be mutable and work with multiple consumers', async () => {
|
||||
const source = MutableConfigSource.create({ data: { a: 1 } });
|
||||
const resultsPromise = readAll(source);
|
||||
|
||||
const it = source.readConfigData();
|
||||
await expect(it.next()).resolves.toEqual({
|
||||
value: {
|
||||
data: [
|
||||
{
|
||||
data: { a: 1 },
|
||||
context: 'mutable-config',
|
||||
},
|
||||
],
|
||||
},
|
||||
done: false,
|
||||
});
|
||||
|
||||
const next2 = it.next();
|
||||
source.setData({ a: 2 });
|
||||
await expect(next2).resolves.toEqual({
|
||||
value: {
|
||||
data: [
|
||||
{
|
||||
data: { a: 2 },
|
||||
context: 'mutable-config',
|
||||
},
|
||||
],
|
||||
},
|
||||
done: false,
|
||||
});
|
||||
|
||||
const next3 = it.next();
|
||||
source.setData({ a: 3 });
|
||||
await expect(next3).resolves.toEqual({
|
||||
value: {
|
||||
data: [
|
||||
{
|
||||
data: { a: 3 },
|
||||
context: 'mutable-config',
|
||||
},
|
||||
],
|
||||
},
|
||||
done: false,
|
||||
});
|
||||
|
||||
const last = it.next();
|
||||
source.close();
|
||||
await expect(last).resolves.toEqual({
|
||||
done: true,
|
||||
});
|
||||
|
||||
await expect(resultsPromise).resolves.toEqual([
|
||||
[{ data: { a: 1 }, context: 'mutable-config' }],
|
||||
[{ data: { a: 2 }, context: 'mutable-config' }],
|
||||
[{ data: { a: 3 }, context: 'mutable-config' }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be self-mutable', async () => {
|
||||
const source = MutableConfigSource.create({ data: { a: 1 } });
|
||||
const resultsPromise = readAll(source);
|
||||
|
||||
for await (const { data } of source.readConfigData()) {
|
||||
const a = data[0].data.a as number;
|
||||
if (a < 3) {
|
||||
source.setData({ a: a + 1 });
|
||||
} else {
|
||||
source.close();
|
||||
}
|
||||
}
|
||||
|
||||
await expect(resultsPromise).resolves.toEqual([
|
||||
[{ data: { a: 1 }, context: 'mutable-config' }],
|
||||
[{ data: { a: 2 }, context: 'mutable-config' }],
|
||||
[{ data: { a: 3 }, context: 'mutable-config' }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -23,18 +23,22 @@ import {
|
||||
import { simpleDefer, SimpleDeferred, waitOrAbort } from './utils';
|
||||
|
||||
export class MutableConfigSource implements ConfigSource {
|
||||
static create(options: { data: JsonObject; context?: string }): ConfigSource {
|
||||
static create(options?: {
|
||||
data?: JsonObject;
|
||||
context?: string;
|
||||
}): MutableConfigSource {
|
||||
return new MutableConfigSource(
|
||||
options.data,
|
||||
options.context ?? 'mutable-config',
|
||||
options?.context ?? 'mutable-config',
|
||||
options?.data,
|
||||
);
|
||||
}
|
||||
|
||||
#currentData: JsonObject;
|
||||
#currentData?: JsonObject;
|
||||
#deferred: SimpleDeferred<void>;
|
||||
readonly #context: string;
|
||||
readonly #abortController = new AbortController();
|
||||
|
||||
private constructor(initialData: JsonObject, context: string) {
|
||||
private constructor(context: string, initialData?: JsonObject) {
|
||||
this.#currentData = initialData;
|
||||
this.#context = context;
|
||||
this.#deferred = simpleDefer();
|
||||
@@ -43,21 +47,39 @@ export class MutableConfigSource implements ConfigSource {
|
||||
async *readConfigData(
|
||||
options?: ReadConfigDataOptions | undefined,
|
||||
): AsyncConfigSourceIterator {
|
||||
yield { data: [{ data: this.#currentData, context: this.#context }] };
|
||||
let deferredPromise = this.#deferred.promise;
|
||||
|
||||
if (this.#currentData !== undefined) {
|
||||
yield { data: [{ data: this.#currentData, context: this.#context }] };
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
const [ok] = await waitOrAbort(this.#deferred.promise, options?.signal);
|
||||
const [ok] = await waitOrAbort(deferredPromise, [
|
||||
options?.signal,
|
||||
this.#abortController.signal,
|
||||
]);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
deferredPromise = this.#deferred.promise;
|
||||
|
||||
yield { data: [{ data: this.#currentData, context: this.#context }] };
|
||||
if (this.#currentData !== undefined) {
|
||||
yield { data: [{ data: this.#currentData, context: this.#context }] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setData(data: JsonObject) {
|
||||
this.#currentData = data;
|
||||
this.#deferred.resolve();
|
||||
this.#deferred = simpleDefer();
|
||||
setData(data: JsonObject): void {
|
||||
if (!this.#abortController.signal.aborted) {
|
||||
this.#currentData = data;
|
||||
const oldDeferred = this.#deferred;
|
||||
this.#deferred = simpleDefer();
|
||||
oldDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#currentData = undefined;
|
||||
this.#abortController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user