diff --git a/packages/celest/lib/celest.dart b/packages/celest/lib/celest.dart index 960cd3f..34e24de 100644 --- a/packages/celest/lib/celest.dart +++ b/packages/celest/lib/celest.dart @@ -19,10 +19,12 @@ export 'src/config/config_values.dart'; /// Core export 'src/core/annotations.dart'; export 'src/core/cloud_widget.dart'; -export 'src/core/context.dart'; export 'src/core/environment.dart'; export 'src/core/project.dart'; +/// Data +export 'src/data/database.dart'; + /// Functions export 'src/functions/cloud_api.dart'; export 'src/functions/cloud_function.dart'; diff --git a/packages/celest/lib/src/core/context.dart b/packages/celest/lib/src/core/context.dart index 8eb776c..b8420d1 100644 --- a/packages/celest/lib/src/core/context.dart +++ b/packages/celest/lib/src/core/context.dart @@ -23,7 +23,8 @@ import 'package:shelf/shelf.dart' as shelf; Context get context => Context.current; /// {@template celest.runtime.celest_context} -/// A per-request context object which propogates request information and common accessors to the Celest server environment. +/// A per-request context object which propogates request information and common +/// accessors to the Celest server environment. /// {@endtemplate} final class Context { /// {@macro celest.runtime.celest_context} diff --git a/packages/celest/lib/src/runtime/auth/supabase/supabase_token_verifier.dart b/packages/celest/lib/src/runtime/auth/supabase/supabase_token_verifier.dart index 1a7a33e..9e549a9 100644 --- a/packages/celest/lib/src/runtime/auth/supabase/supabase_token_verifier.dart +++ b/packages/celest/lib/src/runtime/auth/supabase/supabase_token_verifier.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:celest/celest.dart'; +import 'package:celest/src/core/context.dart'; import 'package:celest/src/runtime/auth/jwt/base64_raw_url.dart'; import 'package:crypto_keys/crypto_keys.dart' show AlgorithmIdentifier, Signature, SymmetricKey; diff --git a/packages/celest/lib/src/runtime/configuration.dart b/packages/celest/lib/src/runtime/configuration.dart index 9b1815e..a87425a 100644 --- a/packages/celest/lib/src/runtime/configuration.dart +++ b/packages/celest/lib/src/runtime/configuration.dart @@ -44,22 +44,27 @@ Future?> _loadJsonFromFileSystem( } /// Configures the environment in which Celest is running. -Future configure() async { +Future configure({ + ResolvedProject? config, +}) async { configureLogging(); final rootContext = Context.root; - final configJson = _loadJsonFromEnv(rootContext) ?? - await _loadJsonFromFileSystem(rootContext); - if (configJson == null) { - throw StateError( - 'No project configuration found. Create a celest.json file and set ' - 'CELEST_CONFIG with its path or CELEST_CONFIG_JSON with its contents.', - ); + if (config == null) { + final configJson = _loadJsonFromEnv(rootContext) ?? + await _loadJsonFromFileSystem(rootContext); + if (configJson == null) { + throw StateError( + 'No project configuration found. Create a celest.json file and set ' + 'CELEST_CONFIG with its path or CELEST_CONFIG_JSON with its contents.', + ); + } + + final configPb = pb.ResolvedProject()..mergeFromProto3Json(configJson); + config = ResolvedProject.fromProto(configPb); } - final configPb = pb.ResolvedProject()..mergeFromProto3Json(configJson); - final config = ResolvedProject.fromProto(configPb); Logger.root ..config('Loaded project configuration') ..config(config); diff --git a/packages/celest/lib/src/runtime/gcp/gcp.dart b/packages/celest/lib/src/runtime/gcp/gcp.dart index 7152628..1d61d83 100644 --- a/packages/celest/lib/src/runtime/gcp/gcp.dart +++ b/packages/celest/lib/src/runtime/gcp/gcp.dart @@ -1,7 +1,7 @@ @internal library; -import 'package:celest/celest.dart'; +import 'package:celest/src/core/context.dart'; import 'package:google_cloud/google_cloud.dart'; import 'package:meta/meta.dart'; diff --git a/packages/celest/lib/src/runtime/http/cloud_middleware.dart b/packages/celest/lib/src/runtime/http/cloud_middleware.dart index ad0886a..edbabf3 100644 --- a/packages/celest/lib/src/runtime/http/cloud_middleware.dart +++ b/packages/celest/lib/src/runtime/http/cloud_middleware.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:math'; import 'package:celest/celest.dart'; +import 'package:celest/src/core/context.dart'; import 'package:celest/src/runtime/http/middleware.dart'; import 'package:celest/src/runtime/json_utils.dart'; import 'package:celest_core/_internal.dart'; diff --git a/packages/celest/lib/src/runtime/http/logging.dart b/packages/celest/lib/src/runtime/http/logging.dart index 4acd754..0dd584e 100644 --- a/packages/celest/lib/src/runtime/http/logging.dart +++ b/packages/celest/lib/src/runtime/http/logging.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; -import 'package:celest/celest.dart'; +import 'package:celest/src/core/context.dart'; import 'package:celest/src/runtime/gcp/gcp.dart'; import 'package:celest_core/_internal.dart'; import 'package:cloud_http/cloud_http.dart'; diff --git a/packages/celest/lib/src/runtime/serve.dart b/packages/celest/lib/src/runtime/serve.dart index 6e1d093..6570dd8 100644 --- a/packages/celest/lib/src/runtime/serve.dart +++ b/packages/celest/lib/src/runtime/serve.dart @@ -6,13 +6,16 @@ import 'dart:io'; import 'package:async/async.dart'; import 'package:celest/celest.dart'; +import 'package:celest/src/core/context.dart'; import 'package:celest/src/runtime/configuration.dart'; import 'package:celest/src/runtime/gcp/gcp.dart'; import 'package:celest/src/runtime/http/cloud_middleware.dart'; import 'package:celest/src/runtime/http/middleware.dart'; import 'package:celest/src/runtime/json_utils.dart'; import 'package:celest/src/runtime/sse/sse_handler.dart'; +import 'package:celest_ast/celest_ast.dart' as ast; import 'package:celest_core/_internal.dart'; +import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart' hide Middleware; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_router/shelf_router.dart'; @@ -31,14 +34,30 @@ part 'targets.dart'; const int defaultCelestPort = 7777; /// Serves [targets] on a local HTTP server. -Future serve({ +/// +/// If [setup] is provided, it is called before the service starts with the root +/// [Context]. This can be used to configure dependencies and environment +/// parameters for the service. +Future serve({ required Map targets, + ast.ResolvedProject? config, + FutureOr Function(Context context)? setup, }) async { - await configure(); + await configure( + config: config, + ); final projectId = await googleCloudProject(); if (projectId != null) { Context.root.put(googleCloudProjectKey, projectId); } + if (setup != null) { + try { + await setup(Context.root); + } on Object catch (e, st) { + Logger.root.severe('Failed to setup', e, st); + rethrow; + } + } final router = Router()..get('/v1/healthz', (_) => Response.ok('OK')); for (final MapEntry(key: route, value: target) in targets.entries) { target._apply(router, route); @@ -61,10 +80,38 @@ Future serve({ poweredByHeader: 'Celest, the Flutter cloud platform', ); print('Serving on http://localhost:$port'); - await StreamGroup.merge([ - ProcessSignal.sigint.watch(), - if (!Platform.isWindows) ProcessSignal.sigterm.watch(), - ]).first; - print('Shutting down...'); - await server.close(); + unawaited( + StreamGroup.merge([ + ProcessSignal.sigint.watch(), + if (!Platform.isWindows) ProcessSignal.sigterm.watch(), + ]).first.then((signal) { + print('Received signal $signal'); + return server.close(force: true); + }), + ); + return CelestService._(server); +} + +/// {@template celest.runtime.celest_service} +/// A running instance of a Celest service. +/// {@endtemplate} +final class CelestService { + /// {@macro celest.runtime.celest_service} + CelestService._(this._server); + + final HttpServer _server; + + /// The address of the running service. + InternetAddress get address => _server.address; + + /// The port on which Celest is running. + int get port => _server.port; + + /// Closes the Celest service. + Future close({ + bool force = false, + }) { + print('Shutting down...'); + return _server.close(force: force); + } } diff --git a/packages/celest/test/runtime/serve_test.dart b/packages/celest/test/runtime/serve_test.dart new file mode 100644 index 0000000..a3a7ab8 --- /dev/null +++ b/packages/celest/test/runtime/serve_test.dart @@ -0,0 +1,63 @@ +@TestOn('vm') +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:celest/src/core/context.dart'; +import 'package:celest/src/runtime/serve.dart'; +import 'package:celest_ast/celest_ast.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +final config = ResolvedProject( + projectId: 'test', + environmentId: 'local', + sdkConfig: SdkConfiguration( + celest: Version.parse('1.0.0'), + dart: Sdk( + type: SdkType.dart, + version: Version.parse(Platform.version.split(' ').first), + ), + ), +); + +void main() { + group('serve', () { + test('calls setup', () async { + Context.root = Context.current; + + final setupCompleter = Completer(); + final service = await serve( + config: config, + targets: {}, + setup: setupCompleter.complete, + ); + addTearDown(service.close); + expect( + setupCompleter.future.timeout(const Duration(seconds: 1)), + completes, + ); + }); + + test('fails to start server if setup fails', () async { + Context.root = Context.current; + + final service = serve( + config: config, + targets: {}, + setup: (_) => throw StateError('Failed to setup'), + ); + await expectLater( + service, + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Failed to setup', + ), + ), + ); + }); + }); +} diff --git a/packages/celest_ast/lib/src/sdk_configuration.dart b/packages/celest_ast/lib/src/sdk_configuration.dart index c4bb936..e8502c8 100644 --- a/packages/celest_ast/lib/src/sdk_configuration.dart +++ b/packages/celest_ast/lib/src/sdk_configuration.dart @@ -28,8 +28,8 @@ abstract class SdkConfiguration required Version celest, required Sdk dart, Sdk? flutter, - required SdkType targetSdk, - required Iterable featureFlags, + SdkType targetSdk = SdkType.dart, + Iterable featureFlags = const [], }) { return _$SdkConfiguration._( celest: celest,