serverFile function

String serverFile(
  1. ServerServer server,
  2. String formatter(
    1. Spec
    ), {
  3. required Options options,
})

Implementation

String serverFile(
  ServerServer server,
  String Function(Spec) formatter, {
  required Options options,
}) {
  final imports = [
    if (options.ignoreLints case final lints when lints.isNotEmpty)
      "// ignore_for_file: ${options.ignoreLints.join(', ')}\n",
    "import 'dart:io';",
    "import 'dart:async';",
    "import 'dart:isolate';",
    '',
    "import 'package:path/path.dart' as p;",
    for (final imprt in {
      if (server.context.mode.isDebug)
        'package:revali_construct/revali_construct.dart',
      'package:revali_router/revali_router.dart',
      ...server.packageImports(),
    })
      "import '$imprt';",
    '',
    for (final imprt in {...server.pathImports()}) "import '../../$imprt';",
  ];

  final app = server.app;
  if (app == null) {
    throw Exception('No app found');
  }

  final isDebug = server.context.mode.isDebug;

  /// Whether anything is annotated with `@Consumes`.
  ///
  /// Gates every piece of messaging wiring, so an app that uses none gets a
  /// generated server byte-identical to before this existed.
  final hasConsumers = server.routes.any((route) => route.consumers.isNotEmpty);

  /// Emitted inside `drainThisIsolate` only when there are consumers.
  ///
  /// Consumers drain first: they pull *new* work, so leaving them running
  /// while HTTP drains means the process keeps taking on messages it is about
  /// to abandon.
  const consumerDrainCall = '  await drainConsumers();\n\n';

  final main = Method(
    (b) => b
      ..name = 'main'
      ..returns = refer('void')
      ..requiredParameters.add(
        Parameter(
          (b) => b
            ..name = 'args'
            ..type = refer('List<String>'),
        ),
      )
      ..body = Block.of([
        if (isDebug)
          refer('hotReload').call([
            Method(
              (b) => b
                ..lambda = true
                ..body = refer(
                  'createServer',
                ).call([literalNull, refer('args')]).code,
            ).closure,
          ]).statement
        else
          refer('createServer').call([literalNull, refer('args')]).statement,
      ]),
  );

  // Worker entry sets a per-isolate flag so createServer keeps its positional
  // signature (used by tests as createServer(httpServer)), and stashes the
  // port the parent uses to tell this isolate to drain plus the index the
  // parent spawned it under.
  final workerEntrypoint = Method(
    (b) => b
      ..name = '_revaliWorkerMain'
      ..returns = refer('void')
      ..requiredParameters.add(
        Parameter(
          (p) => p
            ..name = 'boot'
            ..type = refer('List<Object>'),
        ),
      )
      ..body = const Code('''
_revaliIsWorker = true;
_revaliWorkerRegistration = boot[1] as SendPort;
_revaliIsolateIndex = boot[2] as int;
createServer(null, (boot[0] as List).cast<String>());
'''),
  );

  final createServer = Method(
    (b) => b
      ..name = 'createServer'
      ..returns = TypeReference(
        (p) => p
          ..symbol = (Future).name
          ..types.add(refer((HttpServer).name)),
      )
      ..modifier = MethodModifier.async
      ..optionalParameters.addAll([
        Parameter(
          (e) => e
            ..name = 'providedServer'
            ..named = false
            ..type = refer('${(HttpServer).name}?'),
        ),
        Parameter(
          (e) => e
            ..name = 'rawArgs'
            ..named = false
            ..defaultTo = const Code('const []')
            ..type = refer('List<String>'),
        ),
      ])
      ..body = Block.of([
        declareFinal('isWorker').assign(refer('_revaliIsWorker')).statement,
        refer('_revaliIsWorker').assign(literalFalse).statement,
        declareFinal(
          'workerRegistration',
        ).assign(refer('_revaliWorkerRegistration')).statement,
        refer('_revaliWorkerRegistration').assign(literalNull).statement,
        declareFinal(
          'isolateIndex',
        ).assign(refer('_revaliIsolateIndex')).statement,
        refer('_revaliIsolateIndex').assign(literalNum(0)).statement,
        declareFinal('args')
            .assign(
              refer((Args).name).newInstanceNamed('parse', [refer('rawArgs')]),
            )
            .statement,
        declareFinal(
          'app',
          type: refer((AppConfig).name),
        ).assign(createApp(app)).statement,
        // Emitted for every app, not just one with consumers: which isolate
        // this is, is a general fact about the isolate. It has to be published
        // before `runStartup` below, because `createBroker()` runs in there
        // and a broker that names itself from the identity would otherwise
        // read the unset default and call every worker isolate 0.
        const Code('''
IsolateIdentity.setCurrentForGeneratedCode(
  IsolateIdentity(index: isolateIndex, workerCount: app.workers),
);
'''),
        const Code('''
if (!isWorker && providedServer == null && app.workers > 1) {
  for (final isolate in _revaliWorkerIsolates) {
    isolate.kill(priority: Isolate.immediate);
  }
  _revaliWorkerIsolates = <Isolate>[];
  // Also drops the previous generation's command ports, which a hot reload
  // would otherwise accumulate on every restart.
  final registration = _revaliWorkerFleet.open();
  for (var i = 1; i < app.workers; i++) {
    _revaliWorkerIsolates.add(
      await Isolate.spawn(
        _revaliWorkerMain,
        <Object>[rawArgs, registration, i],
      ),
    );
  }
}
'''),
        refer('app')
            .property('runStartup')
            .call([
              Method(
                (b) => b
                  ..modifier = MethodModifier.async
                  ..body = Block.of([
                    declareFinal(
                      'server',
                      late: true,
                      type: refer('HttpServer'),
                    ).statement,
                    tryCatch(
                      refer('server')
                          .assign(
                            bindServerCall(
                              app: refer('app'),
                              providedServer: refer('providedServer'),
                              shared: isDebug
                                  ? literalTrue
                                  : refer('app')
                                        .property('workers')
                                        .greaterThan(literalNum(1))
                                        .or(refer('isWorker')),
                            ).awaited,
                          )
                          .statement,
                      Block.of([
                        refer('print').call([
                          literalString(r'Failed to bind server:\n$e'),
                        ]).statement,
                        refer('exit').call([literalNum(1)]).statement,
                      ]),
                    ),
                    const Code('\n'),
                    ...createDependencyInjection(server),
                    const Code('\n'),
                    ...createRoutesVariable(server),
                    const Code('\n'),
                    Block.of([
                      const Code('if ('),
                      refer('app').property('prefix').code,
                      const Code(' case'),
                      declareFinal('prefix?').code,
                      const Code(' when '),
                      refer('prefix').property('isNotEmpty').code,
                      const Code(') {'),
                      refer('_routes')
                          .assign(
                            literalList([
                              refer((Route).name).newInstance(
                                [refer('prefix')],
                                {'routes': refer('_routes')},
                              ),
                            ]),
                          )
                          .statement,
                      const Code('}'),
                    ]),
                    const Code('\n'),
                    // Declared before the router so the readiness probe can
                    // read the drain flag off it. The probe has to reflect
                    // the *current* shutdown state, so it closes over
                    // `inFlight` rather than a value read at startup.
                    declareFinal('inFlight')
                        .assign(refer((InFlightRequests).name).newInstance([]))
                        .statement,
                    const Code('\n'),
                    declareFinal('router')
                        .assign(
                          refer((Router).name).newInstance([], {
                            if (server.context.mode.isNotRelease)
                              'debug': literalTrue,
                            'inspect': refer('bool')
                                .property('fromEnvironment')
                                .call([literalString('REVALI_INSPECT')]),
                            'inspectLogPath': refer('String')
                                .property('fromEnvironment')
                                .call(
                                  [literalString('REVALI_INSPECT_LOG')],
                                  {'defaultValue': literalString('')},
                                ),
                            'routes': literalList([
                              refer('_routes').spread,
                              refer('public').spread,
                              // Added here rather than inside the prefix
                              // wrapping above, so probes answer on the bare
                              // paths an orchestrator is configured with.
                              refer('healthRoutes').call([], {
                                'settings': refer('app').property('health'),
                                'isDraining': Method(
                                  (b) => b
                                    ..lambda = true
                                    ..body = refer(
                                      'inFlight',
                                    ).property('isDraining').code,
                                ).closure,
                              }).spread,
                            ]),
                            if (app.observers.hasObservers)
                              'observers': literalList([
                                if (app.observers.types.expand((e) => e.types)
                                    case final observers
                                    when observers.isNotEmpty)
                                  for (final observer in observers)
                                    createClass(observer),
                                if (app.observers.mimics case final mimics
                                    when mimics.isNotEmpty)
                                  for (final type in mimics) createMimic(type),
                              ]),
                            'reflects': refer('reflects'),
                            'defaultResponses': refer(
                              'app',
                            ).property('defaultResponses'),
                            'trustedProxy': refer(
                              'app',
                            ).property('trustedProxy'),
                            'compression': refer('app').property('compression'),
                            // Gives every request its own scope, so
                            // `registerRequestScoped` dependencies are built
                            // once per request and disposed when it ends.
                            'di': refer('di'),
                            if (server.app case final app?
                                when app.globalRouteAnnotations.hasAnnotations)
                              'globalComponents':
                                  refer(
                                    (LifecycleComponentsImpl).name,
                                  ).newInstance([], {
                                    ...createModifierArgs(
                                      annotations: app.globalRouteAnnotations,
                                    ),
                                  }),
                          }),
                        )
                        .statement,
                    const Code('\n'),
                    refer('handleRouterRequests')
                        .call(
                          [
                            refer('server'),
                            refer('router'),
                            refer('router').property('close'),
                          ],
                          {'inFlight': refer('inFlight')},
                        )
                        .property('ignore')
                        .call([])
                        .statement,
                    const Code('\n'),
                    if (hasConsumers)
                      const Code('''
// Messaging is opt-in: an app that supplies no broker registers no
// consumers, even when handlers are annotated.
ConsumerRegistry? consumers;
if (await app.createBroker() case final broker?) {
  consumers = ConsumerRegistry(broker: broker, di: di);
  await registerConsumers(consumers, di);
}
'''),
                    const Code('\n'),
                    // Only a server this function created and owns may install
                    // process-wide signal handlers. A provided server belongs
                    // to the caller (tests pass a TestServer), and a worker
                    // isolate is told when to drain by the parent instead.
                    if (hasConsumers)
                      const Code('''
Future<void> drainConsumers() async {
  if (consumers case final registry?) {
    await registry.drain(app.shutdownTimeout);
    await registry.close();
  }
}
'''),
                    // The consumer drain is emitted only when there is one, so
                    // an app without messaging gets no stub and no call.
                    Code('''
Future<void> drainThisIsolate(Duration drainDelay) async {
${hasConsumers ? consumerDrainCall : ''}  await shutdownServer(
    server: server,
    inFlight: inFlight,
    timeout: app.shutdownTimeout,
    drainDelay: drainDelay,
    onStopped: app.onServerStopped,
    log: print,
  );
  router.close();
}

if (isWorker) {
  // Every isolate binds the same port and keeps its own in-flight set, so a
  // worker has to drain itself. It never watches signals: the parent waits
  // for the reply before exiting, and exit() would take the whole process
  // down with requests still running here.
  if (workerRegistration case final registration?) {
    listenForDrainCommands(registration, drainThisIsolate);
  }
} else if (providedServer == null && app.handleShutdownSignals) {
  listenForShutdown((signal) async {
    print('Received \$signal, shutting down...');
    // SIGINT is a human at a terminal who wants the process gone now.
    // SIGTERM is an orchestrator, which is who the delay exists for.
    final drainDelay = signal == ProcessSignal.sigterm
        ? app.drainDelay
        : Duration.zero;
    // Concurrently, so every isolate flags its own readiness at once and
    // probes report 503 across the whole fleet rather than only whichever
    // isolate happened to handle the signal.
    await Future.wait([
      _revaliWorkerFleet.drainAll(
        drainDelay: drainDelay,
        timeout: drainDelay + app.shutdownTimeout,
        log: print,
      ),
      drainThisIsolate(drainDelay),
    ]);
    exit(0);
  });
}
'''),
                    const Code('\n'),
                    const Code('if (!isWorker) {'),
                    refer('app').property('onServerStarted').call([
                      refer('server'),
                    ]).statement,
                    const Code('}'),
                    const Code('\n'),
                    refer('server').returned.statement,
                  ]),
              ).closure,
            ])
            .returned
            .statement,
      ]),
  );

  final parts = <Spec>[
    declareVar(
      '_revaliWorkerIsolates',
      type: TypeReference(
        (t) => t
          ..symbol = 'List'
          ..types.add(refer((Isolate).name)),
      ),
    ).assign(literalList([], refer((Isolate).name))).statement,
    declareVar(
      '_revaliIsWorker',
      type: refer('bool'),
    ).assign(literalFalse).statement,
    declareFinal(
      '_revaliWorkerFleet',
      type: refer((WorkerFleet).name),
    ).assign(refer((WorkerFleet).name).newInstance([])).statement,
    declareVar(
      '_revaliWorkerRegistration',
      type: refer('${(SendPort).name}?'),
    ).statement,
    // 0 on the parent, and on a worker until `_revaliWorkerMain` unpacks the
    // index the parent spawned it under.
    declareVar(
      '_revaliIsolateIndex',
      type: refer('int'),
    ).assign(literalNum(0)).statement,
    createBindServerMethod(),
    workerEntrypoint,
    main,
    createServer,
  ];

  final content = parts.map(formatter).join('\n');

  return '''
${imports.join('\n')}
$content''';
}