create-planet 1.0.2 → 1.0.5
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/README.md +2 -3
- package/index.js +21 -45
- package/package.json +6 -1
- package/template/.github/workflows/publish.yml +6 -4
- package/template/AGENTS.md +8 -8
- package/template/PROTOCOL_DIAGRAMS.md +39 -40
- package/template/package.json +2 -2
- package/template/scripts/simulate-universe.js +0 -10
- package/template/src/env.d.ts +0 -2
- package/template/src/lib/config.ts +8 -2
- package/template/src/lib/consensus.ts +14 -8
- package/template/src/lib/do-storage.ts +39 -0
- package/template/src/lib/identity.ts +12 -10
- package/template/src/pages/api/v1/port.ts +172 -102
- package/template/src/pages/index.astro +26 -29
- package/template/src/tests/protocol.test.ts +162 -9
- package/template/src/tests/warp-links.test.ts +5 -8
- package/template/src/traffic-control.ts +104 -1
- package/template/wrangler.build.jsonc +1 -15
- package/template/wrangler.dev.jsonc +2 -15
- package/template/schema.sql +0 -22
|
@@ -6,6 +6,7 @@ import { CryptoCore } from "../../../lib/crypto";
|
|
|
6
6
|
import { PlanetIdentity } from "../../../lib/identity";
|
|
7
7
|
import { ConsensusEngine, type TravelPlan } from "../../../lib/consensus";
|
|
8
8
|
import { WARP_LINKS, PLANET_NAME } from "../../../lib/config";
|
|
9
|
+
import { doQuery, doExec } from "../../../lib/do-storage";
|
|
9
10
|
import { env } from "cloudflare:workers";
|
|
10
11
|
|
|
11
12
|
const InitiateSchema = z.object({
|
|
@@ -24,6 +25,7 @@ const TravelPlanSchema = z.object({
|
|
|
24
25
|
status: z.enum(["PREPARING", "PLAN_ACCEPTED"]),
|
|
25
26
|
traffic_controllers: z.array(z.string()),
|
|
26
27
|
signatures: z.record(z.string()),
|
|
28
|
+
origin_lists_dest: z.boolean().optional(),
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
// Returns ms per Flight-Year. Default: 1 hour (production). Override with WARP_MS_PER_FY for dev.
|
|
@@ -92,7 +94,7 @@ async function broadcastEvent(
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
export const GET: APIRoute = async ({ request }) => {
|
|
95
|
-
const {
|
|
97
|
+
const { TRAFFIC_CONTROL } = env as any;
|
|
96
98
|
const url = new URL(request.url);
|
|
97
99
|
const action = url.searchParams.get("action");
|
|
98
100
|
|
|
@@ -103,18 +105,18 @@ export const GET: APIRoute = async ({ request }) => {
|
|
|
103
105
|
status: 400,
|
|
104
106
|
});
|
|
105
107
|
}
|
|
106
|
-
const manifest = await discoverSpacePort(targetUrl,
|
|
108
|
+
const manifest = await discoverSpacePort(targetUrl, TRAFFIC_CONTROL);
|
|
107
109
|
return new Response(JSON.stringify({ has_space_port: manifest !== null }), {
|
|
108
110
|
status: 200,
|
|
109
111
|
});
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
if (action === "neighbors") {
|
|
113
|
-
if (!
|
|
115
|
+
if (!TRAFFIC_CONTROL || WARP_LINKS.length === 0) {
|
|
114
116
|
return new Response(JSON.stringify({ neighbors: [] }), { status: 200 });
|
|
115
117
|
}
|
|
116
118
|
const results = await Promise.all(
|
|
117
|
-
WARP_LINKS.map((l) => discoverSpacePort(l.url,
|
|
119
|
+
WARP_LINKS.map((l) => discoverSpacePort(l.url, TRAFFIC_CONTROL)),
|
|
118
120
|
);
|
|
119
121
|
const neighbors = results.filter((n): n is PlanetManifest => n !== null);
|
|
120
122
|
return new Response(JSON.stringify({ neighbors }), { status: 200 });
|
|
@@ -126,7 +128,7 @@ export const GET: APIRoute = async ({ request }) => {
|
|
|
126
128
|
};
|
|
127
129
|
|
|
128
130
|
export const POST: APIRoute = async ({ request }) => {
|
|
129
|
-
const {
|
|
131
|
+
const { TRAFFIC_CONTROL } = env as any;
|
|
130
132
|
const url = new URL(request.url);
|
|
131
133
|
const action = url.searchParams.get("action");
|
|
132
134
|
const senderOrigin =
|
|
@@ -150,37 +152,13 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
150
152
|
|
|
151
153
|
switch (action) {
|
|
152
154
|
case "initiate":
|
|
153
|
-
return await handleInitiate(
|
|
154
|
-
request,
|
|
155
|
-
KV,
|
|
156
|
-
DB,
|
|
157
|
-
localPlanet,
|
|
158
|
-
TRAFFIC_CONTROL,
|
|
159
|
-
);
|
|
155
|
+
return await handleInitiate(request, TRAFFIC_CONTROL, localPlanet);
|
|
160
156
|
case "prepare":
|
|
161
|
-
return await handlePrepare(
|
|
162
|
-
request,
|
|
163
|
-
KV,
|
|
164
|
-
DB,
|
|
165
|
-
localPlanet,
|
|
166
|
-
TRAFFIC_CONTROL,
|
|
167
|
-
);
|
|
157
|
+
return await handlePrepare(request, TRAFFIC_CONTROL, localPlanet);
|
|
168
158
|
case "register":
|
|
169
|
-
return await handleRegister(
|
|
170
|
-
request,
|
|
171
|
-
KV,
|
|
172
|
-
DB,
|
|
173
|
-
localPlanet,
|
|
174
|
-
TRAFFIC_CONTROL,
|
|
175
|
-
);
|
|
159
|
+
return await handleRegister(request, TRAFFIC_CONTROL, localPlanet);
|
|
176
160
|
case "commit":
|
|
177
|
-
return await handleCommit(
|
|
178
|
-
request,
|
|
179
|
-
KV,
|
|
180
|
-
DB,
|
|
181
|
-
localPlanet,
|
|
182
|
-
TRAFFIC_CONTROL,
|
|
183
|
-
);
|
|
161
|
+
return await handleCommit(request, TRAFFIC_CONTROL, localPlanet);
|
|
184
162
|
default:
|
|
185
163
|
return new Response(JSON.stringify({ error: "Invalid action" }), {
|
|
186
164
|
status: 400,
|
|
@@ -200,24 +178,21 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
200
178
|
|
|
201
179
|
async function discoverSpacePort(
|
|
202
180
|
landingSiteUrl: string,
|
|
203
|
-
|
|
181
|
+
TRAFFIC_CONTROL: DurableObjectNamespace,
|
|
204
182
|
): Promise<PlanetManifest | null> {
|
|
205
183
|
try {
|
|
206
|
-
const cached
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`,
|
|
212
|
-
)
|
|
213
|
-
.bind(landingSiteUrl, Date.now() - 3600000)
|
|
214
|
-
.first();
|
|
184
|
+
const cached = await doQuery(
|
|
185
|
+
TRAFFIC_CONTROL,
|
|
186
|
+
`SELECT * FROM traffic_controllers WHERE planet_url = ? AND last_manifest_fetch > ?`,
|
|
187
|
+
[landingSiteUrl, Date.now() - 3600000],
|
|
188
|
+
);
|
|
215
189
|
|
|
216
|
-
if (cached) {
|
|
190
|
+
if (cached.length > 0) {
|
|
191
|
+
const row: any = cached[0];
|
|
217
192
|
return {
|
|
218
|
-
name:
|
|
219
|
-
landing_site:
|
|
220
|
-
space_port:
|
|
193
|
+
name: row.name,
|
|
194
|
+
landing_site: row.planet_url,
|
|
195
|
+
space_port: row.space_port_url,
|
|
221
196
|
};
|
|
222
197
|
}
|
|
223
198
|
|
|
@@ -251,14 +226,11 @@ async function discoverSpacePort(
|
|
|
251
226
|
space_port: remoteManifest.space_port,
|
|
252
227
|
};
|
|
253
228
|
|
|
254
|
-
await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
.bind(planet.landing_site, planet.name, planet.space_port, Date.now())
|
|
261
|
-
.run();
|
|
229
|
+
await doExec(
|
|
230
|
+
TRAFFIC_CONTROL,
|
|
231
|
+
`INSERT OR REPLACE INTO traffic_controllers (planet_url, name, space_port_url, last_manifest_fetch) VALUES (?, ?, ?, ?)`,
|
|
232
|
+
[planet.landing_site, planet.name, planet.space_port, Date.now()],
|
|
233
|
+
);
|
|
262
234
|
|
|
263
235
|
return planet;
|
|
264
236
|
} catch (e) {
|
|
@@ -269,10 +241,8 @@ async function discoverSpacePort(
|
|
|
269
241
|
|
|
270
242
|
async function handleInitiate(
|
|
271
243
|
request: Request,
|
|
272
|
-
|
|
273
|
-
DB: D1Database,
|
|
244
|
+
TRAFFIC_CONTROL: DurableObjectNamespace,
|
|
274
245
|
localPlanet: any,
|
|
275
|
-
TRAFFIC_CONTROL: any,
|
|
276
246
|
) {
|
|
277
247
|
const body = await request.json();
|
|
278
248
|
const data = InitiateSchema.parse(body);
|
|
@@ -299,8 +269,8 @@ async function handleInitiate(
|
|
|
299
269
|
const endTimestamp = startTimestamp + travelTimeHours * msPerFY();
|
|
300
270
|
|
|
301
271
|
const discoveryPromises = [
|
|
302
|
-
discoverSpacePort(data.destination_url,
|
|
303
|
-
...WARP_LINKS.map((l) => discoverSpacePort(l.url,
|
|
272
|
+
discoverSpacePort(data.destination_url, TRAFFIC_CONTROL),
|
|
273
|
+
...WARP_LINKS.map((l) => discoverSpacePort(l.url, TRAFFIC_CONTROL)),
|
|
304
274
|
];
|
|
305
275
|
const [destManifest, ...neighborResults] =
|
|
306
276
|
await Promise.all(discoveryPromises);
|
|
@@ -341,6 +311,64 @@ async function handleInitiate(
|
|
|
341
311
|
);
|
|
342
312
|
}
|
|
343
313
|
|
|
314
|
+
// Enforce planet-funded shuttle limits based on neighbor relationship
|
|
315
|
+
const originListsDest = originNeighbors.some(
|
|
316
|
+
(n) => n.landing_site === destManifest.landing_site,
|
|
317
|
+
);
|
|
318
|
+
const destListsOrigin = destNeighbors.some(
|
|
319
|
+
(n) => n.landing_site === localPlanet.landing_site,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const shuttleLimit =
|
|
323
|
+
originListsDest && destListsOrigin
|
|
324
|
+
? 2
|
|
325
|
+
: originListsDest || destListsOrigin
|
|
326
|
+
? 1
|
|
327
|
+
: 0;
|
|
328
|
+
|
|
329
|
+
const activeRows = await doQuery(
|
|
330
|
+
TRAFFIC_CONTROL,
|
|
331
|
+
`SELECT COUNT(*) as count FROM travel_plans
|
|
332
|
+
WHERE ((origin_url = ? AND destination_url = ?)
|
|
333
|
+
OR (origin_url = ? AND destination_url = ?))
|
|
334
|
+
AND end_timestamp > ?`,
|
|
335
|
+
[
|
|
336
|
+
localPlanet.landing_site,
|
|
337
|
+
destManifest.landing_site,
|
|
338
|
+
destManifest.landing_site,
|
|
339
|
+
localPlanet.landing_site,
|
|
340
|
+
Date.now(),
|
|
341
|
+
],
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const activeShuttles = (activeRows[0] as any)?.count ?? 0;
|
|
345
|
+
|
|
346
|
+
console.log(
|
|
347
|
+
`[${localPlanet.name}] Shuttle limit check: ${activeShuttles}/${shuttleLimit} active (${localPlanet.landing_site} ↔ ${destManifest.landing_site})`,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const bypassAllowed = (env as any).ALLOW_TEST_SHUTTLE_BYPASS === "true";
|
|
351
|
+
const bypassRequested =
|
|
352
|
+
request.headers.get("X-Bypass-Shuttle-Limit") === "true";
|
|
353
|
+
|
|
354
|
+
if (activeShuttles >= shuttleLimit && !(bypassAllowed && bypassRequested)) {
|
|
355
|
+
const relationship =
|
|
356
|
+
originListsDest && destListsOrigin
|
|
357
|
+
? "mutual_neighbors"
|
|
358
|
+
: originListsDest || destListsOrigin
|
|
359
|
+
? "one_sided_neighbors"
|
|
360
|
+
: "non_neighbors";
|
|
361
|
+
return new Response(
|
|
362
|
+
JSON.stringify({
|
|
363
|
+
error: "shuttle_limit_exceeded",
|
|
364
|
+
active_shuttles: activeShuttles,
|
|
365
|
+
limit: shuttleLimit,
|
|
366
|
+
relationship,
|
|
367
|
+
}),
|
|
368
|
+
{ status: 422, headers: { "Content-Type": "application/json" } },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
344
372
|
// Origin and destination are mandatory TC participants — their votes are always required
|
|
345
373
|
const mandatoryTCs: PlanetManifest[] = [
|
|
346
374
|
{
|
|
@@ -421,13 +449,14 @@ async function handleInitiate(
|
|
|
421
449
|
status: "PREPARING",
|
|
422
450
|
traffic_controllers: electedTCs.map((tc) => tc.landing_site),
|
|
423
451
|
signatures: {},
|
|
452
|
+
origin_lists_dest: originListsDest,
|
|
424
453
|
};
|
|
425
454
|
|
|
426
|
-
const { privateKey } = await PlanetIdentity.getIdentity(
|
|
455
|
+
const { privateKey } = await PlanetIdentity.getIdentity(TRAFFIC_CONTROL);
|
|
427
456
|
const signature = await CryptoCore.sign(JSON.stringify(plan), privateKey);
|
|
428
457
|
plan.signatures[localPlanet.landing_site] = signature;
|
|
429
458
|
|
|
430
|
-
await ConsensusEngine.savePlanState(
|
|
459
|
+
await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, plan);
|
|
431
460
|
await ConsensusEngine.broadcast(plan, "prepare", electedTCs);
|
|
432
461
|
|
|
433
462
|
return new Response(JSON.stringify({ plan }), { status: 200 });
|
|
@@ -435,10 +464,8 @@ async function handleInitiate(
|
|
|
435
464
|
|
|
436
465
|
async function handlePrepare(
|
|
437
466
|
request: Request,
|
|
438
|
-
|
|
439
|
-
DB: D1Database,
|
|
467
|
+
TRAFFIC_CONTROL: DurableObjectNamespace,
|
|
440
468
|
localPlanet: any,
|
|
441
|
-
TRAFFIC_CONTROL: any,
|
|
442
469
|
) {
|
|
443
470
|
const plan = TravelPlanSchema.parse(await request.json());
|
|
444
471
|
|
|
@@ -464,14 +491,14 @@ async function handlePrepare(
|
|
|
464
491
|
throw new Error("Invalid travel time calculation.");
|
|
465
492
|
}
|
|
466
493
|
|
|
467
|
-
const { privateKey } = await PlanetIdentity.getIdentity(
|
|
494
|
+
const { privateKey } = await PlanetIdentity.getIdentity(TRAFFIC_CONTROL);
|
|
468
495
|
const signature = await CryptoCore.sign(JSON.stringify(plan), privateKey);
|
|
469
496
|
plan.signatures[localPlanet.landing_site] = signature;
|
|
470
497
|
|
|
471
|
-
await ConsensusEngine.savePlanState(
|
|
498
|
+
await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, plan);
|
|
472
499
|
|
|
473
500
|
const controllersPromises = plan.traffic_controllers.map((url) =>
|
|
474
|
-
discoverSpacePort(url,
|
|
501
|
+
discoverSpacePort(url, TRAFFIC_CONTROL),
|
|
475
502
|
);
|
|
476
503
|
const controllers = (await Promise.all(controllersPromises)).filter(
|
|
477
504
|
(n): n is PlanetManifest => n !== null,
|
|
@@ -483,10 +510,8 @@ async function handlePrepare(
|
|
|
483
510
|
}
|
|
484
511
|
async function handleRegister(
|
|
485
512
|
request: Request,
|
|
486
|
-
|
|
487
|
-
DB: D1Database,
|
|
513
|
+
TRAFFIC_CONTROL: DurableObjectNamespace,
|
|
488
514
|
localPlanet: any,
|
|
489
|
-
TRAFFIC_CONTROL: any,
|
|
490
515
|
) {
|
|
491
516
|
const plan = TravelPlanSchema.parse(await request.json());
|
|
492
517
|
|
|
@@ -532,18 +557,66 @@ async function handleRegister(
|
|
|
532
557
|
});
|
|
533
558
|
}
|
|
534
559
|
|
|
535
|
-
const alreadyStored = await
|
|
560
|
+
const alreadyStored = await doQuery(
|
|
561
|
+
TRAFFIC_CONTROL,
|
|
536
562
|
`SELECT id FROM travel_plans WHERE id = ?`,
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.first();
|
|
563
|
+
[plan.id],
|
|
564
|
+
);
|
|
540
565
|
|
|
541
|
-
if (
|
|
542
|
-
|
|
566
|
+
if (alreadyStored.length === 0) {
|
|
567
|
+
// Enforce shuttle limit from destination's perspective.
|
|
568
|
+
// originListsDest is carried in the plan (set at initiation) to avoid a
|
|
569
|
+
// circular HTTP call back to the origin while it is processing handleCommit.
|
|
570
|
+
const destListsOrigin = WARP_LINKS.some(
|
|
571
|
+
(l) => new URL(l.url).origin === new URL(plan.origin_url).origin,
|
|
572
|
+
);
|
|
573
|
+
const originListsDest = plan.origin_lists_dest ?? false;
|
|
574
|
+
|
|
575
|
+
const destShuttleLimit =
|
|
576
|
+
originListsDest && destListsOrigin
|
|
577
|
+
? 2
|
|
578
|
+
: originListsDest || destListsOrigin
|
|
579
|
+
? 1
|
|
580
|
+
: 0;
|
|
581
|
+
|
|
582
|
+
const destActiveRows = await doQuery(
|
|
583
|
+
TRAFFIC_CONTROL,
|
|
584
|
+
`SELECT COUNT(*) as count FROM travel_plans
|
|
585
|
+
WHERE ((origin_url = ? AND destination_url = ?)
|
|
586
|
+
OR (origin_url = ? AND destination_url = ?))
|
|
587
|
+
AND end_timestamp > ?`,
|
|
588
|
+
[
|
|
589
|
+
plan.origin_url,
|
|
590
|
+
localPlanet.landing_site,
|
|
591
|
+
localPlanet.landing_site,
|
|
592
|
+
plan.origin_url,
|
|
593
|
+
Date.now(),
|
|
594
|
+
],
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
if (((destActiveRows[0] as any)?.count ?? 0) >= destShuttleLimit) {
|
|
598
|
+
const relationship =
|
|
599
|
+
originListsDest && destListsOrigin
|
|
600
|
+
? "mutual_neighbors"
|
|
601
|
+
: originListsDest || destListsOrigin
|
|
602
|
+
? "one_sided_neighbors"
|
|
603
|
+
: "non_neighbors";
|
|
604
|
+
return new Response(
|
|
605
|
+
JSON.stringify({
|
|
606
|
+
error: "shuttle_limit_exceeded",
|
|
607
|
+
active_shuttles: (destActiveRows[0] as any)?.count ?? 0,
|
|
608
|
+
limit: destShuttleLimit,
|
|
609
|
+
relationship,
|
|
610
|
+
}),
|
|
611
|
+
{ status: 422, headers: { "Content-Type": "application/json" } },
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
await doExec(
|
|
616
|
+
TRAFFIC_CONTROL,
|
|
543
617
|
`INSERT OR IGNORE INTO travel_plans (id, ship_id, origin_url, destination_url, start_timestamp, end_timestamp, status, signatures)
|
|
544
618
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
545
|
-
|
|
546
|
-
.bind(
|
|
619
|
+
[
|
|
547
620
|
plan.id,
|
|
548
621
|
plan.ship_id,
|
|
549
622
|
plan.origin_url,
|
|
@@ -552,8 +625,8 @@ async function handleRegister(
|
|
|
552
625
|
plan.end_timestamp,
|
|
553
626
|
plan.status,
|
|
554
627
|
JSON.stringify(plan.signatures),
|
|
555
|
-
|
|
556
|
-
|
|
628
|
+
],
|
|
629
|
+
);
|
|
557
630
|
|
|
558
631
|
const fmt = (n: number) => n.toFixed(1);
|
|
559
632
|
const originCoordsFormatted = `${fmt(originCoords.x)}:${fmt(originCoords.y)}:${fmt(originCoords.z)}`;
|
|
@@ -572,42 +645,41 @@ async function handleRegister(
|
|
|
572
645
|
|
|
573
646
|
async function handleCommit(
|
|
574
647
|
request: Request,
|
|
575
|
-
|
|
576
|
-
DB: D1Database,
|
|
648
|
+
TRAFFIC_CONTROL: DurableObjectNamespace,
|
|
577
649
|
localPlanet: any,
|
|
578
|
-
TRAFFIC_CONTROL: any,
|
|
579
650
|
) {
|
|
580
651
|
const incomingPlan = TravelPlanSchema.parse(await request.json());
|
|
581
652
|
const existing =
|
|
582
|
-
(await ConsensusEngine.getPlanState(
|
|
653
|
+
(await ConsensusEngine.getPlanState(TRAFFIC_CONTROL, incomingPlan.id)) ||
|
|
654
|
+
incomingPlan;
|
|
583
655
|
|
|
584
656
|
console.log(
|
|
585
657
|
`[${localPlanet.name}] Committing travel plan ${incomingPlan.id} (Existing signatures: ${Object.keys(existing.signatures).length}, New signatures: ${Object.keys(incomingPlan.signatures).length})`,
|
|
586
658
|
);
|
|
587
659
|
|
|
588
660
|
existing.signatures = { ...existing.signatures, ...incomingPlan.signatures };
|
|
589
|
-
await ConsensusEngine.savePlanState(
|
|
661
|
+
await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, existing);
|
|
590
662
|
|
|
591
663
|
if (ConsensusEngine.hasQuorum(existing) && existing.status === "PREPARING") {
|
|
592
664
|
// Check if we already archived it to prevent race condition across TCs
|
|
593
|
-
const alreadyArchived = await
|
|
665
|
+
const alreadyArchived = await doQuery(
|
|
666
|
+
TRAFFIC_CONTROL,
|
|
594
667
|
`SELECT id FROM travel_plans WHERE id = ?`,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
.first();
|
|
668
|
+
[existing.id],
|
|
669
|
+
);
|
|
598
670
|
|
|
599
|
-
if (
|
|
671
|
+
if (alreadyArchived.length === 0) {
|
|
600
672
|
console.log(
|
|
601
673
|
`[${localPlanet.name}] Quorum reached for plan ${existing.id}. Registering with destination.`,
|
|
602
674
|
);
|
|
603
675
|
|
|
604
676
|
existing.status = "PLAN_ACCEPTED";
|
|
605
|
-
await ConsensusEngine.savePlanState(
|
|
677
|
+
await ConsensusEngine.savePlanState(TRAFFIC_CONTROL, existing);
|
|
606
678
|
|
|
607
679
|
// Register with destination synchronously — must succeed before committing locally
|
|
608
680
|
const destManifest = await discoverSpacePort(
|
|
609
681
|
existing.destination_url,
|
|
610
|
-
|
|
682
|
+
TRAFFIC_CONTROL,
|
|
611
683
|
);
|
|
612
684
|
if (!destManifest?.space_port) {
|
|
613
685
|
return new Response(
|
|
@@ -642,13 +714,11 @@ async function handleCommit(
|
|
|
642
714
|
);
|
|
643
715
|
}
|
|
644
716
|
|
|
645
|
-
await
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
)
|
|
651
|
-
.bind(
|
|
717
|
+
await doExec(
|
|
718
|
+
TRAFFIC_CONTROL,
|
|
719
|
+
`INSERT OR IGNORE INTO travel_plans (id, ship_id, origin_url, destination_url, start_timestamp, end_timestamp, status, signatures)
|
|
720
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
721
|
+
[
|
|
652
722
|
existing.id,
|
|
653
723
|
existing.ship_id,
|
|
654
724
|
existing.origin_url,
|
|
@@ -657,8 +727,8 @@ async function handleCommit(
|
|
|
657
727
|
existing.end_timestamp,
|
|
658
728
|
existing.status,
|
|
659
729
|
JSON.stringify(existing.signatures),
|
|
660
|
-
|
|
661
|
-
|
|
730
|
+
],
|
|
731
|
+
);
|
|
662
732
|
|
|
663
733
|
await broadcastEvent(TRAFFIC_CONTROL, {
|
|
664
734
|
type: "QUORUM_REACHED",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import md5 from "md5";
|
|
3
3
|
import { env } from "cloudflare:workers";
|
|
4
4
|
import { WARP_LINKS, PLANET_NAME, PLANET_DESCRIPTION } from "../lib/config";
|
|
5
|
+
import { doQuery } from "../lib/do-storage";
|
|
5
6
|
|
|
6
7
|
const formatCoord = (n: number) => n.toFixed(2).padStart(6, "0");
|
|
7
8
|
|
|
@@ -59,38 +60,34 @@ const myCoords = calculateCoordinates(landingSite);
|
|
|
59
60
|
const departureBuffer =
|
|
60
61
|
parseInt(getSimVar("DEPARTURE_BUFFER_MS") || "") || 30 * 1000;
|
|
61
62
|
|
|
62
|
-
//
|
|
63
|
-
const {
|
|
63
|
+
// Durable Object Storage
|
|
64
|
+
const { TRAFFIC_CONTROL } = (env as any) || {};
|
|
64
65
|
|
|
65
66
|
let traffic = [];
|
|
66
67
|
let archive = [];
|
|
67
68
|
|
|
68
|
-
if (
|
|
69
|
+
if (TRAFFIC_CONTROL) {
|
|
69
70
|
try {
|
|
70
71
|
const now = Date.now();
|
|
71
72
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
.bind(now, landingSite, landingSite)
|
|
80
|
-
.all();
|
|
73
|
+
const activePlans = await doQuery(
|
|
74
|
+
TRAFFIC_CONTROL,
|
|
75
|
+
`SELECT * FROM travel_plans
|
|
76
|
+
WHERE end_timestamp > ? AND (origin_url = ? OR destination_url = ?)
|
|
77
|
+
ORDER BY start_timestamp ASC`,
|
|
78
|
+
[now, landingSite, landingSite],
|
|
79
|
+
);
|
|
81
80
|
|
|
82
81
|
// Build a URL→name map from warp links and the traffic_controllers cache
|
|
83
82
|
const warpNameMap = new Map(WARP_LINKS.map((l) => [l.url, l.name]));
|
|
84
83
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
.bind(now, landingSite, landingSite)
|
|
93
|
-
.all();
|
|
84
|
+
const archiveRecords = await doQuery(
|
|
85
|
+
TRAFFIC_CONTROL,
|
|
86
|
+
`SELECT * FROM travel_plans
|
|
87
|
+
WHERE end_timestamp <= ? AND (origin_url = ? OR destination_url = ?)
|
|
88
|
+
ORDER BY end_timestamp DESC LIMIT 20`,
|
|
89
|
+
[now, landingSite, landingSite],
|
|
90
|
+
);
|
|
94
91
|
|
|
95
92
|
const allPlanUrls = [
|
|
96
93
|
...(activePlans || []).flatMap((p: any) => [
|
|
@@ -107,11 +104,11 @@ if (DB) {
|
|
|
107
104
|
);
|
|
108
105
|
if (uniqueUrls.length > 0) {
|
|
109
106
|
const placeholders = uniqueUrls.map(() => "?").join(",");
|
|
110
|
-
const
|
|
107
|
+
const cachedNames = await doQuery(
|
|
108
|
+
TRAFFIC_CONTROL,
|
|
111
109
|
`SELECT planet_url, name FROM traffic_controllers WHERE planet_url IN (${placeholders})`,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.all();
|
|
110
|
+
uniqueUrls,
|
|
111
|
+
);
|
|
115
112
|
for (const row of cachedNames || [])
|
|
116
113
|
warpNameMap.set((row as any).planet_url, (row as any).name);
|
|
117
114
|
}
|
|
@@ -557,6 +554,8 @@ const enrichedLinks = WARP_LINKS.map((link, index) => {
|
|
|
557
554
|
|
|
558
555
|
const closeDeck = () => {
|
|
559
556
|
flightDeck.classList.add("hidden");
|
|
557
|
+
initiateBtn.disabled = false;
|
|
558
|
+
initiateBtn.textContent = "JUMP";
|
|
560
559
|
window.dispatchEvent(new CustomEvent("clearSelection"));
|
|
561
560
|
};
|
|
562
561
|
|
|
@@ -856,14 +855,12 @@ const enrichedLinks = WARP_LINKS.map((link, index) => {
|
|
|
856
855
|
const data = await response.json();
|
|
857
856
|
if (data.plan) {
|
|
858
857
|
addTrafficRow(data.plan, filedAt);
|
|
859
|
-
|
|
860
|
-
window.dispatchEvent(new CustomEvent("clearSelection"));
|
|
858
|
+
closeDeck();
|
|
861
859
|
} else if (
|
|
862
860
|
data.error === "insufficient_controllers" ||
|
|
863
861
|
data.error === "no_destination_space_port"
|
|
864
862
|
) {
|
|
865
|
-
|
|
866
|
-
window.dispatchEvent(new CustomEvent("clearSelection"));
|
|
863
|
+
closeDeck();
|
|
867
864
|
noTravelReason.textContent =
|
|
868
865
|
NO_TRAVEL_MESSAGES[data.error] || "Travel is not available.";
|
|
869
866
|
noTravelDialog.classList.remove("hidden");
|