@@ -14,7 +14,12 @@ import { env } from 'node:process'
1414import { fileURLToPath } from 'node:url'
1515import { v4 } from 'uuid'
1616import { LocalServer } from './local-server.js'
17- import { loadFunction , type FunctionInvocationOptions } from './lambda-helpers.mjs'
17+ import {
18+ type InvokeFunctionResult ,
19+ loadFunction ,
20+ type LoadFunctionOptions ,
21+ type FunctionInvocationOptions ,
22+ } from './lambda-helpers.mjs'
1823
1924import { glob } from 'fast-glob'
2025import {
@@ -405,48 +410,140 @@ export async function invokeEdgeFunction(
405410 } )
406411}
407412
408- export async function invokeSandboxedFunction (
413+ /**
414+ * Load function in child process and allow for multiple invocations
415+ */
416+ export async function loadSandboxedFunction (
409417 ctx : FixtureTestContext ,
410- options : Parameters < typeof invokeFunction > [ 1 ] = { } ,
418+ options : LoadFunctionOptions = { } ,
411419) {
412- return new Promise < ReturnType < typeof invokeFunction > > ( ( resolve , reject ) => {
413- const childProcess = spawn ( process . execPath , [ import . meta. dirname + '/sandbox-child.mjs' ] , {
414- stdio : [ 'pipe' , 'pipe' , 'pipe' , 'ipc' ] ,
415- cwd : process . cwd ( ) ,
416- } )
420+ const childProcess = spawn ( process . execPath , [ import . meta. dirname + '/sandbox-child.mjs' ] , {
421+ stdio : [ 'pipe' , 'pipe' , 'pipe' , 'ipc' ] ,
422+ cwd : join ( ctx . functionDist , SERVER_HANDLER_NAME ) ,
423+ env : {
424+ ...process . env ,
425+ ...( options . env || { } ) ,
426+ } ,
427+ } )
417428
418- childProcess . stdout ?. on ( 'data' , ( data ) => {
419- console . log ( data . toString ( ) )
420- } )
429+ let isRunning = true
430+ let operationCounter = 1
421431
422- childProcess . stderr ?. on ( 'data' , ( data ) => {
423- console . error ( data . toString ( ) )
424- } )
432+ childProcess . stdout ?. on ( 'data' , ( data ) => {
433+ console . log ( data . toString ( ) )
434+ } )
425435
426- childProcess . on ( 'message' , ( msg : any ) => {
427- if ( msg ?. action === 'invokeFunctionResult' ) {
428- resolve ( msg . result )
429- childProcess . send ( { action : 'exit' } )
430- }
431- } )
436+ childProcess . stderr ?. on ( 'data' , ( data ) => {
437+ console . error ( data . toString ( ) )
438+ } )
439+
440+ const onGoingOperationsMap = new Map <
441+ number ,
442+ {
443+ resolve : ( value ?: any ) => void
444+ reject : ( reason ?: any ) => void
445+ }
446+ > ( )
447+
448+ function createOperation < T > ( ) {
449+ const operationId = operationCounter
450+ operationCounter += 1
432451
433- childProcess . on ( 'exit' , ( ) => {
434- reject ( new Error ( 'worker exited before returning result' ) )
452+ let promiseResolve , promiseReject
453+ const promise = new Promise < T > ( ( innerResolve , innerReject ) => {
454+ promiseResolve = innerResolve
455+ promiseReject = innerReject
435456 } )
436457
458+ function resolve ( value : T ) {
459+ onGoingOperationsMap . delete ( operationId )
460+ promiseResolve ?.( value )
461+ }
462+ function reject ( reason ) {
463+ onGoingOperationsMap . delete ( operationId )
464+ promiseReject ?.( reason )
465+ }
466+
467+ onGoingOperationsMap . set ( operationId , { resolve, reject } )
468+ return { operationId, promise, resolve, reject }
469+ }
470+
471+ childProcess . on ( 'exit' , ( ) => {
472+ isRunning = false
473+
474+ const error = new Error ( 'worker exited before returning result' )
475+
476+ for ( const { reject } of onGoingOperationsMap . values ( ) ) {
477+ reject ( error )
478+ }
479+ } )
480+
481+ function exit ( ) {
482+ if ( isRunning ) {
483+ childProcess . send ( { action : 'exit' } )
484+ }
485+ }
486+
487+ // make sure to exit the child process when the test is done just in case
488+ ctx . cleanup ?. push ( async ( ) => exit ( ) )
489+
490+ const { promise : loadPromise , resolve : loadResolve } = createOperation < void > ( )
491+
492+ childProcess . on ( 'message' , ( msg : any ) => {
493+ if ( msg ?. action === 'invokeFunctionResult' ) {
494+ onGoingOperationsMap . get ( msg . operationId ) ?. resolve ( msg . result )
495+ } else if ( msg ?. action === 'loadedFunction' ) {
496+ loadResolve ( )
497+ }
498+ } )
499+
500+ // context object is not serializable so we create serializable object
501+ // containing required properties to invoke lambda
502+ const serializableCtx = {
503+ functionDist : ctx . functionDist ,
504+ blobStoreHost : ctx . blobStoreHost ,
505+ siteID : ctx . siteID ,
506+ deployID : ctx . deployID ,
507+ }
508+
509+ childProcess . send ( {
510+ action : 'loadFunction' ,
511+ args : [ serializableCtx ] ,
512+ } )
513+
514+ await loadPromise
515+
516+ function invokeFunction ( options : FunctionInvocationOptions ) : InvokeFunctionResult {
517+ if ( ! isRunning ) {
518+ throw new Error ( 'worker is not running anymore' )
519+ }
520+
521+ const { operationId, promise } = createOperation < Awaited < InvokeFunctionResult > > ( )
522+
437523 childProcess . send ( {
438524 action : 'invokeFunction' ,
439- args : [
440- // context object is not serializable so we create serializable object
441- // containing required properties to invoke lambda
442- {
443- functionDist : ctx . functionDist ,
444- blobStoreHost : ctx . blobStoreHost ,
445- siteID : ctx . siteID ,
446- deployID : ctx . deployID ,
447- } ,
448- options ,
449- ] ,
525+ operationId,
526+ args : [ serializableCtx , options ] ,
450527 } )
451- } )
528+
529+ return promise
530+ }
531+
532+ return {
533+ invokeFunction,
534+ exit,
535+ }
536+ }
537+
538+ /**
539+ * Load function in child process and execute single invocation
540+ */
541+ export async function invokeSandboxedFunction (
542+ ctx : FixtureTestContext ,
543+ options : FunctionInvocationOptions = { } ,
544+ ) {
545+ const { invokeFunction, exit } = await loadSandboxedFunction ( ctx , options )
546+ const result = await invokeFunction ( options )
547+ exit ( )
548+ return result
452549}
0 commit comments