@@ -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 {
@@ -24,6 +29,7 @@ import {
24
29
} from '../../src/build/plugin-context.js'
25
30
import { BLOB_TOKEN } from './constants.mjs'
26
31
import { type FixtureTestContext } from './contexts.js'
32
+ // import { createBlobContext } from './helpers.js'
27
33
import { setNextVersionInFixture } from './next-version-helpers.mjs'
28
34
29
35
const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts'
@@ -405,48 +411,140 @@ export async function invokeEdgeFunction(
405
411
} )
406
412
}
407
413
408
- export async function invokeSandboxedFunction (
414
+ /**
415
+ * Load function in child process and allow for multiple invocations
416
+ */
417
+ export async function loadSandboxedFunction (
409
418
ctx : FixtureTestContext ,
410
- options : Parameters < typeof invokeFunction > [ 1 ] = { } ,
419
+ options : LoadFunctionOptions = { } ,
411
420
) {
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
- } )
421
+ const childProcess = spawn ( process . execPath , [ import . meta. dirname + '/sandbox-child.mjs' ] , {
422
+ stdio : [ 'pipe' , 'pipe' , 'pipe' , 'ipc' ] ,
423
+ cwd : join ( ctx . functionDist , SERVER_HANDLER_NAME ) ,
424
+ env : {
425
+ ...process . env ,
426
+ ...( options . env || { } ) ,
427
+ } ,
428
+ } )
417
429
418
- childProcess . stdout ?. on ( 'data' , ( data ) => {
419
- console . log ( data . toString ( ) )
420
- } )
430
+ let isRunning = true
431
+ let operationCounter = 1
421
432
422
- childProcess . stderr ?. on ( 'data' , ( data ) => {
423
- console . error ( data . toString ( ) )
424
- } )
433
+ childProcess . stdout ?. on ( 'data' , ( data ) => {
434
+ console . log ( data . toString ( ) )
435
+ } )
425
436
426
- childProcess . on ( 'message' , ( msg : any ) => {
427
- if ( msg ?. action === 'invokeFunctionResult' ) {
428
- resolve ( msg . result )
429
- childProcess . send ( { action : 'exit' } )
430
- }
431
- } )
437
+ childProcess . stderr ?. on ( 'data' , ( data ) => {
438
+ console . error ( data . toString ( ) )
439
+ } )
440
+
441
+ const onGoingOperationsMap = new Map <
442
+ number ,
443
+ {
444
+ resolve : ( value ?: any ) => void
445
+ reject : ( reason ?: any ) => void
446
+ }
447
+ > ( )
448
+
449
+ function createOperation < T > ( ) {
450
+ const operationId = operationCounter
451
+ operationCounter += 1
432
452
433
- childProcess . on ( 'exit' , ( ) => {
434
- reject ( new Error ( 'worker exited before returning result' ) )
453
+ let promiseResolve , promiseReject
454
+ const promise = new Promise < T > ( ( innerResolve , innerReject ) => {
455
+ promiseResolve = innerResolve
456
+ promiseReject = innerReject
435
457
} )
436
458
459
+ function resolve ( value : T ) {
460
+ onGoingOperationsMap . delete ( operationId )
461
+ promiseResolve ?.( value )
462
+ }
463
+ function reject ( reason ) {
464
+ onGoingOperationsMap . delete ( operationId )
465
+ promiseReject ?.( reason )
466
+ }
467
+
468
+ onGoingOperationsMap . set ( operationId , { resolve, reject } )
469
+ return { operationId, promise, resolve, reject }
470
+ }
471
+
472
+ childProcess . on ( 'exit' , ( ) => {
473
+ isRunning = false
474
+
475
+ const error = new Error ( 'worker exited before returning result' )
476
+
477
+ for ( const { reject } of onGoingOperationsMap . values ( ) ) {
478
+ reject ( error )
479
+ }
480
+ } )
481
+
482
+ function exit ( ) {
483
+ if ( isRunning ) {
484
+ childProcess . send ( { action : 'exit' } )
485
+ }
486
+ }
487
+
488
+ // make sure to exit the child process when the test is done just in case
489
+ ctx . cleanup ?. push ( async ( ) => exit ( ) )
490
+
491
+ const { promise : loadPromise , resolve : loadResolve } = createOperation < void > ( )
492
+
493
+ childProcess . on ( 'message' , ( msg : any ) => {
494
+ if ( msg ?. action === 'invokeFunctionResult' ) {
495
+ onGoingOperationsMap . get ( msg . operationId ) ?. resolve ( msg . result )
496
+ } else if ( msg ?. action === 'loadedFunction' ) {
497
+ loadResolve ( )
498
+ }
499
+ } )
500
+
501
+ // context object is not serializable so we create serializable object
502
+ // containing required properties to invoke lambda
503
+ const serializableCtx = {
504
+ functionDist : ctx . functionDist ,
505
+ blobStoreHost : ctx . blobStoreHost ,
506
+ siteID : ctx . siteID ,
507
+ deployID : ctx . deployID ,
508
+ }
509
+
510
+ childProcess . send ( {
511
+ action : 'loadFunction' ,
512
+ args : [ serializableCtx ] ,
513
+ } )
514
+
515
+ await loadPromise
516
+
517
+ function invokeFunction ( options : FunctionInvocationOptions ) : InvokeFunctionResult {
518
+ if ( ! isRunning ) {
519
+ throw new Error ( 'worker is not running anymore' )
520
+ }
521
+
522
+ const { operationId, promise } = createOperation < Awaited < InvokeFunctionResult > > ( )
523
+
437
524
childProcess . send ( {
438
525
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
- ] ,
526
+ operationId,
527
+ args : [ serializableCtx , options ] ,
450
528
} )
451
- } )
529
+
530
+ return promise
531
+ }
532
+
533
+ return {
534
+ invokeFunction,
535
+ exit,
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Load function in child process and execute single invocation
541
+ */
542
+ export async function invokeSandboxedFunction (
543
+ ctx : FixtureTestContext ,
544
+ options : FunctionInvocationOptions = { } ,
545
+ ) {
546
+ const { invokeFunction, exit } = await loadSandboxedFunction ( ctx , options )
547
+ const result = await invokeFunction ( options )
548
+ exit ( )
549
+ return result
452
550
}
0 commit comments