From 0e38aedf4f0520bcde3d181b7ca513b6c4ee2a17 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Mar 2023 14:22:27 +0200 Subject: [PATCH] config-loader: MutableConfigSource tests + fixes Signed-off-by: Patrik Oldsberg --- .../src/sources/MutableConfigSource.test.ts | 142 ++++++++++++++++++ .../src/sources/MutableConfigSource.ts | 46 ++++-- 2 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 packages/config-loader/src/sources/MutableConfigSource.test.ts diff --git a/packages/config-loader/src/sources/MutableConfigSource.test.ts b/packages/config-loader/src/sources/MutableConfigSource.test.ts new file mode 100644 index 0000000000..eebd276c4d --- /dev/null +++ b/packages/config-loader/src/sources/MutableConfigSource.test.ts @@ -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): Promise { + return Promise.race([promise.then(() => true), Promise.resolve(false)]); +} + +async function readAll( + source: ConfigSource, + signal?: AbortSignal, +): Promise { + 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' }], + ]); + }); +}); diff --git a/packages/config-loader/src/sources/MutableConfigSource.ts b/packages/config-loader/src/sources/MutableConfigSource.ts index cb9401b3bc..e9a5432d36 100644 --- a/packages/config-loader/src/sources/MutableConfigSource.ts +++ b/packages/config-loader/src/sources/MutableConfigSource.ts @@ -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; 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(); } }