1+ import * as NodeServices from "@effect/platform-node/NodeServices" ;
12import { describe , expect , it } from "@effect/vitest" ;
3+ import * as Context from "effect/Context" ;
24import * as Deferred from "effect/Deferred" ;
35import * as Duration from "effect/Duration" ;
46import * as Effect from "effect/Effect" ;
@@ -19,6 +21,7 @@ import type { RelayManagedEndpointRuntimeConfig } from "@t3tools/contracts/relay
1921import * as RelayClient from "@t3tools/shared/relayClient" ;
2022
2123import * as ServerSecretStore from "../auth/ServerSecretStore.ts" ;
24+ import * as ServerConfig from "../config.ts" ;
2225import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts" ;
2326
2427const layerRelayClientAvailable = Layer . succeed (
@@ -38,29 +41,34 @@ const layerRelayClientAvailable = Layer.succeed(
3841
3942const unusedHttpClient = HttpClient . make ( ( ) => Effect . die ( "unused" ) ) ;
4043
44+ const emptySecretStoreLayer = Layer . mock ( ServerSecretStore . ServerSecretStore ) ( {
45+ get : ( ) => Effect . succeedNone ,
46+ set : ( ) => Effect . void ,
47+ } ) ;
48+
4149const runtimeDependencies = (
4250 spawner : ReturnType < typeof ChildProcessSpawner . make > ,
4351 relayClientLayer = layerRelayClientAvailable ,
4452 httpClient = unusedHttpClient ,
53+ secretStoreLayer = emptySecretStoreLayer ,
4554) =>
4655 Layer . mergeAll (
4756 Layer . succeed ( ChildProcessSpawner . ChildProcessSpawner , spawner ) ,
4857 Layer . succeed ( HttpClient . HttpClient , httpClient ) ,
4958 relayClientLayer ,
50- Layer . mock ( ServerSecretStore . ServerSecretStore ) ( {
51- get : ( ) => Effect . succeedNone ,
52- } ) ,
59+ secretStoreLayer ,
5360 ) ;
5461
5562const buildCloudManagedEndpointRuntime = (
5663 spawner : ReturnType < typeof ChildProcessSpawner . make > ,
5764 relayClientLayer = layerRelayClientAvailable ,
5865 httpClient = unusedHttpClient ,
66+ secretStoreLayer = emptySecretStoreLayer ,
5967) =>
6068 Effect . gen ( function * ( ) {
6169 const context = yield * Layer . build (
6270 ManagedEndpointRuntime . layer . pipe (
63- Layer . provide ( runtimeDependencies ( spawner , relayClientLayer , httpClient ) ) ,
71+ Layer . provide ( runtimeDependencies ( spawner , relayClientLayer , httpClient , secretStoreLayer ) ) ,
6472 ) ,
6573 ) ;
6674 return yield * Effect . service ( ManagedEndpointRuntime . CloudManagedEndpointRuntime ) . pipe (
@@ -403,6 +411,117 @@ describe("CloudManagedEndpointRuntime", () => {
403411 } ) ,
404412 ) ;
405413
414+ it . effect (
415+ "retains automatic HTTP/2 fallback across restarts until expiry and respects overrides" ,
416+ ( ) =>
417+ Effect . gen ( function * ( ) {
418+ const context = yield * Layer . build (
419+ ServerSecretStore . layer . pipe (
420+ Layer . provide ( ServerConfig . layerTest ( process . cwd ( ) , { prefix : "t3-transport-test-" } ) ) ,
421+ Layer . provide ( NodeServices . layer ) ,
422+ ) ,
423+ ) ;
424+ const secretStoreLayer = Layer . succeed (
425+ ServerSecretStore . ServerSecretStore ,
426+ context . pipe ( Context . get ( ServerSecretStore . ServerSecretStore ) ) ,
427+ ) ;
428+ const fixture = yield * makeTransportSpawner ( ) ;
429+ const start = ( spawner = fixture . spawner ) =>
430+ buildCloudManagedEndpointRuntime (
431+ spawner ,
432+ layerRelayClientAvailable ,
433+ unusedHttpClient ,
434+ secretStoreLayer ,
435+ ) ;
436+ yield * Effect . gen ( function * ( ) {
437+ const runtime = yield * start ( ) ;
438+ yield * runtime . applyConfig ( tunnelConfig ) ;
439+ expect ( yield * Queue . take ( fixture . started ) ) . toBe ( 0 ) ;
440+ expect ( fixture . spawned [ 0 ] ! . args ) . not . toContain ( "--protocol" ) ;
441+ yield * fixture . emit ( 0 , quicDialWarning . repeat ( 30 ) ) ;
442+ expect ( yield * Queue . take ( fixture . started ) ) . toBe ( 1 ) ;
443+ yield * fixture . observe ( runtime , 1 ) ;
444+ expect ( fixture . spawned [ 1 ] ! . args . slice ( - 3 ) ) . toEqual ( [ "--protocol" , "http2" , "run" ] ) ;
445+ } ) . pipe ( Effect . scoped ) ;
446+
447+ yield * TestClock . adjust ( Duration . hours ( 1 ) ) ;
448+ for ( const transport of [ undefined , "quic" , "http2" , "auto" , undefined ] ) {
449+ yield * Effect . gen ( function * ( ) {
450+ const next = yield * makeTransportSpawner ( transport ? { transport } : { } ) ;
451+ const restarted = yield * start ( next . spawner ) ;
452+ yield * restarted . applyConfig ( tunnelConfig ) ;
453+ if ( transport === undefined ) {
454+ expect ( next . spawned [ 0 ] ! . args . slice ( - 3 ) ) . toEqual ( [ "--protocol" , "http2" , "run" ] ) ;
455+ } else {
456+ expect ( next . spawned [ 0 ] ! . args ) . not . toContain ( "--protocol" ) ;
457+ expect ( next . spawned [ 0 ] ! . options . env ?. TUNNEL_TRANSPORT_PROTOCOL ) . toBe ( transport ) ;
458+ yield * next . observe ( restarted , 0 , quicDialWarning . repeat ( 30 ) ) ;
459+ expect ( next . spawned ) . toHaveLength ( 1 ) ;
460+ }
461+ } ) . pipe ( Effect . scoped ) ;
462+ }
463+
464+ // Reusing the hint one hour later must not extend its original lifetime.
465+ yield * TestClock . adjust ( Duration . hours ( 23 ) ) ;
466+ yield * Effect . gen ( function * ( ) {
467+ const next = yield * makeTransportSpawner ( ) ;
468+ const restarted = yield * start ( next . spawner ) ;
469+ yield * restarted . applyConfig ( tunnelConfig ) ;
470+ expect ( next . spawned [ 0 ] ! . args ) . not . toContain ( "--protocol" ) ;
471+ expect ( yield * Queue . take ( next . started ) ) . toBe ( 0 ) ;
472+ yield * next . emit ( 0 , quicDatagramWarning . repeat ( 30 ) ) ;
473+ expect ( yield * Queue . take ( next . started ) ) . toBe ( 1 ) ;
474+ expect ( next . spawned [ 1 ] ! . args . slice ( - 3 ) ) . toEqual ( [ "--protocol" , "http2" , "run" ] ) ;
475+ } ) . pipe ( Effect . scoped ) ;
476+ } ) ,
477+ ) ;
478+
479+ it . effect (
480+ "ignores invalid or unreadable fallback state and still falls back if persistence fails" ,
481+ ( ) =>
482+ Effect . gen ( function * ( ) {
483+ for ( const cached of [
484+ "not json" ,
485+ '{"fellBackAt":"invalid"}' ,
486+ '{"fellBackAt":9007199254740991}' ,
487+ null ,
488+ ] ) {
489+ yield * Effect . gen ( function * ( ) {
490+ const fixture = yield * makeTransportSpawner ( ) ;
491+ const runtime = yield * buildCloudManagedEndpointRuntime (
492+ fixture . spawner ,
493+ layerRelayClientAvailable ,
494+ unusedHttpClient ,
495+ Layer . mock ( ServerSecretStore . ServerSecretStore ) ( {
496+ get : ( ) =>
497+ cached === null
498+ ? Effect . fail (
499+ new ServerSecretStore . SecretStoreReadError ( {
500+ resource : "test" ,
501+ cause : "unreadable" ,
502+ } ) ,
503+ )
504+ : Effect . succeedSome ( new TextEncoder ( ) . encode ( cached ) ) ,
505+ set : ( ) =>
506+ Effect . fail (
507+ new ServerSecretStore . SecretStorePersistError ( {
508+ resource : "test" ,
509+ cause : "read-only" ,
510+ } ) ,
511+ ) ,
512+ } ) ,
513+ ) ;
514+ yield * runtime . applyConfig ( tunnelConfig ) ;
515+ expect ( yield * Queue . take ( fixture . started ) ) . toBe ( 0 ) ;
516+ expect ( fixture . spawned [ 0 ] ! . args ) . not . toContain ( "--protocol" ) ;
517+ yield * fixture . emit ( 0 , quicDialWarning . repeat ( 30 ) ) ;
518+ expect ( yield * Queue . take ( fixture . started ) ) . toBe ( 1 ) ;
519+ expect ( fixture . spawned [ 1 ] ! . args . slice ( - 3 ) ) . toEqual ( [ "--protocol" , "http2" , "run" ] ) ;
520+ } ) . pipe ( Effect . scoped ) ;
521+ }
522+ } ) ,
523+ ) ;
524+
406525 it . effect ( "requests recovery after a failed HTTP/2 spawn and retries with HTTP/2" , ( ) =>
407526 Effect . gen ( function * ( ) {
408527 const fixture = yield * makeTransportSpawner ( { failSpawn : 1 } ) ;
0 commit comments