@@ -14,7 +14,12 @@ import { env } from 'node:process'
14
14
import { fileURLToPath } from 'node:url'
15
15
import { v4 } from 'uuid'
16
16
import { 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'
18
23
19
24
import { glob } from 'fast-glob'
20
25
import {
@@ -405,48 +410,140 @@ export async function invokeEdgeFunction(
405
410
} )
406
411
}
407
412
408
- export async function invokeSandboxedFunction (
413
+ /**
414
+ * Load function in child process and allow for multiple invocations
415
+ */
416
+ export async function loadSandboxedFunction (
409
417
ctx : FixtureTestContext ,
410
- options : Parameters < typeof invokeFunction > [ 1 ] = { } ,
418
+ options : LoadFunctionOptions = { } ,
411
419
) {
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
+ } )
417
428
418
- childProcess . stdout ?. on ( 'data' , ( data ) => {
419
- console . log ( data . toString ( ) )
420
- } )
429
+ let isRunning = true
430
+ let operationCounter = 1
421
431
422
- childProcess . stderr ?. on ( 'data' , ( data ) => {
423
- console . error ( data . toString ( ) )
424
- } )
432
+ childProcess . stdout ?. on ( 'data' , ( data ) => {
433
+ console . log ( data . toString ( ) )
434
+ } )
425
435
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
432
451
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
435
456
} )
436
457
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
+
437
523
childProcess . send ( {
438
524
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 ] ,
450
527
} )
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
452
549
}
0 commit comments