1- import { registerSharedWorker , SharedWorker } from "ava/plugin"
1+ import { registerSharedWorker } from "ava/plugin"
22import hash from "object-hash"
33import path from "node:path"
4- import {
4+ import type {
55 ConnectionDetailsFromWorker ,
6- FinishedRunningBeforeTemplateIsBakedHookMessage ,
76 InitialWorkerData ,
8- MessageFromWorker ,
9- MessageToWorker ,
7+ SharedWorkerFunctions ,
8+ TestWorkerFunctions ,
109} from "./internal-types"
11- import {
10+ import type {
1211 ConnectionDetails ,
1312 GetTestPostgresDatabase ,
1413 GetTestPostgresDatabaseFactoryOptions ,
1514 GetTestPostgresDatabaseOptions ,
16- GetTestPostgresDatabaseResult ,
1715} from "./public-types"
1816import { Pool } from "pg"
19- import { Jsonifiable } from "type-fest"
20- import { StartedNetwork } from "testcontainers"
21- import { ExecutionContext } from "ava"
17+ import type { Jsonifiable } from "type-fest"
18+ import type { ExecutionContext } from "ava"
19+ import { once } from "node:events"
20+ import { createBirpc } from "birpc"
21+ import { ExecResult } from "testcontainers"
22+ import isPlainObject from "lodash/isPlainObject"
23+
24+ // https://stackoverflow.com/a/30580513
25+ const isSerializable = ( obj : Record < any , any > ) : boolean => {
26+ var isNestedSerializable
27+ function isPlain ( val : any ) {
28+ return (
29+ typeof val === "undefined" ||
30+ typeof val === "string" ||
31+ typeof val === "boolean" ||
32+ typeof val === "number" ||
33+ Array . isArray ( val ) ||
34+ isPlainObject ( val )
35+ )
36+ }
37+ if ( ! isPlain ( obj ) ) {
38+ return false
39+ }
40+ for ( var property in obj ) {
41+ if ( obj . hasOwnProperty ( property ) ) {
42+ if ( ! isPlain ( obj [ property ] ) ) {
43+ return false
44+ }
45+ if ( typeof obj [ property ] == "object" ) {
46+ isNestedSerializable = isSerializable ( obj [ property ] )
47+ if ( ! isNestedSerializable ) {
48+ return false
49+ }
50+ }
51+ }
52+ }
53+ return true
54+ }
2255
2356const getWorker = async (
2457 initialData : InitialWorkerData ,
@@ -50,6 +83,24 @@ const getWorker = async (
5083 } )
5184}
5285
86+ const teardownConnection = async ( {
87+ pool,
88+ pgbouncerPool,
89+ } : ConnectionDetails ) => {
90+ try {
91+ await pool . end ( )
92+ await pgbouncerPool ?. end ( )
93+ } catch ( error ) {
94+ if (
95+ ( error as Error ) . message . includes ( "Called end on pool more than once" )
96+ ) {
97+ return
98+ }
99+
100+ throw error
101+ }
102+ }
103+
53104export const getTestPostgresDatabaseFactory = <
54105 Params extends Jsonifiable = never
55106> (
@@ -63,71 +114,34 @@ export const getTestPostgresDatabaseFactory = <
63114
64115 const workerPromise = getWorker ( initialData , options as any )
65116
66- const getTestPostgresDatabase : GetTestPostgresDatabase < Params > = async (
67- t : ExecutionContext ,
68- params : any ,
69- getTestDatabaseOptions ?: GetTestPostgresDatabaseOptions
70- ) => {
71- const mapWorkerConnectionDetailsToConnectionDetails = (
72- connectionDetailsFromWorker : ConnectionDetailsFromWorker
73- ) : ConnectionDetails => {
74- const pool = new Pool ( {
75- connectionString : connectionDetailsFromWorker . connectionString ,
76- } )
77-
78- let pgbouncerPool : Pool | undefined
79- if ( connectionDetailsFromWorker . pgbouncerConnectionString ) {
80- pgbouncerPool = new Pool ( {
81- connectionString :
82- connectionDetailsFromWorker . pgbouncerConnectionString ,
83- } )
84- }
85-
86- t . teardown ( async ( ) => {
87- try {
88- await pool . end ( )
89- await pgbouncerPool ?. end ( )
90- } catch ( error ) {
91- if (
92- ( error as Error ) . message . includes (
93- "Called end on pool more than once"
94- )
95- ) {
96- return
97- }
117+ const mapWorkerConnectionDetailsToConnectionDetails = (
118+ connectionDetailsFromWorker : ConnectionDetailsFromWorker
119+ ) : ConnectionDetails => {
120+ const pool = new Pool ( {
121+ connectionString : connectionDetailsFromWorker . connectionString ,
122+ } )
98123
99- throw error
100- }
124+ let pgbouncerPool : Pool | undefined
125+ if ( connectionDetailsFromWorker . pgbouncerConnectionString ) {
126+ pgbouncerPool = new Pool ( {
127+ connectionString : connectionDetailsFromWorker . pgbouncerConnectionString ,
101128 } )
102-
103- return {
104- ...connectionDetailsFromWorker ,
105- pool,
106- pgbouncerPool,
107- }
108129 }
109130
110- const worker = await workerPromise
111- await worker . available
112-
113- const waitForAndHandleReply = async (
114- message : SharedWorker . Plugin . PublishedMessage
115- ) : Promise < GetTestPostgresDatabaseResult > => {
116- let reply = await message . replies ( ) . next ( )
117- const replyData : MessageFromWorker = reply . value . data
118-
119- if ( replyData . type === "RUN_HOOK_BEFORE_TEMPLATE_IS_BAKED" ) {
120- let result : FinishedRunningBeforeTemplateIsBakedHookMessage [ "result" ] =
121- {
122- status : "success" ,
123- result : undefined ,
124- }
131+ return {
132+ ...connectionDetailsFromWorker ,
133+ pool,
134+ pgbouncerPool,
135+ }
136+ }
125137
138+ let rpcCallback : ( data : any ) => void
139+ const rpc = createBirpc < SharedWorkerFunctions , TestWorkerFunctions > (
140+ {
141+ runBeforeTemplateIsBakedHook : async ( connection , params ) => {
126142 if ( options ?. beforeTemplateIsBaked ) {
127143 const connectionDetails =
128- mapWorkerConnectionDetailsToConnectionDetails (
129- replyData . connectionDetails
130- )
144+ mapWorkerConnectionDetailsToConnectionDetails ( connection )
131145
132146 // Ignore if the pool is terminated by the shared worker
133147 // (This happens in CI for some reason even though we drain the pool first.)
@@ -143,94 +157,70 @@ export const getTestPostgresDatabaseFactory = <
143157 throw error
144158 } )
145159
146- try {
147- const hookResult = await options . beforeTemplateIsBaked ( {
148- params,
149- connection : connectionDetails ,
150- containerExec : async ( command ) => {
151- const request = reply . value . reply ( {
152- type : "EXEC_COMMAND_IN_CONTAINER" ,
153- command,
154- } )
155-
156- reply = await request . replies ( ) . next ( )
157-
158- if (
159- reply . value . data . type !== "EXEC_COMMAND_IN_CONTAINER_RESULT"
160- ) {
161- throw new Error (
162- "Expected EXEC_COMMAND_IN_CONTAINER_RESULT message"
163- )
164- }
165-
166- return reply . value . data . result
167- } ,
168- } )
169-
170- result = {
171- status : "success" ,
172- result : hookResult ,
173- }
174- } catch ( error ) {
175- result = {
176- status : "error" ,
177- error :
178- error instanceof Error
179- ? error . stack ?? error . message
180- : new Error (
181- "Unknown error type thrown in beforeTemplateIsBaked hook"
182- ) ,
183- }
184- } finally {
185- // Otherwise connection will be killed by worker when converting to template
186- await connectionDetails . pool . end ( )
187- }
188- }
160+ const hookResult = await options . beforeTemplateIsBaked ( {
161+ params : params as any ,
162+ connection : connectionDetails ,
163+ containerExec : async ( command ) : Promise < ExecResult > =>
164+ rpc . execCommandInContainer ( command ) ,
165+ } )
189166
190- try {
191- return waitForAndHandleReply (
192- reply . value . reply ( {
193- type : "FINISHED_RUNNING_HOOK_BEFORE_TEMPLATE_IS_BAKED" ,
194- result,
195- } as MessageToWorker )
196- )
197- } catch ( error ) {
198- if ( error instanceof Error && error . name === "DataCloneError" ) {
167+ await teardownConnection ( connectionDetails )
168+
169+ if ( hookResult && ! isSerializable ( hookResult ) ) {
199170 throw new TypeError (
200171 "Return value of beforeTemplateIsBaked() hook could not be serialized. Make sure it returns only JSON-serializable values."
201172 )
202173 }
203174
204- throw error
205- }
206- } else if ( replyData . type === "GOT_DATABASE" ) {
207- if ( replyData . beforeTemplateIsBakedResult . status === "error" ) {
208- if ( typeof replyData . beforeTemplateIsBakedResult . error === "string" ) {
209- throw new Error ( replyData . beforeTemplateIsBakedResult . error )
210- }
211-
212- throw replyData . beforeTemplateIsBakedResult . error
175+ return hookResult
213176 }
177+ } ,
178+ } ,
179+ {
180+ post : async ( data ) => {
181+ const worker = await workerPromise
182+ await worker . available
183+ worker . publish ( data )
184+ } ,
185+ on : ( data ) => {
186+ rpcCallback = data
187+ } ,
188+ }
189+ )
214190
215- return {
216- ...mapWorkerConnectionDetailsToConnectionDetails (
217- replyData . connectionDetails
218- ) ,
219- beforeTemplateIsBakedResult :
220- replyData . beforeTemplateIsBakedResult . result ,
221- }
222- }
191+ // Automatically cleaned up by AVA since each test file runs in a separate worker
192+ const _messageHandlerPromise = ( async ( ) => {
193+ const worker = await workerPromise
194+ await worker . available
223195
224- throw new Error ( `Unexpected message type: ${ replyData . type } ` )
196+ for await ( const msg of worker . subscribe ( ) ) {
197+ rpcCallback ! ( msg . data )
225198 }
199+ } ) ( )
226200
227- return waitForAndHandleReply (
228- worker . publish ( {
229- type : "GET_TEST_DATABASE" ,
230- params,
231- key : getTestDatabaseOptions ?. databaseDedupeKey ,
232- } as MessageToWorker )
201+ const getTestPostgresDatabase : GetTestPostgresDatabase < Params > = async (
202+ t : ExecutionContext ,
203+ params : any ,
204+ getTestDatabaseOptions ?: GetTestPostgresDatabaseOptions
205+ ) => {
206+ const testDatabaseConnection = await rpc . getTestDatabase ( {
207+ databaseDedupeKey : getTestDatabaseOptions ?. databaseDedupeKey ,
208+ params,
209+ } )
210+
211+ const connectionDetails = mapWorkerConnectionDetailsToConnectionDetails (
212+ testDatabaseConnection . connectionDetails
233213 )
214+
215+ t . teardown ( async ( ) => {
216+ await teardownConnection ( connectionDetails )
217+ } )
218+
219+ return {
220+ ...connectionDetails ,
221+ beforeTemplateIsBakedResult :
222+ testDatabaseConnection . beforeTemplateIsBakedResult ,
223+ }
234224 }
235225
236226 return getTestPostgresDatabase
0 commit comments