Skip to content

Commit

Permalink
chore(runtime): Add setup parameter to serve
Browse files Browse the repository at this point in the history
This allows generated backends to provide project-specific configuration at startup.
  • Loading branch information
dnys1 committed Oct 10, 2024
1 parent 90bae7e commit 85b1f50
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 24 deletions.
4 changes: 3 additions & 1 deletion packages/celest/lib/celest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion packages/celest/lib/src/core/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 15 additions & 10 deletions packages/celest/lib/src/runtime/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,27 @@ Future<Map<String, Object?>?> _loadJsonFromFileSystem(
}

/// Configures the environment in which Celest is running.
Future<void> configure() async {
Future<void> 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);
Expand Down
2 changes: 1 addition & 1 deletion packages/celest/lib/src/runtime/gcp/gcp.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
1 change: 1 addition & 0 deletions packages/celest/lib/src/runtime/http/cloud_middleware.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/celest/lib/src/runtime/http/logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
63 changes: 55 additions & 8 deletions packages/celest/lib/src/runtime/serve.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,14 +34,30 @@ part 'targets.dart';
const int defaultCelestPort = 7777;

/// Serves [targets] on a local HTTP server.
Future<void> 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<CelestService> serve({
required Map<String, CloudFunctionTarget> targets,
ast.ResolvedProject? config,
FutureOr<void> 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);
Expand All @@ -61,10 +80,38 @@ Future<void> 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<void> close({
bool force = false,
}) {
print('Shutting down...');
return _server.close(force: force);
}
}
63 changes: 63 additions & 0 deletions packages/celest/test/runtime/serve_test.dart
Original file line number Diff line number Diff line change
@@ -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<void>();
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<StateError>().having(
(e) => e.message,
'message',
'Failed to setup',
),
),
);
});
});
}
4 changes: 2 additions & 2 deletions packages/celest_ast/lib/src/sdk_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ abstract class SdkConfiguration
required Version celest,
required Sdk dart,
Sdk? flutter,
required SdkType targetSdk,
required Iterable<FeatureFlag> featureFlags,
SdkType targetSdk = SdkType.dart,
Iterable<FeatureFlag> featureFlags = const [],
}) {
return _$SdkConfiguration._(
celest: celest,
Expand Down

0 comments on commit 85b1f50

Please sign in to comment.