config-loader: MutableConfigSource tests + fixes

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-03-30 14:22:27 +02:00
parent 18c1eb6330
commit 0e38aedf4f
2 changed files with 176 additions and 12 deletions
@@ -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();
}
}