From 67340717313c17d8bac12211784a2d917650784a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:32:56 +0000 Subject: [PATCH 01/11] chore: bump @biomejs/biome Bumps the npm-development group with 1 update: [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome). Updates `@biomejs/biome` from 2.5.15 to 2.6.2 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/@biomejs/biome@2.6.2/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.6.2/packages/@biomejs/biome) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.6.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-development ... Signed-off-by: dependabot[bot] --- package-lock.json | 108 ++++++++++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 73 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index e6c3667..21be272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "testcontainers": "12.2.1" }, "devDependencies": { - "@biomejs/biome": "2.5.15", + "@biomejs/biome": "2.6.2", "@types/node": "26.6.5", "get-port": "7.2.0", "tsx": "4.23.15", @@ -26,9 +26,9 @@ "license": "Apache-2.0" }, "node_modules/@biomejs/biome": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.15.tgz", - "integrity": "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.6.2.tgz", + "integrity": "sha512-JLTFAFF9vfJcWwLhwuL4ZHflLg+XfTEC4IUnAlJRX1cMRt57++Xd1Ysb9Fdmlh9YUrvyCXfPEHjjEIqKJGdPKw==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -42,20 +42,56 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.15", - "@biomejs/cli-darwin-x64": "2.5.15", - "@biomejs/cli-linux-arm64": "2.5.15", - "@biomejs/cli-linux-arm64-musl": "2.5.15", - "@biomejs/cli-linux-x64": "2.5.15", - "@biomejs/cli-linux-x64-musl": "2.5.15", - "@biomejs/cli-win32-arm64": "2.5.15", - "@biomejs/cli-win32-x64": "2.5.15" + "@biomejs/cli-android-arm64": "2.6.2", + "@biomejs/cli-android-x64": "2.6.2", + "@biomejs/cli-darwin-arm64": "2.6.2", + "@biomejs/cli-darwin-x64": "2.6.2", + "@biomejs/cli-linux-arm64": "2.6.2", + "@biomejs/cli-linux-arm64-musl": "2.6.2", + "@biomejs/cli-linux-x64": "2.6.2", + "@biomejs/cli-linux-x64-musl": "2.6.2", + "@biomejs/cli-win32-arm64": "2.6.2", + "@biomejs/cli-win32-x64": "2.6.2" + } + }, + "node_modules/@biomejs/cli-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-android-arm64/-/cli-android-arm64-2.6.2.tgz", + "integrity": "sha512-dMpdpH7zFEL5FxuIA9LJW+RoNyC3oeDB7PJyjqVVwtMNoKB5uo8OZYmrVSBikn71txzYwy/3xga7UME6bJQZ6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-android-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-android-x64/-/cli-android-x64-2.6.2.tgz", + "integrity": "sha512-XCwEhPBGYzuRA502dRkCcQ+5cefQwuxQR7f9wa4do92dvIcuBL6VMLMFB/fL9hlpZ4dGSfHdpGFSIDlp52mg0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.15.tgz", - "integrity": "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-rpXtabRgyZNz3aCjd/cRoSZGYi92R9eAXdUUe1sigIsRDyfMwgThaa98Yxt1fiZ7mxquqkAyIHIo08N/e1ToBA==", "cpu": [ "arm64" ], @@ -70,9 +106,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.15.tgz", - "integrity": "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.6.2.tgz", + "integrity": "sha512-7GGzlm1GxUKoQ9CNV8tQZ+XXad4sz+v+uzNsWlhaGjsnZqUOVBw1OmAMpG3uNV/Mr0UrCpRJvSK0qvfuqSQOjg==", "cpu": [ "x64" ], @@ -87,9 +123,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.15.tgz", - "integrity": "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.6.2.tgz", + "integrity": "sha512-Qs4CofCE8MdkjZFfEjswh2NAcbyQ7IVYtGSzQBvLqFlla/CSXroGbktUFndWG/kVSTywadauzAspYWtSfqAc2g==", "cpu": [ "arm64" ], @@ -107,9 +143,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.15.tgz", - "integrity": "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-I+FYCLVSmR3cYfKcQ+KNeLOZ5w0wRd9fQFlw3JbSvy7bkegvTqdHaBRRcJzHQB/1ldldTLIcG21ka6X2cTWmJw==", "cpu": [ "arm64" ], @@ -127,9 +163,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.15.tgz", - "integrity": "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.6.2.tgz", + "integrity": "sha512-FJKsb95418139xDdbv9XnNJsIIwvsgR21Of0aE9B9U+nQlOtRq5gJl2ps0jZUmYBxTaOlP9BKo8N+Ff8+Rui1g==", "cpu": [ "x64" ], @@ -147,9 +183,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.15.tgz", - "integrity": "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-wnjEXAK2pDd2j8r9gKI3L+44sIxjkYzS5aHmD1+K57RNJ3sFyT4n22NH6QFPqqeVdRBxN54H3kckUUCrFKCcJw==", "cpu": [ "x64" ], @@ -167,9 +203,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.15.tgz", - "integrity": "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.6.2.tgz", + "integrity": "sha512-/PEOo6evlgrQOfKhMNtC/4Gk8ai3hySXXEtjUzCqZe/4iAbMdqelpp7qvZEDyLxlZS+BjOMQFpq2gXI/BPaAqw==", "cpu": [ "arm64" ], @@ -184,9 +220,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.15", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.15.tgz", - "integrity": "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.6.2.tgz", + "integrity": "sha512-FVlWD+mDDz8nN5LWKRWUhhb7wKIK0VE3oXQvQBX7Fwwj00CvqMVhMyiz1iz8O5aZGmnkhFlKSwJPrUvMh5prQw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index dfdbc73..5277b6d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "testcontainers": "12.2.1" }, "devDependencies": { - "@biomejs/biome": "2.5.15", + "@biomejs/biome": "2.6.2", "@types/node": "26.6.5", "get-port": "7.2.0", "tsx": "4.23.15", From feaf038fd5c52467ec5d8668b79d8bb118791a2c Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:47:54 +0100 Subject: [PATCH 02/11] chore: Rename the suppression of useThisInClassMethods to its new group. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/container/startContainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/container/startContainer.ts b/src/container/startContainer.ts index 7e93c52..ca0eb91 100644 --- a/src/container/startContainer.ts +++ b/src/container/startContainer.ts @@ -58,7 +58,7 @@ class ReadinessWait implements WaitStrategy { return this; } - // biome-ignore lint/nursery/useThisInClassMethods: The interface of testcontainers asks for this method. + // biome-ignore lint/style/useThisInClassMethods: The interface of testcontainers asks for this method. public isStartupTimeoutSet(): boolean { return true; } From 0018f9d593d879f371f8f28cf93dd8d4ea1ed8b9 Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:49:39 +0100 Subject: [PATCH 03/11] chore: Assert unconditionally in the tests whose expectations differ between the APIs. The test of a malformed event type determines the expected code and status before asserting them, and the tests of the recursive option are split into one for the v1 API and one for everything else, each of which skips itself on a server it does not apply to. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/readEventType.test.ts | 17 ++++++------- src/writeEvents.test.ts | 53 ++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/readEventType.test.ts b/src/readEventType.test.ts index df27765..79596c3 100644 --- a/src/readEventType.test.ts +++ b/src/readEventType.test.ts @@ -44,21 +44,20 @@ suite('readEventType', () => { const client = container.getClient(); const apiVersion = await getApiVersionOf(container); + // The v1 API has no code for a malformed event type. + const expected = + apiVersion === 'v1' + ? { code: undefined, status: 400 } + : { code: ErrorCode.eventTypeMalformed, status: 200 }; + await assert.rejects( async () => { await client.readEventType('malformed.eventType.'); }, (error: unknown): boolean => { assert.ok(error instanceof DbApiError); - - // The v1 API has no code for a malformed event type. - if (apiVersion === 'v1') { - assert.equal(error.code, undefined); - assert.equal(error.status, 400); - } else { - assert.equal(error.code, ErrorCode.eventTypeMalformed); - assert.equal(error.status, 200); - } + assert.equal(error.code, expected.code); + assert.equal(error.status, expected.status); return true; }, ); diff --git a/src/writeEvents.test.ts b/src/writeEvents.test.ts index 5bc2ed2..ece176a 100644 --- a/src/writeEvents.test.ts +++ b/src/writeEvents.test.ts @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { afterEach, beforeEach, suite, test } from 'node:test'; +import { afterEach, beforeEach, suite, type TestContext, test } from 'node:test'; import type { Client } from './Client.js'; import { Container } from './Container.js'; import { DbApiError } from './DbApiError.js'; @@ -30,6 +30,13 @@ const getEvent = (subject: string): EventCandidate => ({ }, }); +// Writes one event on each of the subjects, one write after another. +const writeEventsOn = async (client: Client, subjects: string[]): Promise => { + for (const subject of subjects) { + await client.writeEvents([getEvent(subject)]); + } +}; + const readAllEvents = async (client: Client): Promise => { const events: Event[] = []; for await (const event of client.readEvents('/', { recursive: true })) { @@ -344,27 +351,28 @@ suite('writeEvents', () => { }, ); - const definePreconditionOptionsTest = ({ + const definePreconditionOptionsTests = ({ name, recursive, subjects, violated, holding, }: PreconditionOptionsCase): void => { - test( - `supports the ${name} precondition with the recursive option set to ${recursive}.`, - integrationTimeout, - async (): Promise => { - const client = container.getClient(); - const apiVersion = await getApiVersionOf(container); + // The v1 API cannot check a recursive precondition, so the write fails + // before it is sent, whether the precondition would hold or not. + if (recursive) { + test( + `rejects the ${name} precondition with the recursive option set to true through the v1 API.`, + integrationTimeout, + async (context: TestContext): Promise => { + if ((await getApiVersionOf(container)) !== 'v1') { + context.skip('The server offers the v2 API, which checks recursive preconditions.'); + return; + } - for (const subject of subjects) { - await client.writeEvents([getEvent(subject)]); - } + const client = container.getClient(); + await writeEventsOn(client, subjects); - // The v1 API cannot check a recursive precondition, so the write fails - // before it is sent, whether the precondition would hold or not. - if (recursive && apiVersion === 'v1') { for (const precondition of [violated, holding]) { await assert.rejects(async () => { await client.writeEvents([getEvent('/test')], [precondition]); @@ -372,9 +380,24 @@ suite('writeEvents', () => { } assert.equal((await readAllEvents(client)).length, subjects.length); + }, + ); + } + + test( + `supports the ${name} precondition with the recursive option set to ${recursive}.`, + integrationTimeout, + async (context: TestContext): Promise => { + if (recursive && (await getApiVersionOf(container)) === 'v1') { + context.skip( + 'The server offers only the v1 API, which cannot check recursive preconditions.', + ); return; } + const client = container.getClient(); + await writeEventsOn(client, subjects); + await assert.rejects(async () => { await client.writeEvents([getEvent('/test')], [violated]); }, isPreconditionViolated); @@ -386,6 +409,6 @@ suite('writeEvents', () => { }; for (const preconditionOptionsCase of preconditionOptionsCases) { - definePreconditionOptionsTest(preconditionOptionsCase); + definePreconditionOptionsTests(preconditionOptionsCase); } }); From a8706a0169e287e99d49d7df39c7a22033ec0183 Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:49:56 +0100 Subject: [PATCH 04/11] chore: Assert that the calls do not reject in the tests that had no assertion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/ping.test.ts | 5 +++-- src/registerEventSchema.test.ts | 5 +++-- src/stallTimeout.test.ts | 3 +-- src/verifyApiToken.test.ts | 5 +++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ping.test.ts b/src/ping.test.ts index 8a6bae8..c054fc6 100644 --- a/src/ping.test.ts +++ b/src/ping.test.ts @@ -25,8 +25,9 @@ suite('ping', () => { async (): Promise => { const client = container.getClient(); - // Should not throw. - await client.ping(); + await assert.doesNotReject(async () => { + await client.ping(); + }); }, ); diff --git a/src/registerEventSchema.test.ts b/src/registerEventSchema.test.ts index af1ff91..d676cac 100644 --- a/src/registerEventSchema.test.ts +++ b/src/registerEventSchema.test.ts @@ -35,8 +35,9 @@ suite('registerEventSchema', () => { additionalProperties: false, }; - // Should not throw. - await client.registerEventSchema(eventType, schema); + await assert.doesNotReject(async () => { + await client.registerEventSchema(eventType, schema); + }); }); test( diff --git a/src/stallTimeout.test.ts b/src/stallTimeout.test.ts index f68237a..b5c5ab9 100644 --- a/src/stallTimeout.test.ts +++ b/src/stallTimeout.test.ts @@ -353,8 +353,7 @@ suite('stallTimeout', { timeout: 30_000 }, () => { trickle(response, answer.body); }; - // Should not throw. - await run(client); + await assert.doesNotReject(async () => await run(client)); }); }); }; diff --git a/src/verifyApiToken.test.ts b/src/verifyApiToken.test.ts index 077be1c..d6be798 100644 --- a/src/verifyApiToken.test.ts +++ b/src/verifyApiToken.test.ts @@ -26,8 +26,9 @@ suite('verifyApiToken', () => { async (): Promise => { const client = container.getClient(); - // Should not throw. - await client.verifyApiToken(); + await assert.doesNotReject(async () => { + await client.verifyApiToken(); + }); }, ); From 12dfc8ccc97222ed956f0508fc10c5483bd361b9 Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:56:59 +0100 Subject: [PATCH 05/11] chore: Parse the signature of an event in a function of its own, apart from verifying it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/Event.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Event.ts b/src/Event.ts index 50101ab..b47a8a5 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -13,9 +13,20 @@ const hashVersionRegex = /^esdb:hash:(?[^:]*):/u; // have like this. const missingField = '-'; +// A signature starts with this prefix, followed by its bytes in hex. +const signaturePrefix = 'esdb:signature:v1:'; + const getSha256 = (input: string): string => crypto.createHash('sha256').update(input).digest('hex'); +const getSignatureBytes = (signature: string): Buffer => { + if (!signature.startsWith(signaturePrefix)) { + throw new Error(`Signature must start with '${signaturePrefix}'`); + } + + return Buffer.from(signature.slice(signaturePrefix.length), 'hex'); +}; + class Event { public specversion: string; public id: string; @@ -145,14 +156,7 @@ class Event { this.verifyHash(); - const signaturePrefix = 'esdb:signature:v1:'; - - if (!this.signature.startsWith(signaturePrefix)) { - throw new Error(`Signature must start with '${signaturePrefix}'`); - } - - const signature = this.signature.slice(signaturePrefix.length); - const signatureBytes = Buffer.from(signature, 'hex'); + const signatureBytes = getSignatureBytes(this.signature); const hashBytes = Buffer.from(this.hash, 'utf8'); const isSignatureValid = crypto.verify(null, hashBytes, verificationKey, signatureBytes); From e94cd355f2141d30a219cd4eaa69a9b7af08300d Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:56:59 +0100 Subject: [PATCH 06/11] chore: Check each member of a shape in a function of its own, instead of nesting the checks in a loop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/types/hasShapeOf.ts | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/types/hasShapeOf.ts b/src/types/hasShapeOf.ts index a69c43c..ed54453 100644 --- a/src/types/hasShapeOf.ts +++ b/src/types/hasShapeOf.ts @@ -9,32 +9,17 @@ type Shape = { const isGuard = (value: unknown): value is Guard => typeof value === 'function'; const isShape = (value: unknown): value is Shape => isRecord(value); -const hasShapeOf = (value: unknown, shape: Shape): value is T => { - if (!isRecord(value)) { - return false; +// A value matches a guard if the guard accepts it, and a nested shape if it is +// a record of that shape. +const matchesSpec = (value: unknown, spec: unknown): boolean => { + if (isGuard(spec)) { + return spec(value); } - for (const [key, spec] of Object.entries(shape)) { - const val = value[key]; - - if (isGuard(spec)) { - if (!spec(val)) { - return false; - } - - continue; - } - - if (!(isRecord(val) && isShape(spec))) { - return false; - } - - if (!hasShapeOf(val, spec)) { - return false; - } - } - - return true; + return isRecord(value) && isShape(spec) && hasShapeOf(value, spec); }; +const hasShapeOf = (value: unknown, shape: Shape): value is T => + isRecord(value) && Object.entries(shape).every(([key, spec]) => matchesSpec(value[key], spec)); + export { hasShapeOf }; From 9433f2213cf62572ba5860d61e7ebb3cc71c37cb Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:56:59 +0100 Subject: [PATCH 07/11] chore: Scan the items of a JSON array or object in one place, for both the elements and the members. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/json/getRawJsonElements.ts | 23 ++++++++--------------- src/json/getRawJsonMember.ts | 23 ++++++++--------------- src/json/scanJson.ts | 23 ++++++++++++++++++++++- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/json/getRawJsonElements.ts b/src/json/getRawJsonElements.ts index 078d4ef..db9bb47 100644 --- a/src/json/getRawJsonElements.ts +++ b/src/json/getRawJsonElements.ts @@ -1,28 +1,21 @@ -import { scanJsonValue, skipJsonWhitespace } from './scanJson.js'; +import { scanJsonItems, scanJsonValue, skipJsonWhitespace } from './scanJson.js'; // Returns the elements of a JSON array exactly as the text contains them, or // an empty array if the text is not an array. const getRawJsonElements = (text: string): string[] => { const elements: string[] = []; - let position = skipJsonWhitespace(text, 0); + const start = skipJsonWhitespace(text, 0); - if (text[position] !== '[') { + if (text[start] !== '[') { return elements; } - position = skipJsonWhitespace(text, position + 1); + scanJsonItems(text, start, elementStart => { + const elementEnd = scanJsonValue(text, elementStart); + elements.push(text.slice(elementStart, elementEnd)); - while (position < text.length && text[position] !== ']') { - const elementEnd = scanJsonValue(text, position); - elements.push(text.slice(position, elementEnd)); - - position = skipJsonWhitespace(text, elementEnd); - if (text[position] !== ',') { - break; - } - - position = skipJsonWhitespace(text, position + 1); - } + return elementEnd; + }); return elements; }; diff --git a/src/json/getRawJsonMember.ts b/src/json/getRawJsonMember.ts index 6d1b33d..2b7b137 100644 --- a/src/json/getRawJsonMember.ts +++ b/src/json/getRawJsonMember.ts @@ -1,22 +1,20 @@ -import { scanJsonString, scanJsonValue, skipJsonWhitespace } from './scanJson.js'; +import { scanJsonItems, scanJsonString, scanJsonValue, skipJsonWhitespace } from './scanJson.js'; // Returns the value of a member of a JSON object exactly as the text contains // it, or undefined if the text is not an object or has no such member. If the // object has the member more than once, the last one wins, as with JSON.parse. const getRawJsonMember = (text: string, key: string): string | undefined => { - let position = skipJsonWhitespace(text, 0); + const start = skipJsonWhitespace(text, 0); - if (text[position] !== '{') { + if (text[start] !== '{') { return; } - position = skipJsonWhitespace(text, position + 1); - let rawValue: string | undefined; - while (text[position] === '"') { - const keyEnd = scanJsonString(text, position); - const currentKey: unknown = JSON.parse(text.slice(position, keyEnd)); + scanJsonItems(text, start, memberStart => { + const keyEnd = scanJsonString(text, memberStart); + const currentKey: unknown = JSON.parse(text.slice(memberStart, keyEnd)); const colon = skipJsonWhitespace(text, keyEnd); const valueStart = skipJsonWhitespace(text, colon + 1); @@ -26,13 +24,8 @@ const getRawJsonMember = (text: string, key: string): string | undefined => { rawValue = text.slice(valueStart, valueEnd); } - position = skipJsonWhitespace(text, valueEnd); - if (text[position] !== ',') { - break; - } - - position = skipJsonWhitespace(text, position + 1); - } + return valueEnd; + }); return rawValue; }; diff --git a/src/json/scanJson.ts b/src/json/scanJson.ts index bad9093..42ec0d3 100644 --- a/src/json/scanJson.ts +++ b/src/json/scanJson.ts @@ -85,4 +85,25 @@ const scanJsonValue = (text: string, start: number): number => { return position; }; -export { scanJsonString, scanJsonValue, skipJsonWhitespace }; +// Scans the items of the JSON array or object whose opening bracket is at the +// given position, the elements of an array or the members of an object, one +// after another. It passes the start of each item to the given function, +// which returns the position right after the item. +const scanJsonItems = ( + text: string, + start: number, + scanItem: (itemStart: number) => number, +): void => { + let position = skipJsonWhitespace(text, start + 1); + + while (position < text.length && text[position] !== ']' && text[position] !== '}') { + position = skipJsonWhitespace(text, scanItem(position)); + if (text[position] !== ',') { + return; + } + + position = skipJsonWhitespace(text, position + 1); + } +}; + +export { scanJsonItems, scanJsonString, scanJsonValue, skipJsonWhitespace }; From a9c695c29f32c008627347a854dbc6effefef17e Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:56:59 +0100 Subject: [PATCH 08/11] chore: Remove what a failed attempt to start a container has left behind in a function of its own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/container/startContainer.ts | 85 +++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/container/startContainer.ts b/src/container/startContainer.ts index ca0eb91..6c364c5 100644 --- a/src/container/startContainer.ts +++ b/src/container/startContainer.ts @@ -32,6 +32,15 @@ interface StartContainerOptions { onBackgroundRemoval?: (removal: Promise) => void; } +// A failed attempt leaves behind the containers that carry its start label, +// and possibly a start and a wait for readiness that are still pending. +interface FailedAttempt { + client: ContainerRuntimeClient; + startId: string; + starting: Promise; + readiness: Promise; +} + const maximumAttempts = 3; const millisecondsPerSecond = 1000; const defaultSettleTimeout = 10_000; @@ -89,16 +98,49 @@ const waitForSettling = async (promises: Promise[], timeout: number): P return hasSettled; }; +// Removes what a failed attempt has left behind. It removes the container at +// once. If the time ran out while Docker was still creating or starting the +// container, it then waits for that to end, for a while at most, and removes +// what was created meanwhile. If that takes longer, it removes the container +// once Docker has created it, without waiting for that. A failed removal is +// collected. +const removeLeftoversOfAttempt = async ( + { client, startId, starting, readiness }: FailedAttempt, + options: StartContainerOptions, + removalErrors: unknown[], +): Promise => { + const removeContainers = async (): Promise => { + try { + await removeContainersOfAttempt(client, startId); + } catch (removalError) { + removalErrors.push(removalError); + } + }; + + await removeContainers(); + + // The attempt has failed already, so how the start ends no longer + // matters, only that nothing of it is left. + const hasSettled = await waitForSettling( + [starting, readiness], + options.settleTimeout ?? defaultSettleTimeout, + ); + await removeContainers(); + + if (!hasSettled) { + // The caller does not wait for a start that Docker does not finish, and + // the removal collects its own errors. + const removal = Promise.allSettled([starting]).then(removeContainers); + options.onBackgroundRemoval?.(removal); + } +}; + // Starts a container and waits until its database is reachable, for the // startup timeout at most, from creating the container on. If that fails, it -// removes the container at once. If the time ran out while Docker was still -// creating or starting the container, it then waits for that to end, for a -// while at most, and removes what was created meanwhile. If that takes longer, -// it removes the container once Docker has created it, without waiting for -// that. A failed removal does not change the outcome of the attempt, but is -// collected. It returns -// undefined if the database did not become reachable, so that another -// container can take its place. +// removes what the attempt has left behind. A failed removal does not change +// the outcome of the attempt, but is collected. It returns undefined if the +// database did not become reachable, so that another container can take its +// place. const attemptToStart = async ( client: ContainerRuntimeClient, options: StartContainerOptions, @@ -134,30 +176,11 @@ const attemptToStart = async ( return container; } catch (error) { attemptEnded.abort(); - const removeContainers = async (): Promise => { - try { - await removeContainersOfAttempt(client, startId); - } catch (removalError) { - removalErrors.push(removalError); - } - }; - - await removeContainers(); - - // The attempt has failed already, so how the start ends no longer - // matters, only that nothing of it is left. - const hasSettled = await waitForSettling( - [starting, readiness], - options.settleTimeout ?? defaultSettleTimeout, + await removeLeftoversOfAttempt( + { client, startId, starting, readiness }, + options, + removalErrors, ); - await removeContainers(); - - if (!hasSettled) { - // The caller does not wait for a start that Docker does not finish, and - // the removal collects its own errors. - const removal = Promise.allSettled([starting]).then(removeContainers); - options.onBackgroundRemoval?.(removal); - } if ((deadline.signal.aborted && error === deadline.signal.reason) || isPortError(error)) { return undefined; From 9cbb4a639f2114839b28b87a1a4b9b16a8909d6f Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:57:06 +0100 Subject: [PATCH 09/11] chore: Suppress noExcessiveStatementsPerFunction where a function of the SDK is one unit. Each suppression names what makes the function one unit. The stream readers and the stall guard are left as they are, so that their behavior stays exactly the same. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/Event.ts | 1 + src/container/startContainer.ts | 2 ++ src/container/waitUntilReachable.ts | 1 + src/detectApiVersion.ts | 1 + src/json/scanJson.ts | 1 + src/ndjson/readNdJsonStream.ts | 1 + src/stall/readResponseText.ts | 1 + src/v1/V1Transport.ts | 4 ++++ src/v2/readV2Stream.ts | 1 + 9 files changed, 13 insertions(+) diff --git a/src/Event.ts b/src/Event.ts index b47a8a5..3e7a515 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -44,6 +44,7 @@ class Event { public tracestate?: string; public signature: string | null; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The constructor assigns each field of the event, one statement per field. public constructor({ specversion, id, diff --git a/src/container/startContainer.ts b/src/container/startContainer.ts index 6c364c5..383320c 100644 --- a/src/container/startContainer.ts +++ b/src/container/startContainer.ts @@ -141,6 +141,7 @@ const removeLeftoversOfAttempt = async ( // the outcome of the attempt, but is collected. It returns undefined if the // database did not become reachable, so that another container can take its // place. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The attempt owns its deadline and its controllers, which starting, waiting, and telling a timeout from a failure all share, and releases them in one place. const attemptToStart = async ( client: ContainerRuntimeClient, options: StartContainerOptions, @@ -198,6 +199,7 @@ const attemptToStart = async ( // reachable within the startup timeout, gives way to another one, up to 3 // containers in total. If Docker fails, or if the container exits, starting // fails at once. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: Starting prepares Docker, runs the attempts one after another, and turns every failure into one error that names the failed removals as well. const startContainer = async (options: StartContainerOptions): Promise => { const removalErrors: unknown[] = []; const createError = (message: string, errorOptions?: ErrorOptions): Error => diff --git a/src/container/waitUntilReachable.ts b/src/container/waitUntilReachable.ts index 4da09e5..a2d155f 100644 --- a/src/container/waitUntilReachable.ts +++ b/src/container/waitUntilReachable.ts @@ -35,6 +35,7 @@ const readMappedPort = (container: ContainerInfo, port: number): number | undefi // Returns the mapped port once the database answers on it, and undefined // until then. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A poll checks the container, its port, and its database in turn, and each check can end the poll early. const poll = async ({ client, startId, port, signal }: Readiness): Promise => { const [container] = await listContainersOfAttempt(client, startId); if (container === undefined) { diff --git a/src/detectApiVersion.ts b/src/detectApiVersion.ts index c2e131a..30d7cd7 100644 --- a/src/detectApiVersion.ts +++ b/src/detectApiVersion.ts @@ -35,6 +35,7 @@ const detectApiVersion = async ({ url: URL; stallTimeout: number; }): Promise => + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The callback tells apart every answer that the ping can get, in the order in which they have to be checked. await runWithStallGuard({ stallTimeout }, async stallGuard => { const response = await stallGuard.send(new URL('/api/v2/system/ping', url).toString(), { method: 'GET', diff --git a/src/json/scanJson.ts b/src/json/scanJson.ts index 42ec0d3..1098121 100644 --- a/src/json/scanJson.ts +++ b/src/json/scanJson.ts @@ -38,6 +38,7 @@ const scanJsonString = (text: string, start: number): number => { throw new Error('Failed to scan JSON string.'); }; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The scan follows strings and the nesting depth in one loop, character by character. const scanJsonContainer = (text: string, start: number): number => { let depth = 0; let position = start; diff --git a/src/ndjson/readNdJsonStream.ts b/src/ndjson/readNdJsonStream.ts index 24805e5..a163512 100644 --- a/src/ndjson/readNdJsonStream.ts +++ b/src/ndjson/readNdJsonStream.ts @@ -7,6 +7,7 @@ interface NdJsonLine { value: Record; } +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator holds the reader, the buffer, and the abort listener, which its finally block has to release however the stream ends. const readNdJsonStream = async function* ( stream: ReadableStream, signal: AbortSignal, diff --git a/src/stall/readResponseText.ts b/src/stall/readResponseText.ts index cffe1a3..f0bd941 100644 --- a/src/stall/readResponseText.ts +++ b/src/stall/readResponseText.ts @@ -2,6 +2,7 @@ import type { StallGuard } from './StallGuard.js'; // Reads the body of a response as text, like response.text(), but restarts // the clock of the stall guard with every chunk that arrives. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: Reading restarts the stall guard before every chunk, and has to release the reader however reading ends. const readResponseText = async (response: Response, stallGuard: StallGuard): Promise => { if (!response.body) { return ''; diff --git a/src/v1/V1Transport.ts b/src/v1/V1Transport.ts index a19b46e..052b1d6 100644 --- a/src/v1/V1Transport.ts +++ b/src/v1/V1Transport.ts @@ -277,6 +277,7 @@ class V1Transport implements Transport { const apiToken = this.#apiToken; const stallTimeout = this.#stallTimeout; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator sends the query and handles every kind of line that its stream can contain, failing on any other. return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) { const response = await stallGuard.send(url, { method: 'POST', @@ -320,6 +321,7 @@ class V1Transport implements Transport { const apiToken = this.#apiToken; const stallTimeout = this.#stallTimeout; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator sends the request, handles every kind of line that its stream can contain, and fails if the stream ends, since observing never ends on its own. return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) { const response = await stallGuard.send(url, { method: 'POST', @@ -411,6 +413,7 @@ class V1Transport implements Transport { // only a string, and only to read the subject alone. const normalizedSubject = !recursive && isString(subject) ? normalizeSubject(subject) : subject; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator sends the request and handles every kind of line that its stream can contain, ending once it has read the subject alone. return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) { const response = await stallGuard.send(url, { method: 'POST', @@ -483,6 +486,7 @@ class V1Transport implements Transport { const apiToken = this.#apiToken; const stallTimeout = this.#stallTimeout; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator sends the request and handles every kind of line that its stream can contain, failing on any other. return streamWithStallGuard({ stallTimeout, signal }, async function* (stallGuard) { const response = await stallGuard.send(url, { method: 'POST', diff --git a/src/v2/readV2Stream.ts b/src/v2/readV2Stream.ts index 1b79de3..b1475ff 100644 --- a/src/v2/readV2Stream.ts +++ b/src/v2/readV2Stream.ts @@ -14,6 +14,7 @@ import type { V2Item } from './V2Item.js'; // broke off and fails, unless the caller aborted it. A server that answers // with 404 or 405 no longer offers the v2 API, so the client forgets which API // the server offers, and the stream fails as with any other status. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The generator checks the response, yields its items, and then tells whether the stream broke off, which depends on how the loop over the items ended. const readV2Stream = async function* ( response: Response, stallGuard: StallGuard, From 9c20d85f37d20c6f8906e68537f45993c7a1687b Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:57:41 +0100 Subject: [PATCH 10/11] chore: Hold the first probe and the requests of the v2 API in the rollback tests without nesting the conditions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/rollback.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/rollback.test.ts b/src/rollback.test.ts index f429cdc..81bccd5 100644 --- a/src/rollback.test.ts +++ b/src/rollback.test.ts @@ -312,17 +312,12 @@ suite('a server that is rolled back to a version without the v2 API', { timeout: let isProbeHeld = false; const respond = respondTo(readEventType); server.respond = (request: FakeRequest, response: ServerResponse): void => { - if (isProbe(request)) { - if (isProbeHeld) { - respond(request, response); - return; - } - + if (isProbe(request) && !isProbeHeld) { isProbeHeld = true; heldProbe.resolve(response); return; } - if (getApiVersion(request) === 'v2' && !offersV2) { + if (!isProbe(request) && getApiVersion(request) === 'v2' && !offersV2) { heldRequests.shift()?.resolve(response); return; } From c11fd7ec1f176e56c9429737371bcd2846d1ca1e Mon Sep 17 00:00:00 2001 From: Golo Roden Date: Sat, 31 Oct 2026 05:59:06 +0100 Subject: [PATCH 11/11] chore: Suppress noExcessiveStatementsPerFunction for suites, linear tests, and test helpers that are one unit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzFciB749GdjoE6dDqyCrY --- src/Event.test.ts | 4 ++++ src/abortSignal.test.ts | 2 ++ src/container/isReachable.test.ts | 1 + src/container/startContainer.test.ts | 6 ++++++ src/convertCloudEventToEvent.test.ts | 1 + src/detectApiVersion.test.ts | 1 + src/observeEvents.test.ts | 1 + src/preconditionOptions.test.ts | 2 ++ src/readEvents.test.ts | 6 ++++++ src/readSubjects.test.ts | 2 ++ src/readSubjectsOptions.test.ts | 1 + src/rollback.test.ts | 5 +++++ src/runEventQlQuery.test.ts | 1 + src/stallTimeout.test.ts | 2 ++ src/writeBeforeSending.test.ts | 1 + src/writeEvents.test.ts | 4 ++++ 16 files changed, 40 insertions(+) diff --git a/src/Event.test.ts b/src/Event.test.ts index b2f0175..efbb2c4 100644 --- a/src/Event.test.ts +++ b/src/Event.test.ts @@ -164,6 +164,7 @@ suite('Event', () => { test( 'throws an error if the hash verification fails.', containerStartTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { await container.stop(); const imageTag = getImageTag(); @@ -203,6 +204,7 @@ suite('Event', () => { test( 'throws an error if the signature verification fails.', containerStartTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { await container.stop(); const imageTag = getImageTag(); @@ -238,6 +240,7 @@ suite('Event', () => { }, ); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('verifies the signature.', containerStartTimeout, async (): Promise => { await container.stop(); const imageTag = getImageTag(); @@ -273,6 +276,7 @@ suite('Event', () => { test( 'verifies the signature of an event as written, read, and observed, whatever its data and trace context.', containerStartTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { await container.stop(); const imageTag = getImageTag(); diff --git a/src/abortSignal.test.ts b/src/abortSignal.test.ts index 6483ef7..9e62a1e 100644 --- a/src/abortSignal.test.ts +++ b/src/abortSignal.test.ts @@ -137,6 +137,7 @@ suite('abortSignal', { timeout: 30_000 }, () => { defineTestsFor(request); } + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('throws an abort error if the caller aborts while the response body arrives.', async (): Promise => { server.respond = (_request: FakeRequest, response: ServerResponse): void => { writeHead(response, 200, 'application/json'); @@ -173,6 +174,7 @@ suite('abortSignal', { timeout: 30_000 }, () => { await server.connectionsClosed(); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('throws an unknown outcome if the caller aborts a write while the response body arrives.', async (): Promise => { server.respond = (_request: FakeRequest, response: ServerResponse): void => { writeHead(response, 200, 'application/json'); diff --git a/src/container/isReachable.test.ts b/src/container/isReachable.test.ts index a58f6e7..37d4f18 100644 --- a/src/container/isReachable.test.ts +++ b/src/container/isReachable.test.ts @@ -13,6 +13,7 @@ interface Answer { delay?: number; } +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('isReachable', () => { let server: FakeServer; diff --git a/src/container/startContainer.test.ts b/src/container/startContainer.test.ts index a7277bb..d818913 100644 --- a/src/container/startContainer.test.ts +++ b/src/container/startContainer.test.ts @@ -47,6 +47,7 @@ const listContainers = async (testId: string): Promise => { // instead of asking for its events afterwards. Docker passes on its events in // order, so to stop, the test creates a container of its own and waits for its // event, after which the events of all containers created before have arrived. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: Watching opens the event stream of Docker, collects the start labels from it, and returns the function that ends it, all of which share the stream. const watchCreatedContainers = async ( testId: string, image: string, @@ -98,6 +99,7 @@ const watchCreatedContainers = async ( }; }; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('startContainer', () => { const image = `thenativeweb/eventsourcingdb:${getImageTag()}`; @@ -256,6 +258,7 @@ suite('startContainer', () => { test( 'replaces a running container whose port testcontainers cannot find, and removes it.', integrationTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { const testId = randomUUID(); const endFailing = await failPortLookups(1); @@ -327,6 +330,7 @@ suite('startContainer', () => { test( 'does not count pulling the image towards the startup timeout.', integrationTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { const testId = randomUUID(); const client = await getContainerRuntimeClient(); @@ -377,6 +381,7 @@ suite('startContainer', () => { test( 'removes a container that Docker creates only after the wait for it has ended, without waiting for it.', integrationTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { const testId = randomUUID(); const client = await getContainerRuntimeClient(); @@ -439,6 +444,7 @@ suite('startContainer', () => { test( 'reports a failed removal after the error of the start, and goes on with the next attempt.', integrationTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { const testId = randomUUID(); const client = await getContainerRuntimeClient(); diff --git a/src/convertCloudEventToEvent.test.ts b/src/convertCloudEventToEvent.test.ts index cb53cb3..fb4e101 100644 --- a/src/convertCloudEventToEvent.test.ts +++ b/src/convertCloudEventToEvent.test.ts @@ -4,6 +4,7 @@ import type { CloudEvent } from './CloudEvent.js'; import { convertCloudEventToEvent } from './convertCloudEventToEvent.js'; suite('convertCloudEventToEvent', () => { + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('converts a cloud event to an event.', () => { const now = Date.now(); diff --git a/src/detectApiVersion.test.ts b/src/detectApiVersion.test.ts index 41d43cd..b951e7a 100644 --- a/src/detectApiVersion.test.ts +++ b/src/detectApiVersion.test.ts @@ -33,6 +33,7 @@ interface FailedProbe { const expectedEventType: EventType = { type: 'io.eventsourcingdb.test', hasEvents: true }; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('detectApiVersion', { timeout: 30_000 }, () => { let server: FakeServer; let client: Client; diff --git a/src/observeEvents.test.ts b/src/observeEvents.test.ts index 9e731c7..9fc7ee2 100644 --- a/src/observeEvents.test.ts +++ b/src/observeEvents.test.ts @@ -12,6 +12,7 @@ import { integrationTimeout } from './testing/integrationTimeout.js'; // arrived, and fails if they have not arrived within this many milliseconds. const observeTimeout = 10_000; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The helper has to clear its timer however observing ends, and only then checks whether enough events arrived. const observeUntil = async ( observe: (signal: AbortSignal) => AsyncGenerator, expectedCount: number, diff --git a/src/preconditionOptions.test.ts b/src/preconditionOptions.test.ts index 139eaed..7cc9a59 100644 --- a/src/preconditionOptions.test.ts +++ b/src/preconditionOptions.test.ts @@ -100,6 +100,7 @@ const isIncompatibleServerError = (error: unknown): boolean => error.name === 'IncompatibleServerError' && error.message === 'The server does not offer the v2 API, which a recursive precondition needs.'; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('preconditionOptions', { timeout: 30_000 }, () => { let server: FakeServer; let client: Client; @@ -328,6 +329,7 @@ suite('preconditionOptions', { timeout: 30_000 }, () => { assert.deepEqual(getPaths(), ['/api/v1/verify-api-token', '/api/v1/verify-api-token']); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('goes on with the v1 API for every other request while it finds out again, and uses the v2 API once it finds it.', async (): Promise => { const probe = Promise.withResolvers(); heldProbe = probe; diff --git a/src/readEvents.test.ts b/src/readEvents.test.ts index abfaabf..cecbdfe 100644 --- a/src/readEvents.test.ts +++ b/src/readEvents.test.ts @@ -7,6 +7,7 @@ import { getImageTag } from './getImageTag.js'; import { containerStartTimeout } from './testing/containerStartTimeout.js'; import { integrationTimeout } from './testing/integrationTimeout.js'; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('readEvents', () => { let container: Container; @@ -99,6 +100,7 @@ suite('readEvents', () => { assert.equal(eventsRead.length, 2); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads chronologically.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -138,6 +140,7 @@ suite('readEvents', () => { assert.equal(secondEventRead.data.value, 42); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads anti-chronologically.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -177,6 +180,7 @@ suite('readEvents', () => { assert.equal(secondEventRead.data.value, 23); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads with lower bound.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -214,6 +218,7 @@ suite('readEvents', () => { assert.equal(eventRead.data.value, 42); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads with upper bound.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -251,6 +256,7 @@ suite('readEvents', () => { assert.equal(eventRead.data.value, 23); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads from latest event.', integrationTimeout, async (): Promise => { const client = container.getClient(); diff --git a/src/readSubjects.test.ts b/src/readSubjects.test.ts index fd06bad..06cf549 100644 --- a/src/readSubjects.test.ts +++ b/src/readSubjects.test.ts @@ -34,6 +34,7 @@ suite('readSubjects', () => { }, ); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads all subjects.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -69,6 +70,7 @@ suite('readSubjects', () => { assert.equal(subjectsRead[3], '/test/2'); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads all subjects from the subject.', integrationTimeout, async (): Promise => { const client = container.getClient(); diff --git a/src/readSubjectsOptions.test.ts b/src/readSubjectsOptions.test.ts index 3c9e369..8e4876e 100644 --- a/src/readSubjectsOptions.test.ts +++ b/src/readSubjectsOptions.test.ts @@ -30,6 +30,7 @@ suite('readSubjects', { timeout: 30_000 }, () => { } }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('v1', () => { let server: FakeServer; let client: Client; diff --git a/src/rollback.test.ts b/src/rollback.test.ts index 81bccd5..f185fd5 100644 --- a/src/rollback.test.ts +++ b/src/rollback.test.ts @@ -170,6 +170,7 @@ const keptAnswers: KeptAnswer[] = [ ), ]; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('a server that is rolled back to a version without the v2 API', { timeout: 30_000 }, () => { let server: FakeServer; let client: Client; @@ -227,6 +228,7 @@ suite('a server that is rolled back to a version without the v2 API', { timeout: }; const defineKeepTest = (call: Call, { name, status, answer }: KeptAnswer): void => { + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test(`keeps the API after ${name}.`, async (): Promise => { server.respond = respondTo(call); await call.run(client); @@ -267,6 +269,7 @@ suite('a server that is rolled back to a version without the v2 API', { timeout: }); } + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('keeps the API after the status 404 of the v1 API, which it uses once the server is rolled back.', async (): Promise => { server.respond = respondTo(readEventType); await readEventType.run(client); @@ -339,6 +342,7 @@ suite('a server that is rolled back to a version without the v2 API', { timeout: return [result, await held.promise]; }; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('shares the probe, and keeps its answer, if requests of the v2 API that started before fail with 404 meanwhile.', async (): Promise => { const [failsFirst, firstResponse] = await sendHeld(); const [failsDuringProbe, duringProbeResponse] = await sendHeld(); @@ -373,6 +377,7 @@ suite('a server that is rolled back to a version without the v2 API', { timeout: ]); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('keeps the answer of the probe if it finds the v2 API again, and a request of the v2 API that started before fails with 404 afterwards.', async (): Promise => { const [failsFirst, firstResponse] = await sendHeld(); const [failsLate, lateResponse] = await sendHeld(); diff --git a/src/runEventQlQuery.test.ts b/src/runEventQlQuery.test.ts index f9dd307..ce07f9a 100644 --- a/src/runEventQlQuery.test.ts +++ b/src/runEventQlQuery.test.ts @@ -35,6 +35,7 @@ suite('runEventQlQuery', () => { }, ); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('reads all rows the query returns.', integrationTimeout, async (): Promise => { const client = container.getClient(); diff --git a/src/stallTimeout.test.ts b/src/stallTimeout.test.ts index b5c5ab9..9780c67 100644 --- a/src/stallTimeout.test.ts +++ b/src/stallTimeout.test.ts @@ -208,6 +208,7 @@ suite('stallTimeout', { timeout: 30_000 }, () => { for (const apiVersion of apiVersions) { const { event, row, heartbeat, end } = lines[apiVersion]; + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite(apiVersion, () => { let server: FakeServer; let client: Client; @@ -256,6 +257,7 @@ suite('stallTimeout', { timeout: 30_000 }, () => { test('ends a stream with heartbeats once nothing arrives for 30 seconds by default.', { timeout: 5000, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. }, async (context): Promise => { server.respond = (_request: FakeRequest, response: ServerResponse): void => { writeHead(response, 200, ndJson); diff --git a/src/writeBeforeSending.test.ts b/src/writeBeforeSending.test.ts index 3ae18e9..cb6474f 100644 --- a/src/writeBeforeSending.test.ts +++ b/src/writeBeforeSending.test.ts @@ -58,6 +58,7 @@ const isAbortError = (error: unknown): boolean => // however it fails, so it fails with the error itself. The server accepts the // connection, but never answers the TLS handshake, so the request never gets // beyond establishing the connection. +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('a write that fails before its request went out', { timeout: 30_000 }, () => { let server: net.Server; let sockets: net.Socket[]; diff --git a/src/writeEvents.test.ts b/src/writeEvents.test.ts index ece176a..3149708 100644 --- a/src/writeEvents.test.ts +++ b/src/writeEvents.test.ts @@ -105,6 +105,7 @@ const preconditionOptionsCases: PreconditionOptionsCase[] = [ }, ]; +// biome-ignore lint/nursery/noExcessiveStatementsPerFunction: A suite counts each of its tests as a statement. suite('writeEvents', () => { let container: Container; @@ -138,6 +139,7 @@ suite('writeEvents', () => { assert.equal(writtenEvent.id, '0'); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('writes multiple events.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -173,6 +175,7 @@ suite('writeEvents', () => { assert.equal(secondWrittenEvent.data.value, 42); }); + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. test('writes the trace context of an event.', integrationTimeout, async (): Promise => { const client = container.getClient(); @@ -247,6 +250,7 @@ suite('writeEvents', () => { test( 'supports the isSubjectPopulated precondition.', integrationTimeout, + // biome-ignore lint/nursery/noExcessiveStatementsPerFunction: The test is one linear sequence of steps, and splitting it would make it harder to follow. async (): Promise => { const client = container.getClient();