create-planet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +235 -0
- package/package.json +20 -0
- package/template/.claude/settings.json +5 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +96 -0
- package/template/LICENSE +201 -0
- package/template/PROTOCOL_DIAGRAMS.md +195 -0
- package/template/README.md +51 -0
- package/template/astro.config.mjs +20 -0
- package/template/package.json +38 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/map.js +329 -0
- package/template/public/planet.css +873 -0
- package/template/public/three.min.js +29395 -0
- package/template/schema.sql +22 -0
- package/template/scripts/inject-do-exports.js +61 -0
- package/template/scripts/simulate-universe.js +158 -0
- package/template/src/env.d.ts +21 -0
- package/template/src/lib/config.ts +52 -0
- package/template/src/lib/consensus.ts +127 -0
- package/template/src/lib/crypto.ts +89 -0
- package/template/src/lib/identity.ts +35 -0
- package/template/src/lib/travel.ts +75 -0
- package/template/src/pages/api/v1/control-ws.ts +22 -0
- package/template/src/pages/api/v1/port.ts +673 -0
- package/template/src/pages/control.astro +232 -0
- package/template/src/pages/index.astro +1009 -0
- package/template/src/pages/manifest.json.ts +37 -0
- package/template/src/tests/protocol.test.ts +158 -0
- package/template/src/tests/warp-links.test.ts +117 -0
- package/template/src/traffic-control.ts +52 -0
- package/template/tsconfig.json +9 -0
- package/template/vitest.config.ts +12 -0
- package/template/wrangler.build.jsonc +36 -0
- package/template/wrangler.dev.jsonc +41 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { env } from "cloudflare:workers";
|
|
3
|
+
import { PLANET_NAME, PLANET_DESCRIPTION } from "../lib/config";
|
|
4
|
+
|
|
5
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
6
|
+
// Robust helper to get simulation variables
|
|
7
|
+
const getSimVar = (name: string): string | undefined => {
|
|
8
|
+
if (env && (env as any)[name]) return (env as any)[name];
|
|
9
|
+
if (
|
|
10
|
+
typeof process !== "undefined" &&
|
|
11
|
+
process.env &&
|
|
12
|
+
(process.env as any)[name]
|
|
13
|
+
)
|
|
14
|
+
return (process.env as any)[name];
|
|
15
|
+
if (import.meta.env && (import.meta.env as any)[name])
|
|
16
|
+
return (import.meta.env as any)[name];
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const simUrl = getSimVar("PUBLIC_SIM_LANDING_SITE");
|
|
21
|
+
const simName = getSimVar("PUBLIC_SIM_PLANET_NAME");
|
|
22
|
+
|
|
23
|
+
const landingSite = simUrl || new URL(request.url).origin;
|
|
24
|
+
|
|
25
|
+
const manifest: any = {
|
|
26
|
+
name: simName || PLANET_NAME,
|
|
27
|
+
description: PLANET_DESCRIPTION,
|
|
28
|
+
landing_site: landingSite,
|
|
29
|
+
space_port: `${landingSite}/api/v1/port`,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return new Response(JSON.stringify(manifest), {
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
2
|
+
import { spawn, execSync, type ChildProcess } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PROJECT_ROOT = path.join(__dirname, "../../");
|
|
8
|
+
|
|
9
|
+
const NUM_PLANETS = 4; // Minimal quorum size (3f + 1 where f=1)
|
|
10
|
+
const BASE_PORT = 4000;
|
|
11
|
+
const BASE_INSPECTOR_PORT = 29229;
|
|
12
|
+
|
|
13
|
+
const allPlanets = Array.from({ length: NUM_PLANETS }, (_, i) => ({
|
|
14
|
+
name: `Towel ${i + 1}`,
|
|
15
|
+
url: `http://towel-${i + 1}.localhost:${BASE_PORT + i}`,
|
|
16
|
+
port: BASE_PORT + i,
|
|
17
|
+
id: i + 1,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const processes: ChildProcess[] = [];
|
|
21
|
+
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
console.log("Cleaning up processes...");
|
|
24
|
+
processes.forEach((p) => {
|
|
25
|
+
try {
|
|
26
|
+
p.kill();
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
execSync("pkill -f 'test-planet' || true");
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const startPlanet = (planet: (typeof allPlanets)[0]) => {
|
|
35
|
+
const { id, name, url, port } = planet;
|
|
36
|
+
const inspectorPort = BASE_INSPECTOR_PORT + id;
|
|
37
|
+
|
|
38
|
+
// Initialize Database first
|
|
39
|
+
console.log(`[${name}] Initializing database...`);
|
|
40
|
+
execSync(
|
|
41
|
+
`npx wrangler d1 execute planet_db --file=schema.sql -c wrangler.dev.jsonc --local --persist-to=.wrangler/state/test-planet-${id}`,
|
|
42
|
+
{
|
|
43
|
+
cwd: PROJECT_ROOT,
|
|
44
|
+
stdio: "inherit",
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const child = spawn(
|
|
49
|
+
"npx",
|
|
50
|
+
[
|
|
51
|
+
"wrangler",
|
|
52
|
+
"dev",
|
|
53
|
+
"--port",
|
|
54
|
+
port.toString(),
|
|
55
|
+
"--inspector-port",
|
|
56
|
+
inspectorPort.toString(),
|
|
57
|
+
"-c",
|
|
58
|
+
"wrangler.dev.jsonc",
|
|
59
|
+
"--persist-to",
|
|
60
|
+
`.wrangler/state/test-planet-${id}`,
|
|
61
|
+
"--var",
|
|
62
|
+
`PUBLIC_SIM_PLANET_NAME:"${name}"`,
|
|
63
|
+
"--var",
|
|
64
|
+
`PUBLIC_SIM_LANDING_SITE:"${url}"`,
|
|
65
|
+
"--var",
|
|
66
|
+
`PUBLIC_SIM_WARP_LINKS:'${JSON.stringify(allPlanets.filter((p) => p.url !== url).map((n) => ({ name: n.name, url: n.url })))}'`,
|
|
67
|
+
],
|
|
68
|
+
{
|
|
69
|
+
cwd: PROJECT_ROOT,
|
|
70
|
+
env: process.env,
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
shell: true,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
processes.push(child);
|
|
77
|
+
|
|
78
|
+
return new Promise<void>((resolve, reject) => {
|
|
79
|
+
let isReady = false;
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
if (!isReady)
|
|
82
|
+
reject(new Error(`[${name}] Timed out waiting for readiness`));
|
|
83
|
+
}, 45000); // Increased timeout for CI/slow environments
|
|
84
|
+
|
|
85
|
+
const handleData = (data: Buffer) => {
|
|
86
|
+
const str = data.toString();
|
|
87
|
+
process.stdout.write(`[${name}] ${str}`);
|
|
88
|
+
if (str.includes("Ready on")) {
|
|
89
|
+
isReady = true;
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
resolve();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
child.stdout?.on("data", handleData);
|
|
96
|
+
child.stderr?.on("data", handleData);
|
|
97
|
+
child.on("error", reject);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
describe("Federated Planets Protocol", () => {
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
console.log(`Starting ${NUM_PLANETS} planets...`);
|
|
104
|
+
const startupPromises = allPlanets.map((p) => startPlanet(p));
|
|
105
|
+
await Promise.all(startupPromises);
|
|
106
|
+
}, 120000); // 2 minute setup timeout
|
|
107
|
+
|
|
108
|
+
afterAll(() => {
|
|
109
|
+
cleanup();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should reach quorum when initiating a jump", async () => {
|
|
113
|
+
console.log("Initiating jump from Towel 1 to Towel 2...");
|
|
114
|
+
const response = await fetch(
|
|
115
|
+
`${allPlanets[0].url}/api/v1/port?action=initiate`,
|
|
116
|
+
{
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
ship_id: "TEST-SHIP",
|
|
121
|
+
destination_url: allPlanets[1].url,
|
|
122
|
+
departure_timestamp: Date.now(),
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(response.ok).toBe(true);
|
|
128
|
+
const data = (await response.json()) as any;
|
|
129
|
+
expect(data.plan.id).toBeDefined();
|
|
130
|
+
console.log("Plan initiated:", data.plan.id);
|
|
131
|
+
|
|
132
|
+
console.log("Monitoring events for QUORUM_REACHED...");
|
|
133
|
+
let quorumReached = false;
|
|
134
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
135
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
136
|
+
|
|
137
|
+
const eventsRes = await fetch(`${allPlanets[0].url}/api/v1/control-ws`);
|
|
138
|
+
if (eventsRes.ok) {
|
|
139
|
+
const events = (await eventsRes.json()) as any[];
|
|
140
|
+
const quorumEvent = events.find((e) => e.type === "QUORUM_REACHED");
|
|
141
|
+
const errorEvent = events.find((e) => e.type === "API_ERROR");
|
|
142
|
+
|
|
143
|
+
if (errorEvent) {
|
|
144
|
+
console.error("API ERROR DETECTED:", errorEvent.error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (quorumEvent) {
|
|
148
|
+
console.log("SUCCESS: Quorum reached!");
|
|
149
|
+
quorumReached = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.log(`Waiting for quorum... (attempt ${attempt + 1}/30)`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
expect(quorumReached).toBe(true);
|
|
157
|
+
}, 90000); // 90s test timeout
|
|
158
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
|
|
2
|
+
import { spawn, execSync, type ChildProcess } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PROJECT_ROOT = path.join(__dirname, "../../");
|
|
8
|
+
|
|
9
|
+
const TEST_PORT = 4500;
|
|
10
|
+
const TEST_HOST = "towel-warp-test.localhost";
|
|
11
|
+
const TEST_NAME = "Warp Test Planet";
|
|
12
|
+
const TEST_LINKS = [
|
|
13
|
+
{ name: "Test Link Alpha", url: "https://alpha.test" },
|
|
14
|
+
{ name: "Test Link Beta", url: "https://beta.test" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const processes: ChildProcess[] = [];
|
|
18
|
+
|
|
19
|
+
const cleanup = () => {
|
|
20
|
+
console.log("Cleaning up...");
|
|
21
|
+
processes.forEach((p) => {
|
|
22
|
+
try {
|
|
23
|
+
p.kill();
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
execSync("pkill -f 'warp-test' || true");
|
|
28
|
+
} catch (e) {}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("Warp Links Configuration", () => {
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
console.log(`[${TEST_NAME}] Initializing database...`);
|
|
34
|
+
execSync(
|
|
35
|
+
`npx wrangler d1 execute planet_db --file=schema.sql -c wrangler.dev.jsonc --local --persist-to=.wrangler/state/warp-test`,
|
|
36
|
+
{
|
|
37
|
+
cwd: PROJECT_ROOT,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
console.log(`Starting ${TEST_NAME} on http://${TEST_HOST}:${TEST_PORT}...`);
|
|
43
|
+
|
|
44
|
+
const child = spawn(
|
|
45
|
+
"npx",
|
|
46
|
+
[
|
|
47
|
+
"wrangler",
|
|
48
|
+
"dev",
|
|
49
|
+
"--port",
|
|
50
|
+
TEST_PORT.toString(),
|
|
51
|
+
"-c",
|
|
52
|
+
"wrangler.dev.jsonc",
|
|
53
|
+
"--persist-to",
|
|
54
|
+
".wrangler/state/warp-test",
|
|
55
|
+
"--var",
|
|
56
|
+
`PUBLIC_SIM_PLANET_NAME:"${TEST_NAME}"`,
|
|
57
|
+
"--var",
|
|
58
|
+
`PUBLIC_SIM_LANDING_SITE:"http://${TEST_HOST}:${TEST_PORT}"`,
|
|
59
|
+
"--var",
|
|
60
|
+
`PUBLIC_SIM_WARP_LINKS:'${JSON.stringify(TEST_LINKS)}'`,
|
|
61
|
+
],
|
|
62
|
+
{
|
|
63
|
+
cwd: PROJECT_ROOT,
|
|
64
|
+
env: { ...process.env },
|
|
65
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
66
|
+
shell: true,
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
processes.push(child);
|
|
71
|
+
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
let isReady = false;
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
if (!isReady)
|
|
76
|
+
reject(new Error(`[${TEST_NAME}] Timed out waiting for readiness`));
|
|
77
|
+
}, 45000);
|
|
78
|
+
|
|
79
|
+
const handleData = (data: Buffer) => {
|
|
80
|
+
const str = data.toString();
|
|
81
|
+
process.stdout.write(`[${TEST_NAME}] ${str}`);
|
|
82
|
+
if (str.includes("Ready on")) {
|
|
83
|
+
isReady = true;
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
resolve();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
child.stdout?.on("data", handleData);
|
|
90
|
+
child.stderr?.on("data", handleData);
|
|
91
|
+
child.on("error", reject);
|
|
92
|
+
});
|
|
93
|
+
}, 60000);
|
|
94
|
+
|
|
95
|
+
afterAll(() => {
|
|
96
|
+
cleanup();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should override planet name in manifest", async () => {
|
|
100
|
+
const response = await fetch(
|
|
101
|
+
`http://${TEST_HOST}:${TEST_PORT}/manifest.json`,
|
|
102
|
+
);
|
|
103
|
+
expect(response.ok).toBe(true);
|
|
104
|
+
const manifest = (await response.json()) as any;
|
|
105
|
+
expect(manifest.name).toBe(TEST_NAME);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should correctly override warp links on homepage", async () => {
|
|
109
|
+
const homeRes = await fetch(`http://${TEST_HOST}:${TEST_PORT}/`);
|
|
110
|
+
expect(homeRes.ok).toBe(true);
|
|
111
|
+
const html = await homeRes.text();
|
|
112
|
+
|
|
113
|
+
expect(html).toContain("Test Link Alpha");
|
|
114
|
+
expect(html).toContain("Test Link Beta");
|
|
115
|
+
expect(html).not.toContain("Aether Reach"); // Default link should be absent
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
|
|
3
|
+
export default class TrafficControl extends DurableObject {
|
|
4
|
+
private sessions: Set<WebSocket> = new Set();
|
|
5
|
+
private events: any[] = [];
|
|
6
|
+
|
|
7
|
+
constructor(state: any, env: any) {
|
|
8
|
+
super(state, env);
|
|
9
|
+
console.log("[TrafficControl] Initialized");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async fetch(request: Request) {
|
|
13
|
+
const url = new URL(request.url);
|
|
14
|
+
|
|
15
|
+
if (url.pathname === "/events" && request.method === "POST") {
|
|
16
|
+
const event = await request.json();
|
|
17
|
+
this.events.push({ ...event, timestamp: Date.now() });
|
|
18
|
+
if (this.events.length > 50) this.events.shift();
|
|
19
|
+
this.broadcast(JSON.stringify(event));
|
|
20
|
+
return new Response("OK");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
24
|
+
const pair = new WebSocketPair();
|
|
25
|
+
const [client, server] = Object.values(pair);
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
server.accept();
|
|
28
|
+
this.sessions.add(server);
|
|
29
|
+
|
|
30
|
+
server.send(JSON.stringify({ type: "history", data: this.events }));
|
|
31
|
+
|
|
32
|
+
server.addEventListener("close", () => this.sessions.delete(server));
|
|
33
|
+
server.addEventListener("error", () => this.sessions.delete(server));
|
|
34
|
+
|
|
35
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Response(JSON.stringify(this.events), {
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
broadcast(message: string) {
|
|
44
|
+
for (const ws of this.sessions) {
|
|
45
|
+
try {
|
|
46
|
+
ws.send(message);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
this.sessions.delete(ws);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
testTimeout: 90000, // Increased for full builds
|
|
8
|
+
hookTimeout: 60000,
|
|
9
|
+
fileParallelism: true,
|
|
10
|
+
include: ["src/tests/**/*.test.{ts,js}"],
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "federated-planet",
|
|
3
|
+
"compatibility_date": "2026-03-31",
|
|
4
|
+
"assets": {
|
|
5
|
+
"directory": "dist/client",
|
|
6
|
+
"binding": "STATIC_ASSETS",
|
|
7
|
+
},
|
|
8
|
+
"d1_databases": [
|
|
9
|
+
{
|
|
10
|
+
"binding": "DB",
|
|
11
|
+
"database_name": "planet_db",
|
|
12
|
+
"database_id": "your-d1-id-here", // To be replaced during deployment
|
|
13
|
+
"migrations_dir": "migrations",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
"kv_namespaces": [
|
|
17
|
+
{
|
|
18
|
+
"binding": "KV",
|
|
19
|
+
"id": "your-kv-id-here", // To be replaced during deployment
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
"durable_objects": {
|
|
23
|
+
"bindings": [
|
|
24
|
+
{
|
|
25
|
+
"name": "TRAFFIC_CONTROL",
|
|
26
|
+
"class_name": "TrafficControl",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
"migrations": [
|
|
31
|
+
{
|
|
32
|
+
"tag": "v1",
|
|
33
|
+
"new_classes": ["TrafficControl"],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "federated-planet",
|
|
3
|
+
"main": "dist/server/entry.mjs",
|
|
4
|
+
"compatibility_date": "2026-03-31",
|
|
5
|
+
"assets": {
|
|
6
|
+
"directory": "dist/client",
|
|
7
|
+
"binding": "STATIC_ASSETS",
|
|
8
|
+
},
|
|
9
|
+
"d1_databases": [
|
|
10
|
+
{
|
|
11
|
+
"binding": "DB",
|
|
12
|
+
"database_name": "planet_db",
|
|
13
|
+
"database_id": "your-d1-id-here",
|
|
14
|
+
"migrations_dir": "migrations",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
"kv_namespaces": [
|
|
18
|
+
{
|
|
19
|
+
"binding": "KV",
|
|
20
|
+
"id": "your-kv-id-here",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
"durable_objects": {
|
|
24
|
+
"bindings": [
|
|
25
|
+
{
|
|
26
|
+
"name": "TRAFFIC_CONTROL",
|
|
27
|
+
"class_name": "TrafficControl",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
"vars": {
|
|
32
|
+
"WARP_MS_PER_FY": "10000",
|
|
33
|
+
"DEPARTURE_BUFFER_MS": "5000",
|
|
34
|
+
},
|
|
35
|
+
"migrations": [
|
|
36
|
+
{
|
|
37
|
+
"tag": "v1",
|
|
38
|
+
"new_classes": ["TrafficControl"],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|