exec method

  1. @override
Future<void> exec(
  1. ExecContext context
)
override

Run command.

The contents of katana.yaml and the arguments of the command are passed to context.

コマンドを実行します。

contextにkatana.yamlの内容やコマンドの引数が渡されます。

Implementation

@override
Future<void> exec(ExecContext context) async {
  if (context.flavorContext?.explicit != true) {
    throw StateError("--flavorを指定してください。");
  }
  final config =
      context.yaml.getAsMap("cloudflare").getAsMap("durable_object");
  final manifest = jsonDecode(
      File(config.get("schema", "do/schema/schema.json")).readAsStringSync());
  final binding = config.get("binding", "MASAMUNE_DO").toString();
  final coordinator =
      config.get("coordinator_binding", "MASAMUNE_QUEUE").toString();
  final file = File("cloudflare/wrangler.jsonc");
  if (!file.existsSync()) {
    throw StateError("先にCloudflare Workerを作成してください。");
  }
  var source = WranglerEnvironmentSynchronizer.transformEnvironment(
      WranglerEnvironmentSynchronizer.synchronize(file.readAsStringSync(),
          flavor: context.flavorContext!.flavor.name,
          workerName: context.yaml
              .getAsMap("cloudflare")
              .get("project_id", "")
              .toString()),
      flavor: context.flavorContext!.flavor.name,
      transform: (s) => updateDurableObjectBindings(s,
          binding: binding,
          className: "MasamuneUserDatabase",
          coordinatorBinding: coordinator,
          coordinatorClass: "MasamuneQueueCoordinator",
          tag: config
              .get("class_migration_tag", "masamune-do-v1")
              .toString()));
  final sharedConfig = config.getAsMap("shared_hub");
  final sharedEnabled = sharedConfig.get("enable", false);
  final sharedBinding =
      sharedConfig.get("binding", "MASAMUNE_SHARED_HUB").toString();
  final dynamic sharedShards = sharedConfig["shards"] ?? 4;
  final sharedGeneration = sharedConfig.get("generation", "v1").toString();
  if (sharedEnabled &&
      (sharedShards is! int ||
          sharedShards < 1 ||
          sharedShards > 32 ||
          !RegExp(r"^[A-Za-z0-9_-]{1,64}$").hasMatch(sharedGeneration))) {
    throw StateError("共有hubは1〜32 shards、有効なgenerationを指定してください。");
  }
  if (!sharedEnabled && source.contains('"MasamuneSharedHub"')) {
    throw StateError("既存共有hubの無効化にはclass lifecycleの明示的な移行が必要です。");
  }
  if (sharedEnabled) {
    source = WranglerEnvironmentSynchronizer.transformEnvironment(source,
        flavor: context.flavorContext!.flavor.name,
        transform: (s) => updateSharedHubBinding(s,
            binding: sharedBinding,
            tag: sharedConfig
                .get("class_migration_tag", "masamune-shared-hub-v1")
                .toString()));
  }
  final sharedJson = jsonEncode({
    "binding": sharedBinding,
    "shards": sharedShards,
    "generation": sharedGeneration
  });
  final vectorSpecs = <String, Map<String, dynamic>>{};
  for (final table in manifest["tables"] as List) {
    for (final raw in (table["vectors"] as List? ?? const [])) {
      final vector = Map<String, dynamic>.from(raw as Map);
      final bindingName = vector["binding"] as String;
      final previous = vectorSpecs[bindingName];
      if (previous != null &&
          (previous["dimensions"] != vector["dimensions"] ||
              previous["metric"] != vector["metric"])) {
        throw StateError("同じVectorize bindingの次元・距離指標が一致しません。");
      }
      vectorSpecs[bindingName] = vector;
    }
  }
  if (vectorSpecs.isNotEmpty) {
    final cloudflare = context.yaml.getAsMap("cloudflare");
    final flavor = context.flavorContext!.flavor.name;
    final nameConfig = config.getAsMap("vectorize").getAsMap(flavor);
    source = await ensureCloudflareVectorizeBindings(
      source: source,
      wrangler: context.yaml.getAsMap("bin").get("wrangler", "wrangler"),
      account: config.get("account_id", cloudflare.get("account_id", "")),
      flavor: flavor,
      specs: vectorSpecs,
      names: {
        for (final bindingName in vectorSpecs.keys)
          bindingName: nameConfig.get(bindingName, "").toString(),
      },
    );
  }
  await file.writeAsString(source);
  await File("cloudflare/src/do_schema.json")
      .writeAsString(jsonEncode(manifest));
  final approved =
      File(config.get("approved", "cloudflare/src/do_revisions.json"));
  if (!approved.existsSync()) {
    await approved.parent.create(recursive: true);
    await approved.writeAsString("[]\n");
  }
  if (approved.path != "cloudflare/src/do_revisions.json") {
    throw StateError("approvedはcloudflare/src/do_revisions.jsonを指定してください。");
  }
  final generated = File("cloudflare/src/masamune_do.ts");
  await generated.writeAsString('''
// Katana CLI生成。Hibernation handlersは基底クラスが提供する。
// 移行元の設定はindex.tsからsetDoSourceへ登録する。
import { DurableObjectDatabase, QueueCoordinator, type DoConfig${sharedEnabled ? ", SharedHub, type SharedHubOptions" : ""} } from "@mathrunet/masamune_cloudflare_do";
import manifest from "./do_schema.json";
import revisions from "./do_revisions.json";
let sourceFactory: ((env: unknown) => DoConfig["source"]) | undefined;
export function setDoSource(factory: (env: unknown) => DoConfig["source"]) { sourceFactory = factory; }
export class MasamuneUserDatabase extends DurableObjectDatabase {
constructor(ctx: any, env: any) { super(ctx, env, {schemaManifest: manifest, revisions, source: sourceFactory?.(env)${sharedEnabled ? ", shared: $sharedJson" : ""}} as DoConfig); }
}
export class MasamuneQueueCoordinator extends QueueCoordinator {}
${sharedEnabled ? """
export class MasamuneSharedHub extends SharedHub {}
let sharedAuthorize: SharedHubOptions["authorize"] = () => false;
export function setSharedHubAuthorize(authorize: SharedHubOptions["authorize"]) { sharedAuthorize = authorize; }
export const sharedHubOptions: SharedHubOptions = {...$sharedJson, authorize: (context, scope) => sharedAuthorize(context, scope)};
""" : ""}
''');
  final index = File(cloudflareEdgeEntryPath);
  var text = index.readAsStringSync();
  final exports =
      'export { MasamuneUserDatabase, MasamuneQueueCoordinator${sharedEnabled ? ", MasamuneSharedHub" : ""} } from "./masamune_do";';
  final exportPattern = RegExp(
      r'''export\s*\{\s*MasamuneUserDatabase\s*,\s*MasamuneQueueCoordinator\s*(?:,\s*MasamuneSharedHub\s*)?\}\s*from\s*["']\./masamune_do["'];?\r?\n?''');
  text = "$exports\n${text.replaceAll(exportPattern, "")}";
  final importPattern = RegExp(
      r'''import\s*\*\s*as\s+durableObject\s+from\s*["']@mathrunet/masamune_cloudflare_do["'];?\r?\n?''');
  if (importPattern.hasMatch(text)) {
    text =
        'import * as durableObject from "@mathrunet/masamune_cloudflare_do";\n${text.replaceAll(importPattern, "")}';
  }
  final sharedImport = RegExp(
      r"""import\s*\{\s*sharedHubOptions\s*\}\s*from\s*["']\./masamune_do["'];?\r?\n?""");
  text = text.replaceAll(sharedImport, "");
  if (sharedEnabled) {
    text = 'import { sharedHubOptions } from "./masamune_do";\n$text';
  }
  await index.writeAsString(text);
  final databases =
      (manifest["tables"] as List).map((t) => t["database"]).toSet().toList();
  if (!await applyCloudflareWorkersFunctions(
      alias: "durableObject",
      package: "@mathrunet/masamune_cloudflare_do",
      functions: {
        "durableObject.Functions.durableObjectSockets":
            "durableObject.Functions.durableObjectSockets({ binding: ${jsonEncode(binding)}, databases: ${jsonEncode(databases)}${sharedEnabled ? ", shared: sharedHubOptions" : ""} }),",
        "durableObject.Functions.durableObject":
            "durableObject.Functions.durableObject({ binding: ${jsonEncode(binding)}, databases: ${jsonEncode(databases)}${sharedEnabled ? ", shared: sharedHubOptions" : ""} }),"
      })) {
    throw StateError("Worker登録位置がありません。");
  }
  label("DO設定完了。migrateで承認後、Workerをdeployしてください。");
}