@jterrazz/test 16.1.1 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +2 -2980
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4534 -2628
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3004
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6127 -5693
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +373 -525
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -248
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -332
- package/docs/19-linting.md +0 -512
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TestProject } from "vitest/node";
|
|
2
|
+
//#region src/specifications/api/routes/ledger.d.ts
|
|
3
|
+
type Shard = {
|
|
4
|
+
count: number;
|
|
5
|
+
index: number;
|
|
6
|
+
};
|
|
7
|
+
declare module 'vitest' {
|
|
8
|
+
interface ProvidedContext {
|
|
9
|
+
routesLedger?: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/specifications/api/routes/route-coverage.global-setup.d.ts
|
|
14
|
+
/** Judged only on a whole green run: a shard, a `-t` filter, `--changed` or a failed file is no count. The ratchet is exact both ways; an app with no known route table fails the run. */
|
|
15
|
+
export type RouteProject = {
|
|
16
|
+
config: {
|
|
17
|
+
root: string;
|
|
18
|
+
};
|
|
19
|
+
globTestFiles: () => Promise<{
|
|
20
|
+
testFiles: string[];
|
|
21
|
+
}>;
|
|
22
|
+
name: string;
|
|
23
|
+
provide: (key: 'routesLedger', value: string) => void;
|
|
24
|
+
vitest: {
|
|
25
|
+
config: {
|
|
26
|
+
shard?: Shard | undefined;
|
|
27
|
+
testNamePattern?: RegExp | undefined;
|
|
28
|
+
};
|
|
29
|
+
state: {
|
|
30
|
+
getFailedFilepaths: () => string[];
|
|
31
|
+
getFiles: () => readonly {
|
|
32
|
+
filepath: string;
|
|
33
|
+
projectName?: string | undefined;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export declare function routeCoverageSetup(): string;
|
|
39
|
+
export declare function setupRoutes(project: RouteProject): () => Promise<void>;
|
|
40
|
+
export default function setup(project: TestProject): () => Promise<void>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//# sourceMappingURL=route-coverage.global-setup.d.ts.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { C as IsolationStrategy, S as ServiceHandle, a as processService, b as ServiceRecord, c as RedisOptions, d as postgres, i as ProcessOptions, l as redis, n as SeedSource, o as sqlite, r as ProcessHandle, s as SqliteOptions, u as PostgresOptions, w as DatabasePort, y as DatabaseKeys } from "./shared-index.services.js";
|
|
2
|
+
import { n as ContainerPort, t as ContainerOptions } from "./shared-container.port.js";
|
|
3
|
+
export { type ContainerOptions, type ContainerPort, type DatabaseKeys, type DatabasePort, type IsolationStrategy, type PostgresOptions, ProcessHandle, type ProcessOptions, type RedisOptions, type SeedSource, type ServiceHandle, type ServiceRecord, type SqliteOptions, postgres, processService as process, redis, sqlite };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { n as ContainerPort, r as ContainerRequest } from "./shared-container.port.js";
|
|
2
|
+
import { ContainerRuntimeClient } from "testcontainers";
|
|
3
|
+
//#region src/services/containers/run-containers.d.ts
|
|
4
|
+
/** Two requests alike (image, port, environment, options) share one container; the run stops them all at teardown. */
|
|
5
|
+
type RunAddress = {
|
|
6
|
+
host: string;
|
|
7
|
+
key: string;
|
|
8
|
+
port: number;
|
|
9
|
+
};
|
|
10
|
+
declare class RunContainers {
|
|
11
|
+
private readonly started;
|
|
12
|
+
private readonly create;
|
|
13
|
+
private readonly ready;
|
|
14
|
+
/** `ready`'s rejection is the one answer every request gets, and no container is created. */
|
|
15
|
+
constructor(create: (request: ContainerRequest) => ContainerPort, ready?: () => Promise<void>);
|
|
16
|
+
acquire(request: ContainerRequest): Promise<RunAddress>;
|
|
17
|
+
logs(key: string, since?: Date): Promise<string>;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
private start;
|
|
20
|
+
}
|
|
21
|
+
type ServedContainers = {
|
|
22
|
+
close: () => Promise<void>;
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
declare function serveContainers(containers: RunContainers): Promise<ServedContainers>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/services/containers/services.global-setup.d.ts
|
|
28
|
+
/** Every project's setup joins the one run's containers. Default images are pulled in the background, so the first file waits on a pull at most once; a project declaring no service needs no Docker. */
|
|
29
|
+
export declare const DOCKER_ABSENT = "no Docker daemon answered";
|
|
30
|
+
/** `connect` stands in for the runtime's own client lookup, so the refusal is reachable without a daemon to stop. */
|
|
31
|
+
export declare function prepareDocker(connect?: () => Promise<ContainerRuntimeClient>): Promise<void>;
|
|
32
|
+
export declare function servicesSetup(): string;
|
|
33
|
+
type RunProject = {
|
|
34
|
+
provide: (key: 'serviceContainers', value: string) => void;
|
|
35
|
+
};
|
|
36
|
+
type RunSeams = {
|
|
37
|
+
create: (request: ContainerRequest) => ContainerPort;
|
|
38
|
+
prepare: () => Promise<void>;
|
|
39
|
+
serve: typeof serveContainers;
|
|
40
|
+
};
|
|
41
|
+
export default function setup(project: RunProject, seams?: Partial<RunSeams>): Promise<() => Promise<void>>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//# sourceMappingURL=services.global-setup.d.ts.map
|
package/dist/services.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { t as discoverRoot } from "./shared-resolve.js";
|
|
2
|
+
import { n as refusal, t as invariant } from "./shared-refusal.js";
|
|
3
|
+
import { t as pause } from "./shared-pause.js";
|
|
4
|
+
import { n as processService, t as ProcessHandle } from "./shared-process.adapter.js";
|
|
5
|
+
import { n as requireBuiltPeer, t as loadPeer } from "./shared-peer.js";
|
|
6
|
+
import { c as SQLITE_TEMPLATE_DIR } from "./shared-artifacts.js";
|
|
7
|
+
import { t as SERVICE_IMAGES } from "./shared-service-images.js";
|
|
8
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
//#region src/services/driver-reason.ts
|
|
13
|
+
/** What a database driver says went wrong: its message, else its code; a refused connection is an `AggregateError` with an empty message and `ECONNREFUSED`. */
|
|
14
|
+
function driverReason(error) {
|
|
15
|
+
if (error instanceof Error && error.message !== "") return error.message;
|
|
16
|
+
const code = Reflect.get(new Object(error), "code");
|
|
17
|
+
return typeof code === "string" && code !== "" ? code : String(error);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/services/postgres/template.ts
|
|
21
|
+
/** An optional peer: a repository that never connects never pays for the import. */
|
|
22
|
+
async function clientConstructor() {
|
|
23
|
+
return await loadPeer("pg", "postgres()", async () => {
|
|
24
|
+
return (await import("pg")).Client;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function endQuietly(client) {
|
|
28
|
+
try {
|
|
29
|
+
await client?.end();
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
function withDatabase$1(connectionString, database) {
|
|
33
|
+
const url = new URL(connectionString);
|
|
34
|
+
url.pathname = `/${database}`;
|
|
35
|
+
return url.toString();
|
|
36
|
+
}
|
|
37
|
+
/** Unique among one container's clones and the same on every run, so a clone a crashed run left is dropped by the next worker of that name. */
|
|
38
|
+
function cloneName(base, service, workerId) {
|
|
39
|
+
return `${base}_${service}_${workerId}`.toLowerCase().replaceAll(/[^a-z0-9_]/gu, "_");
|
|
40
|
+
}
|
|
41
|
+
async function withClient(connectionString, work) {
|
|
42
|
+
const client = new (await (clientConstructor()))({ connectionString });
|
|
43
|
+
client.on("error", () => {});
|
|
44
|
+
await client.connect();
|
|
45
|
+
try {
|
|
46
|
+
return await work(client);
|
|
47
|
+
} finally {
|
|
48
|
+
await endQuietly(client);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Named by the script's digest; built under an advisory lock, marked `IS_TEMPLATE` only once the script ran whole, a crash's unmarked one rebuilt. */
|
|
52
|
+
async function buildTemplate(connectionString, sql, initPath) {
|
|
53
|
+
const template = `template_${createHash("sha256").update(sql).digest("hex").slice(0, 16)}`;
|
|
54
|
+
return await withClient(connectionString, async (admin) => {
|
|
55
|
+
await admin.query("SELECT pg_advisory_lock(hashtext($1))", [template]);
|
|
56
|
+
try {
|
|
57
|
+
if ((await admin.query("SELECT datistemplate FROM pg_database WHERE datname = $1", [template])).rows[0]?.datistemplate === true) return template;
|
|
58
|
+
await admin.query(`DROP DATABASE IF EXISTS "${template}" WITH (FORCE)`);
|
|
59
|
+
await admin.query(`CREATE DATABASE "${template}"`);
|
|
60
|
+
try {
|
|
61
|
+
await withClient(withDatabase$1(connectionString, template), async (builder) => {
|
|
62
|
+
await builder.query(sql);
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
await admin.query(`DROP DATABASE IF EXISTS "${template}" WITH (FORCE)`);
|
|
66
|
+
throw new Error(refusal({
|
|
67
|
+
fix: `correct ${initPath}: the SQL fails as written`,
|
|
68
|
+
found: `the postgres init script ${initPath} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
69
|
+
read: "docs/reference/services.md#dockerserviceinitsql",
|
|
70
|
+
why: "A postgres service's template database is built by its init script, run whole."
|
|
71
|
+
}), { cause: error });
|
|
72
|
+
}
|
|
73
|
+
await admin.query(`ALTER DATABASE "${template}" IS_TEMPLATE true`);
|
|
74
|
+
return template;
|
|
75
|
+
} finally {
|
|
76
|
+
await admin.query("SELECT pg_advisory_unlock(hashtext($1))", [template]);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/services/postgres/postgres.adapter.ts
|
|
82
|
+
const DISPOSABLE = {
|
|
83
|
+
command: [
|
|
84
|
+
"postgres",
|
|
85
|
+
"-c",
|
|
86
|
+
"fsync=off",
|
|
87
|
+
"-c",
|
|
88
|
+
"synchronous_commit=off",
|
|
89
|
+
"-c",
|
|
90
|
+
"full_page_writes=off",
|
|
91
|
+
"-c",
|
|
92
|
+
"log_checkpoints=off"
|
|
93
|
+
],
|
|
94
|
+
tmpfs: { "/var/lib/postgresql/data": "rw" }
|
|
95
|
+
};
|
|
96
|
+
var PostgresHandle = class {
|
|
97
|
+
type = "postgres";
|
|
98
|
+
serviceName = null;
|
|
99
|
+
defaultPort = 5432;
|
|
100
|
+
defaultImage;
|
|
101
|
+
environment;
|
|
102
|
+
containerOptions = DISPOSABLE;
|
|
103
|
+
connectionString = "";
|
|
104
|
+
client = null;
|
|
105
|
+
template = null;
|
|
106
|
+
clone = null;
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.defaultImage = options.image ?? SERVICE_IMAGES.postgres;
|
|
109
|
+
this.environment = {
|
|
110
|
+
POSTGRES_DB: "test",
|
|
111
|
+
POSTGRES_PASSWORD: "test",
|
|
112
|
+
POSTGRES_USER: "test",
|
|
113
|
+
...options.env
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
buildConnectionString(host, port) {
|
|
117
|
+
return `postgresql://${this.environment.POSTGRES_USER ?? "test"}:${this.environment.POSTGRES_PASSWORD ?? "test"}@${host}:${port}/${this.environment.POSTGRES_DB ?? "test"}`;
|
|
118
|
+
}
|
|
119
|
+
createDatabaseAdapter() {
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
async healthcheck() {
|
|
123
|
+
if (!this.connectionString) throw invariant("postgres() was healthchecked before it had a connection string");
|
|
124
|
+
try {
|
|
125
|
+
const client = new (await (clientConstructor()))({ connectionString: this.connectionString });
|
|
126
|
+
await client.connect();
|
|
127
|
+
await client.query("SELECT 1");
|
|
128
|
+
await client.end();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(refusal({
|
|
131
|
+
fix: "check the container is running and reachable (`docker ps`), and run again",
|
|
132
|
+
found: `postgres() did not answer its healthcheck: ${driverReason(error)}`,
|
|
133
|
+
read: "docs/reference/services.md#service-factories",
|
|
134
|
+
why: "A declared service is handed to the chain once it answers its healthcheck."
|
|
135
|
+
}), { cause: error });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async initialize(dockerDir) {
|
|
139
|
+
const initPaths = [...this.serviceName === null ? [] : [resolve(dockerDir, `${this.serviceName}/init.sql`)], resolve(dockerDir, "postgres/init.sql")];
|
|
140
|
+
for (const initPath of initPaths) if (existsSync(initPath)) {
|
|
141
|
+
this.template = await buildTemplate(this.connectionString, readFileSync(initPath, "utf8"), initPath);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async getClient() {
|
|
146
|
+
if (this.client) return this.client;
|
|
147
|
+
const client = new (await (clientConstructor()))({ connectionString: this.connectionString });
|
|
148
|
+
client.on("error", () => {
|
|
149
|
+
this.client = null;
|
|
150
|
+
});
|
|
151
|
+
await client.connect();
|
|
152
|
+
this.client = client;
|
|
153
|
+
return client;
|
|
154
|
+
}
|
|
155
|
+
async seed(sql) {
|
|
156
|
+
await (await this.getClient()).query(sql);
|
|
157
|
+
}
|
|
158
|
+
async reset() {
|
|
159
|
+
const client = await this.getClient();
|
|
160
|
+
const tables = (await client.query(`
|
|
161
|
+
SELECT tablename FROM pg_tables
|
|
162
|
+
WHERE schemaname = 'public'
|
|
163
|
+
AND tablename NOT LIKE '_prisma%'
|
|
164
|
+
`)).rows.map(({ tablename }) => `"public"."${tablename}"`);
|
|
165
|
+
if (tables.length > 0) await client.query(`TRUNCATE ${tables.join(", ")} RESTART IDENTITY CASCADE`);
|
|
166
|
+
}
|
|
167
|
+
async query(table, columns) {
|
|
168
|
+
const client = await this.getClient();
|
|
169
|
+
const columnList = columns.join(", ");
|
|
170
|
+
return (await client.query(`SELECT ${columnList} FROM "public"."${table}" ORDER BY 1`)).rows.map((row) => columns.map((col) => row[col]));
|
|
171
|
+
}
|
|
172
|
+
async columns(table) {
|
|
173
|
+
return (await (await this.getClient()).query("SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position", ["public", table])).rows.map((row) => row.column_name);
|
|
174
|
+
}
|
|
175
|
+
isolation() {
|
|
176
|
+
return {
|
|
177
|
+
acquire: async (workerId) => {
|
|
178
|
+
const database = cloneName(this.environment.POSTGRES_DB ?? "test", this.serviceName ?? this.type, workerId);
|
|
179
|
+
await withClient(this.connectionString, async (admin) => {
|
|
180
|
+
await admin.query(`DROP DATABASE IF EXISTS "${database}" WITH (FORCE)`);
|
|
181
|
+
await admin.query(`CREATE DATABASE "${database}" TEMPLATE "${this.template ?? "template1"}"`);
|
|
182
|
+
});
|
|
183
|
+
await this.closeClient();
|
|
184
|
+
this.clone = {
|
|
185
|
+
container: this.connectionString,
|
|
186
|
+
database
|
|
187
|
+
};
|
|
188
|
+
this.connectionString = withDatabase$1(this.connectionString, database);
|
|
189
|
+
},
|
|
190
|
+
reset: async () => {
|
|
191
|
+
await this.reset();
|
|
192
|
+
},
|
|
193
|
+
release: async () => {
|
|
194
|
+
await this.closeClient();
|
|
195
|
+
const { clone } = this;
|
|
196
|
+
if (clone === null) return;
|
|
197
|
+
this.connectionString = clone.container;
|
|
198
|
+
this.clone = null;
|
|
199
|
+
await withClient(this.connectionString, async (admin) => {
|
|
200
|
+
await admin.query(`DROP DATABASE IF EXISTS "${clone.database}" WITH (FORCE)`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async closeClient() {
|
|
206
|
+
const { client } = this;
|
|
207
|
+
this.client = null;
|
|
208
|
+
await endQuietly(client);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
/** Create a PostgreSQL service handle; its record key names the init script folder (`docker/<key>/init.sql`). */
|
|
212
|
+
function postgres(options = {}) {
|
|
213
|
+
return new PostgresHandle(options);
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/services/redis/redis.adapter.ts
|
|
217
|
+
const DATABASES = 256;
|
|
218
|
+
const SERVICES_PER_WORKER = 4;
|
|
219
|
+
const WORKERS = Math.floor(255 / SERVICES_PER_WORKER);
|
|
220
|
+
const PAIR_INDEXES = { command: [
|
|
221
|
+
"redis-server",
|
|
222
|
+
"--databases",
|
|
223
|
+
String(DATABASES)
|
|
224
|
+
] };
|
|
225
|
+
const slots = /* @__PURE__ */ new Map();
|
|
226
|
+
function slotsOf(worker) {
|
|
227
|
+
const held = slots.get(worker) ?? /* @__PURE__ */ new Map();
|
|
228
|
+
slots.set(worker, held);
|
|
229
|
+
return held;
|
|
230
|
+
}
|
|
231
|
+
function takeSlot(held, service) {
|
|
232
|
+
const pair = held.get(service) ?? {
|
|
233
|
+
holders: 0,
|
|
234
|
+
slot: lowestFree(held)
|
|
235
|
+
};
|
|
236
|
+
pair.holders += 1;
|
|
237
|
+
held.set(service, pair);
|
|
238
|
+
return {
|
|
239
|
+
free: () => {
|
|
240
|
+
pair.holders -= 1;
|
|
241
|
+
if (pair.holders === 0) held.delete(service);
|
|
242
|
+
},
|
|
243
|
+
slot: pair.slot
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function lowestFree(held) {
|
|
247
|
+
const taken = new Set([...held.values()].map((pair) => pair.slot));
|
|
248
|
+
let slot = 0;
|
|
249
|
+
while (taken.has(slot)) slot += 1;
|
|
250
|
+
return slot;
|
|
251
|
+
}
|
|
252
|
+
async function redisDriver() {
|
|
253
|
+
return await loadPeer("redis", "redis()", async () => await import("redis"));
|
|
254
|
+
}
|
|
255
|
+
var RedisHandle = class {
|
|
256
|
+
type = "redis";
|
|
257
|
+
serviceName = null;
|
|
258
|
+
defaultPort = 6379;
|
|
259
|
+
defaultImage;
|
|
260
|
+
environment = {};
|
|
261
|
+
containerOptions = PAIR_INDEXES;
|
|
262
|
+
connectionString = "";
|
|
263
|
+
dbIndex = 0;
|
|
264
|
+
/** What this handle holds: its slot's release, and the container's own string it extended. */
|
|
265
|
+
pair = null;
|
|
266
|
+
constructor(options = {}) {
|
|
267
|
+
this.defaultImage = options.image ?? SERVICE_IMAGES.redis;
|
|
268
|
+
}
|
|
269
|
+
buildConnectionString(host, port) {
|
|
270
|
+
return `redis://${host}:${port}`;
|
|
271
|
+
}
|
|
272
|
+
createDatabaseAdapter() {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
async healthcheck() {
|
|
276
|
+
if (!this.connectionString) throw invariant("redis() was healthchecked before it had a connection string");
|
|
277
|
+
try {
|
|
278
|
+
const { createClient } = await redisDriver();
|
|
279
|
+
const client = createClient({ url: this.connectionString });
|
|
280
|
+
await client.connect();
|
|
281
|
+
await client.ping();
|
|
282
|
+
client.destroy();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new Error(refusal({
|
|
285
|
+
fix: "check the container is running and reachable (`docker ps`), and run again",
|
|
286
|
+
found: `redis() did not answer its healthcheck: ${driverReason(error)}`,
|
|
287
|
+
read: "docs/reference/services.md#service-factories",
|
|
288
|
+
why: "A declared service is handed to the chain once it answers its healthcheck."
|
|
289
|
+
}), { cause: error });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async initialize() {}
|
|
293
|
+
async reset() {
|
|
294
|
+
const { createClient } = await redisDriver();
|
|
295
|
+
const client = createClient({
|
|
296
|
+
url: this.connectionString,
|
|
297
|
+
database: this.dbIndex
|
|
298
|
+
});
|
|
299
|
+
await client.connect();
|
|
300
|
+
try {
|
|
301
|
+
await client.flushDb();
|
|
302
|
+
} finally {
|
|
303
|
+
client.destroy();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
isolation() {
|
|
307
|
+
return {
|
|
308
|
+
acquire: async (workerId) => {
|
|
309
|
+
this.letGo();
|
|
310
|
+
const worker = Number.parseInt(workerId, 10) || 0;
|
|
311
|
+
const service = this.serviceName ?? this.type;
|
|
312
|
+
const { free, slot } = takeSlot(slotsOf(workerId), service);
|
|
313
|
+
if (slot >= SERVICES_PER_WORKER || worker > WORKERS) {
|
|
314
|
+
free();
|
|
315
|
+
throw new Error(refusal({
|
|
316
|
+
fix: slot >= SERVICES_PER_WORKER ? `declare fewer redis services: a worker holds ${SERVICES_PER_WORKER} at once` : `run with fewer workers (--maxWorkers): a container serves ${WORKERS}`,
|
|
317
|
+
found: `redis: worker ${workerId} has no database index for "${service}"`,
|
|
318
|
+
read: "docs/reference/services.md#per-worker-isolation",
|
|
319
|
+
why: "A redis container's indexes are shared out four per worker, so its workers and their services are counted."
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
this.pair = {
|
|
323
|
+
free,
|
|
324
|
+
shared: this.connectionString
|
|
325
|
+
};
|
|
326
|
+
this.dbIndex = 1 + SERVICES_PER_WORKER * (Math.max(worker, 1) - 1) + slot;
|
|
327
|
+
this.connectionString = `${this.connectionString}/${this.dbIndex}`;
|
|
328
|
+
await this.reset();
|
|
329
|
+
},
|
|
330
|
+
reset: async () => {
|
|
331
|
+
await this.reset();
|
|
332
|
+
},
|
|
333
|
+
release: async () => {
|
|
334
|
+
try {
|
|
335
|
+
await this.reset();
|
|
336
|
+
} finally {
|
|
337
|
+
this.letGo();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
letGo() {
|
|
343
|
+
if (this.pair === null) return;
|
|
344
|
+
this.pair.free();
|
|
345
|
+
this.connectionString = this.pair.shared;
|
|
346
|
+
this.pair = null;
|
|
347
|
+
this.dbIndex = 0;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
/** Create a Redis service handle, reported under its record key. */
|
|
351
|
+
function redis(options = {}) {
|
|
352
|
+
return new RedisHandle(options);
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/services/sqlite/sqlite-template.ts
|
|
356
|
+
const SQLITE_FILE_HEADER = Buffer.from("SQLite format 3\0");
|
|
357
|
+
function withDescriptor(path, flags, use) {
|
|
358
|
+
const fd = openSync(path, flags);
|
|
359
|
+
try {
|
|
360
|
+
return use(fd);
|
|
361
|
+
} finally {
|
|
362
|
+
closeSync(fd);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** Checks the header only: enough to reject a 0-byte or truncated leftover. */
|
|
366
|
+
function isValidSqliteTemplate(path) {
|
|
367
|
+
try {
|
|
368
|
+
return withDescriptor(path, "r", (fd) => {
|
|
369
|
+
const header = Buffer.alloc(SQLITE_FILE_HEADER.length);
|
|
370
|
+
return readSync(fd, header, 0, header.length, 0) === SQLITE_FILE_HEADER.length && header.equals(SQLITE_FILE_HEADER);
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const PRISMA_PUSH = "npx prisma db push --force-reset";
|
|
377
|
+
/** `--schema` is required: without it Prisma's own discovery ignores the option, and only a machine without a cached template fails. */
|
|
378
|
+
function prismaPushCommand(prismaSchema) {
|
|
379
|
+
if (prismaSchema === null) return PRISMA_PUSH;
|
|
380
|
+
return `${PRISMA_PUSH} --schema ${JSON.stringify(resolve(prismaSchema))}`;
|
|
381
|
+
}
|
|
382
|
+
const TEMPLATE_PREFIX = "template";
|
|
383
|
+
const LOCK_STALE_MS = 12e4;
|
|
384
|
+
function schemaBytes(path) {
|
|
385
|
+
try {
|
|
386
|
+
if (statSync(path).isDirectory()) return readdirSync(path).toSorted().map((entry) => schemaBytes(join(path, entry))).join("\0");
|
|
387
|
+
return `${path}\0${readFileSync(path, "utf8")}`;
|
|
388
|
+
} catch {
|
|
389
|
+
return `${path}\0<unreadable>`;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Editing a schema builds a new template: the stale file is header-valid and would never be rebuilt. */
|
|
393
|
+
function sqliteTemplateName(options = {}) {
|
|
394
|
+
const [source, path] = options.prismaSchema === void 0 ? ["init", options.init] : ["prisma", options.prismaSchema];
|
|
395
|
+
const fingerprint = path === void 0 ? "empty" : `${source}\0${resolve(path)}\0${schemaBytes(resolve(path))}`;
|
|
396
|
+
const key = createHash("sha256").update(fingerprint).digest("hex").slice(0, 8);
|
|
397
|
+
return `${TEMPLATE_PREFIX}-${key}.sqlite`;
|
|
398
|
+
}
|
|
399
|
+
/** Per project, never the OS tmpdir: two checkouts sharing one template inherit each other's schema. */
|
|
400
|
+
function sqliteTemplateDir(root) {
|
|
401
|
+
const dir = resolve(root, SQLITE_TEMPLATE_DIR);
|
|
402
|
+
mkdirSync(dir, { recursive: true });
|
|
403
|
+
return dir;
|
|
404
|
+
}
|
|
405
|
+
/** `wx` creates and claims the file in one syscall: two workers cannot both win and run `prisma db push` on one file. */
|
|
406
|
+
function acquireTemplateLock(lockPath) {
|
|
407
|
+
try {
|
|
408
|
+
withDescriptor(lockPath, "wx", (fd) => writeSync(fd, process.pid.toString()));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") return false;
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
function releaseTemplateLock(lockPath) {
|
|
416
|
+
try {
|
|
417
|
+
unlinkSync(lockPath);
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
/** A lock that no longer exists is not stale: the waiter retries. */
|
|
421
|
+
function isStaleTemplateLock(lockPath, now = Date.now()) {
|
|
422
|
+
try {
|
|
423
|
+
return now - statSync(lockPath).mtimeMs > LOCK_STALE_MS;
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/services/sqlite/sqlite.adapter.ts
|
|
430
|
+
let databaseConstructor = null;
|
|
431
|
+
async function loadDatabase() {
|
|
432
|
+
if (databaseConstructor === null) {
|
|
433
|
+
const constructor = await loadPeer("better-sqlite3", "sqlite()", async () => {
|
|
434
|
+
return (await import("better-sqlite3")).default;
|
|
435
|
+
});
|
|
436
|
+
requireBuiltPeer("better-sqlite3", "sqlite()", () => {
|
|
437
|
+
new constructor(":memory:").close();
|
|
438
|
+
});
|
|
439
|
+
databaseConstructor = constructor;
|
|
440
|
+
}
|
|
441
|
+
return databaseConstructor;
|
|
442
|
+
}
|
|
443
|
+
function database() {
|
|
444
|
+
if (databaseConstructor === null) throw invariant("sqlite() was read before `initialize()` loaded its driver");
|
|
445
|
+
return databaseConstructor;
|
|
446
|
+
}
|
|
447
|
+
function openInWal(path) {
|
|
448
|
+
const db = new (database())(path);
|
|
449
|
+
db.pragma("journal_mode = WAL");
|
|
450
|
+
return db;
|
|
451
|
+
}
|
|
452
|
+
const LOCK_POLL_MS = 50;
|
|
453
|
+
const SIDECARS = [
|
|
454
|
+
"",
|
|
455
|
+
"-wal",
|
|
456
|
+
"-shm"
|
|
457
|
+
];
|
|
458
|
+
function removeDatabaseFiles(path) {
|
|
459
|
+
for (const suffix of SIDECARS) rmSync(`${path}${suffix}`, { force: true });
|
|
460
|
+
}
|
|
461
|
+
function withDatabase(path, use) {
|
|
462
|
+
const db = new (database())(path);
|
|
463
|
+
try {
|
|
464
|
+
use(db);
|
|
465
|
+
} finally {
|
|
466
|
+
db.close();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
var SqliteHandle = class {
|
|
470
|
+
type = "sqlite";
|
|
471
|
+
serviceName = null;
|
|
472
|
+
defaultPort = 0;
|
|
473
|
+
defaultImage = "";
|
|
474
|
+
environment = {};
|
|
475
|
+
connectionString = "";
|
|
476
|
+
db = null;
|
|
477
|
+
templatePath = "";
|
|
478
|
+
workerDbPath = "";
|
|
479
|
+
initSql;
|
|
480
|
+
prismaSchema;
|
|
481
|
+
options;
|
|
482
|
+
constructor(options = {}) {
|
|
483
|
+
this.options = options;
|
|
484
|
+
this.initSql = options.init ?? null;
|
|
485
|
+
this.prismaSchema = options.prismaSchema ?? null;
|
|
486
|
+
}
|
|
487
|
+
buildConnectionString() {
|
|
488
|
+
return `file:${this.workerDbPath || this.templatePath}`;
|
|
489
|
+
}
|
|
490
|
+
createDatabaseAdapter() {
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
async healthcheck() {}
|
|
494
|
+
/** One worker builds under an exclusive lock, the others wait: two concurrent `prisma db push` on one file is `database is locked`. */
|
|
495
|
+
async initialize(_dockerDir, root) {
|
|
496
|
+
await loadDatabase();
|
|
497
|
+
const projectRoot = root ?? discoverRoot(process.cwd());
|
|
498
|
+
this.templatePath = resolve(sqliteTemplateDir(projectRoot), sqliteTemplateName(this.options));
|
|
499
|
+
const lockPath = `${this.templatePath}.lock`;
|
|
500
|
+
for (;;) {
|
|
501
|
+
if (isValidSqliteTemplate(this.templatePath)) break;
|
|
502
|
+
if (!acquireTemplateLock(lockPath)) {
|
|
503
|
+
if (isStaleTemplateLock(lockPath)) releaseTemplateLock(lockPath);
|
|
504
|
+
else await pause(LOCK_POLL_MS);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
await this.buildTemplate();
|
|
509
|
+
} finally {
|
|
510
|
+
releaseTemplateLock(lockPath);
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
this.connectionString = `file:${this.templatePath}`;
|
|
515
|
+
}
|
|
516
|
+
/** Built on a private path, then renamed into place: `prisma db push` writes the header long before the tables exist. */
|
|
517
|
+
async buildTemplate() {
|
|
518
|
+
const buildPath = `${this.templatePath}.${process.pid}.building`;
|
|
519
|
+
removeDatabaseFiles(buildPath);
|
|
520
|
+
try {
|
|
521
|
+
await this.writeTemplate(buildPath);
|
|
522
|
+
renameSync(buildPath, this.templatePath);
|
|
523
|
+
} finally {
|
|
524
|
+
removeDatabaseFiles(buildPath);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async writeTemplate(buildPath) {
|
|
528
|
+
if (this.prismaSchema !== null) {
|
|
529
|
+
const { runToCompletion } = await import("./shared-serve.js").then((n) => n.n);
|
|
530
|
+
runToCompletion(prismaPushCommand(this.prismaSchema), {
|
|
531
|
+
...process.env,
|
|
532
|
+
DATABASE_URL: `file:${buildPath}`,
|
|
533
|
+
PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: "yes"
|
|
534
|
+
});
|
|
535
|
+
withDatabase(buildPath, (db) => {
|
|
536
|
+
db.pragma("wal_checkpoint(TRUNCATE)");
|
|
537
|
+
});
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const { initSql } = this;
|
|
541
|
+
withDatabase(buildPath, (db) => {
|
|
542
|
+
if (initSql !== null) db.exec(readFileSync(initSql, "utf8"));
|
|
543
|
+
db.exec("VACUUM");
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
getDb() {
|
|
547
|
+
this.db ??= openInWal(this.workerDbPath || this.templatePath);
|
|
548
|
+
return this.db;
|
|
549
|
+
}
|
|
550
|
+
closeDb() {
|
|
551
|
+
this.db?.close();
|
|
552
|
+
this.db = null;
|
|
553
|
+
}
|
|
554
|
+
async seed(sql) {
|
|
555
|
+
this.getDb().exec(sql);
|
|
556
|
+
}
|
|
557
|
+
async query(table, columns) {
|
|
558
|
+
const columnList = columns.join(", ");
|
|
559
|
+
return this.getDb().prepare(`SELECT ${columnList} FROM "${table}" ORDER BY 1`).all().map((row) => columns.map((col) => row[col]));
|
|
560
|
+
}
|
|
561
|
+
async columns(table) {
|
|
562
|
+
return this.getDb().prepare(`PRAGMA table_info("${table}")`).all().map((row) => row.name);
|
|
563
|
+
}
|
|
564
|
+
async reset() {
|
|
565
|
+
const db = this.getDb();
|
|
566
|
+
const tables = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_prisma%' AND name != 'sqlite_sequence'`).all();
|
|
567
|
+
for (const { name } of tables) db.exec(`DELETE FROM "${name}"`);
|
|
568
|
+
if (db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = 'sqlite_sequence'`).get() !== void 0) db.exec("DELETE FROM sqlite_sequence");
|
|
569
|
+
}
|
|
570
|
+
isolation() {
|
|
571
|
+
return {
|
|
572
|
+
acquire: async (workerId) => {
|
|
573
|
+
this.closeDb();
|
|
574
|
+
this.workerDbPath = resolve(tmpdir(), `test-worker-${workerId}-${process.pid}-${Date.now()}.sqlite`);
|
|
575
|
+
copyFileSync(this.templatePath, this.workerDbPath);
|
|
576
|
+
this.connectionString = `file:${this.workerDbPath}`;
|
|
577
|
+
},
|
|
578
|
+
reset: async () => {
|
|
579
|
+
await this.reset();
|
|
580
|
+
},
|
|
581
|
+
release: async () => {
|
|
582
|
+
this.closeDb();
|
|
583
|
+
removeDatabaseFiles(this.workerDbPath);
|
|
584
|
+
this.workerDbPath = "";
|
|
585
|
+
this.connectionString = `file:${this.templatePath}`;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
/** Create a SQLite service handle, isolated per test by file copy. */
|
|
591
|
+
function sqlite(options = {}) {
|
|
592
|
+
return new SqliteHandle(options);
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
export { ProcessHandle, postgres, processService as process, redis, sqlite };
|
|
596
|
+
|
|
597
|
+
//# sourceMappingURL=services.js.map
|