exec method
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してください。");
}