@irtio/cli 0.5.0 → 0.5.2
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/dist/chunk-32QTPKVT.js +93 -0
- package/dist/{static-deploy-LZXTDYNY.js → chunk-GBNHBWES.js} +95 -33
- package/dist/chunk-TV66QHFP.js +167 -0
- package/dist/{deploy-6OM3UYNL.js → deploy-3SABPL3T.js} +114 -55
- package/dist/{dev-7UZZGE4U.js → dev-QJOGXLKM.js} +164 -89
- package/dist/index.js +35 -18
- package/dist/init.d.ts +8 -1
- package/dist/init.js +43 -6
- package/dist/{keys-KEKO3EJ6.js → keys-XBORZAPI.js} +47 -20
- package/dist/{login-OV2EFNTJ.js → login-3EXB4CGX.js} +18 -1
- package/dist/{logs-5OUDPAIX.js → logs-2EOXLNWF.js} +40 -19
- package/dist/{migrate-3R6VRW4J.js → migrate-WUO2GBMX.js} +41 -15
- package/dist/{rollback-CLQVYFHW.js → rollback-GA6UY772.js} +39 -19
- package/dist/{rooms-OJ3JLYHD.js → rooms-B66LQIIF.js} +46 -19
- package/dist/simulate.d.ts +71 -2
- package/dist/simulate.js +196 -22
- package/dist/static-deploy-5TBH4VNA.js +17 -0
- package/dist/{whoami-S73O6KJF.js → whoami-CI5D5RCC.js} +19 -4
- package/package.json +10 -9
- package/dist/chunk-I37DLT7K.js +0 -85
- package/dist/chunk-NVUKSP5U.js +0 -61
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-I37DLT7K.js";
|
|
2
|
+
readProjectConfig
|
|
3
|
+
} from "./chunk-32QTPKVT.js";
|
|
5
4
|
import {
|
|
5
|
+
HelpRequested,
|
|
6
|
+
createApiClient,
|
|
7
|
+
helpFor,
|
|
8
|
+
helpRequested,
|
|
9
|
+
isLoginRequired,
|
|
6
10
|
resolveControlUrlForUser
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-TV66QHFP.js";
|
|
8
12
|
|
|
9
13
|
// src/rollback.ts
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
14
|
+
import "fs";
|
|
15
|
+
import "fs/promises";
|
|
16
|
+
import "path";
|
|
13
17
|
import pc from "picocolors";
|
|
18
|
+
var USAGE = `usage: irtio rollback <version> [options]
|
|
19
|
+
|
|
20
|
+
Re-promotes deployment <version> and brings rooms that migrated past it back to their
|
|
21
|
+
pre-migration state. This DISCARDS whatever those rooms wrote since their migration.
|
|
22
|
+
|
|
23
|
+
options:
|
|
24
|
+
<version> the deployment version to go back to
|
|
25
|
+
--project <id> project id (default: the project file)
|
|
26
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
27
|
+
--url <control> control plane (default: your stored login)
|
|
28
|
+
-h, --help print this
|
|
29
|
+
`;
|
|
14
30
|
function parseRollbackArgs(args) {
|
|
31
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
15
32
|
const parsed = {};
|
|
16
33
|
for (let i = 0; i < args.length; i++) {
|
|
17
34
|
const arg = args[i];
|
|
@@ -22,6 +39,14 @@ function parseRollbackArgs(args) {
|
|
|
22
39
|
parsed.project = value;
|
|
23
40
|
break;
|
|
24
41
|
}
|
|
42
|
+
case "-c":
|
|
43
|
+
case "--config": {
|
|
44
|
+
const value = args[++i];
|
|
45
|
+
if (value === void 0 || value === "")
|
|
46
|
+
throw new Error("irtio rollback: --config needs a value");
|
|
47
|
+
parsed.config = value;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
25
50
|
case "--url": {
|
|
26
51
|
const value = args[++i];
|
|
27
52
|
if (value === void 0) throw new Error("irtio rollback: --url needs a value");
|
|
@@ -75,23 +100,13 @@ async function runRollback(options) {
|
|
|
75
100
|
}
|
|
76
101
|
return result;
|
|
77
102
|
}
|
|
78
|
-
async function readIrtioJsonProject(cwd) {
|
|
79
|
-
const file = path.join(cwd, "irtio.json");
|
|
80
|
-
if (!existsSync(file)) return void 0;
|
|
81
|
-
let parsed;
|
|
82
|
-
try {
|
|
83
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
84
|
-
} catch {
|
|
85
|
-
throw new Error(`irtio rollback: ${file} is not valid JSON`);
|
|
86
|
-
}
|
|
87
|
-
return typeof parsed.project === "string" && parsed.project.length > 0 ? parsed.project : void 0;
|
|
88
|
-
}
|
|
89
103
|
async function rollback(args, deps = {}) {
|
|
90
104
|
const log = deps.log ?? ((line) => console.log(line));
|
|
91
105
|
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
92
106
|
try {
|
|
93
107
|
const parsed = parseRollbackArgs(args);
|
|
94
|
-
const
|
|
108
|
+
const config = await readProjectConfig(process.cwd(), "irtio rollback", parsed.config);
|
|
109
|
+
const project = parsed.project ?? config.project;
|
|
95
110
|
if (project === void 0) {
|
|
96
111
|
throw new Error(
|
|
97
112
|
"irtio rollback: no project id \u2014 pass --project or run this from a project with irtio.json"
|
|
@@ -105,6 +120,10 @@ async function rollback(args, deps = {}) {
|
|
|
105
120
|
...deps.client !== void 0 ? { client: deps.client } : {}
|
|
106
121
|
});
|
|
107
122
|
} catch (err) {
|
|
123
|
+
if (err instanceof HelpRequested) {
|
|
124
|
+
log(err.usage);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
108
127
|
if (isLoginRequired(err)) {
|
|
109
128
|
errorLog(pc.red("not logged in"));
|
|
110
129
|
errorLog("run: irtio login");
|
|
@@ -116,6 +135,7 @@ async function rollback(args, deps = {}) {
|
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
export {
|
|
138
|
+
USAGE,
|
|
119
139
|
parseRollbackArgs,
|
|
120
140
|
rollback,
|
|
121
141
|
runRollback
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-I37DLT7K.js";
|
|
2
|
+
readProjectConfig
|
|
3
|
+
} from "./chunk-32QTPKVT.js";
|
|
5
4
|
import {
|
|
5
|
+
HelpRequested,
|
|
6
|
+
createApiClient,
|
|
7
|
+
helpFor,
|
|
8
|
+
helpRequested,
|
|
9
|
+
isLoginRequired,
|
|
6
10
|
resolveControlUrlForUser
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-TV66QHFP.js";
|
|
8
12
|
|
|
9
13
|
// src/rooms.ts
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
14
|
+
import "fs";
|
|
15
|
+
import "fs/promises";
|
|
16
|
+
import "path";
|
|
13
17
|
import pc from "picocolors";
|
|
18
|
+
var USAGE = `usage: irtio rooms [options]
|
|
19
|
+
irtio rooms saves <room> [options]
|
|
20
|
+
irtio rooms restore <room> --save <id> [options]
|
|
21
|
+
|
|
22
|
+
Lists a project's rooms. Rows not reported for longer than the control plane's retention window
|
|
23
|
+
(30 days by default) are pruned.
|
|
24
|
+
|
|
25
|
+
subcommands:
|
|
26
|
+
saves <room> list a room's save generations, newest first
|
|
27
|
+
restore <room> --save <id>
|
|
28
|
+
bring a room back from a save. This DISCARDS its current state, and
|
|
29
|
+
stops the tenant if it is live
|
|
30
|
+
|
|
31
|
+
options:
|
|
32
|
+
--project <id> project id (default: the project file)
|
|
33
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
34
|
+
--url <control> control plane (default: your stored login)
|
|
35
|
+
-h, --help print this
|
|
36
|
+
`;
|
|
14
37
|
function parseRoomsArgs(args) {
|
|
38
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
15
39
|
const parsed = { sub: "list" };
|
|
16
40
|
let rest = args;
|
|
17
41
|
const first = args[0];
|
|
@@ -39,6 +63,14 @@ function parseRoomsArgs(args) {
|
|
|
39
63
|
parsed.project = value;
|
|
40
64
|
break;
|
|
41
65
|
}
|
|
66
|
+
case "-c":
|
|
67
|
+
case "--config": {
|
|
68
|
+
const value = args[++i];
|
|
69
|
+
if (value === void 0 || value === "")
|
|
70
|
+
throw new Error("irtio rooms: --config needs a value");
|
|
71
|
+
parsed.config = value;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
42
74
|
case "--url": {
|
|
43
75
|
const value = rest[++i];
|
|
44
76
|
if (value === void 0) throw new Error("irtio rooms: --url needs a value");
|
|
@@ -140,23 +172,13 @@ async function runRooms(options) {
|
|
|
140
172
|
for (const line of formatTable(rows)) log(line);
|
|
141
173
|
return rows;
|
|
142
174
|
}
|
|
143
|
-
async function readIrtioJsonProject(cwd) {
|
|
144
|
-
const file = path.join(cwd, "irtio.json");
|
|
145
|
-
if (!existsSync(file)) return void 0;
|
|
146
|
-
let parsed;
|
|
147
|
-
try {
|
|
148
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
149
|
-
} catch {
|
|
150
|
-
throw new Error(`irtio rooms: ${file} is not valid JSON`);
|
|
151
|
-
}
|
|
152
|
-
return typeof parsed.project === "string" && parsed.project.length > 0 ? parsed.project : void 0;
|
|
153
|
-
}
|
|
154
175
|
async function rooms(args, deps = {}) {
|
|
155
176
|
const log = deps.log ?? ((line) => console.log(line));
|
|
156
177
|
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
157
178
|
try {
|
|
158
179
|
const parsed = parseRoomsArgs(args);
|
|
159
|
-
const
|
|
180
|
+
const config = await readProjectConfig(process.cwd(), "irtio rooms", parsed.config);
|
|
181
|
+
const project = parsed.project ?? config.project;
|
|
160
182
|
if (project === void 0) {
|
|
161
183
|
throw new Error(
|
|
162
184
|
"irtio rooms: no project id \u2014 pass --project or run this from a project with irtio.json"
|
|
@@ -180,6 +202,10 @@ async function rooms(args, deps = {}) {
|
|
|
180
202
|
await runRooms(common);
|
|
181
203
|
}
|
|
182
204
|
} catch (err) {
|
|
205
|
+
if (err instanceof HelpRequested) {
|
|
206
|
+
log(err.usage);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
183
209
|
if (isLoginRequired(err)) {
|
|
184
210
|
errorLog(pc.red("not logged in"));
|
|
185
211
|
errorLog("run: irtio login");
|
|
@@ -191,6 +217,7 @@ async function rooms(args, deps = {}) {
|
|
|
191
217
|
}
|
|
192
218
|
}
|
|
193
219
|
export {
|
|
220
|
+
USAGE,
|
|
194
221
|
parseRoomsArgs,
|
|
195
222
|
rooms,
|
|
196
223
|
runRestore,
|
package/dist/simulate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SimulationReport } from '@irtio/bots';
|
|
1
|
+
import { RunEnd, SimulationReport, TickHealthReading } from '@irtio/bots';
|
|
2
2
|
import { AnySchema } from '@irtio/schema';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -26,7 +26,9 @@ interface SimulateArgs {
|
|
|
26
26
|
mispredictionMax?: number;
|
|
27
27
|
snapsMax?: number;
|
|
28
28
|
correctionsMax?: number;
|
|
29
|
+
overrunsMax?: number;
|
|
29
30
|
}
|
|
31
|
+
declare const USAGE = "usage: irtio simulate [options]\n\nDrives N real clients at a running room and checks the built-in invariants. Run it from a project\ndirectory so it can load irtio/schema.ts; without one it falls back to a relay run.\n\noptions:\n --bots <n> how many clients to spawn (default 5)\n --seconds <n> how long to play for (default 10)\n --room <code> join this room instead of creating one\n --url <ws://...> where to connect (default ws://localhost:7070)\n --key <projectKey> the project key to present\n --trace <path> write the frame trace here\n --cheat send illegal writes and expect corrections\n --misprediction-max <units> fail if one correction snaps a prediction further than this\n --snaps-max <n> fail above this many cap-exceeded reconciliations\n --corrections-max <perSec> fail above this correction rate per bot\n --overruns-max <n> fail above this many server tick overruns in the run window\n (default 0; the count is read from the server, and the run says\n so when it could not be read)\n -h, --help print this\n\nexit codes: 0 every invariant held, 1 something was measured and failed, 2 the run could not be\nperformed (nobody joined, or it passed its wall-clock ceiling).\n";
|
|
30
32
|
/** Hand-rolled, like `parseDevArgs`: the CLI has no argument-parsing dependency. */
|
|
31
33
|
declare function parseSimulateArgs(args: readonly string[]): SimulateArgs;
|
|
32
34
|
interface LoadSchemaOptions {
|
|
@@ -70,6 +72,41 @@ declare function loadProjectWorld(options: LoadSchemaOptions): Promise<LoadedWor
|
|
|
70
72
|
* description, or `undefined` when the worlds agree.
|
|
71
73
|
*/
|
|
72
74
|
declare function checkWorldDeterminism(world: LoadedWorld): Promise<string | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* The dev server's JSON view, which is where a local run reads the server's own tick counters.
|
|
77
|
+
* `irtio dev` serves it on the socket's own port; nothing else does, and that is deliberate — a
|
|
78
|
+
* deployed tenant's counters belong to the control plane, not to whoever can reach the socket.
|
|
79
|
+
*/
|
|
80
|
+
declare function stateUrlFor(wsUrl: string): string | undefined;
|
|
81
|
+
interface RoomTickCounters {
|
|
82
|
+
readonly overruns?: number;
|
|
83
|
+
readonly maxTickMs?: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reads one room's tick counters off the dev server, or explains why it could not. Every failure
|
|
87
|
+
* path returns a sentence rather than a zero: the `tick-health` invariant has an `unavailable`
|
|
88
|
+
* state precisely so that "the room dropped no ticks" and "nobody asked the room" stay different
|
|
89
|
+
* answers.
|
|
90
|
+
*/
|
|
91
|
+
declare function readTickCounters(wsUrl: string, roomId: string): Promise<{
|
|
92
|
+
counters?: RoomTickCounters;
|
|
93
|
+
unavailable?: string;
|
|
94
|
+
source?: string;
|
|
95
|
+
}>;
|
|
96
|
+
/**
|
|
97
|
+
* The run-window reading: overruns are a lifetime counter on the server, so the window is the
|
|
98
|
+
* difference between a baseline taken as the bots came up and a reading taken as they went down.
|
|
99
|
+
* `maxTickMs` has no windowed form (it is a running maximum) and is reported as what it is.
|
|
100
|
+
*/
|
|
101
|
+
declare function tickHealthFrom(before: {
|
|
102
|
+
counters?: RoomTickCounters;
|
|
103
|
+
unavailable?: string;
|
|
104
|
+
source?: string;
|
|
105
|
+
}, after: {
|
|
106
|
+
counters?: RoomTickCounters;
|
|
107
|
+
unavailable?: string;
|
|
108
|
+
source?: string;
|
|
109
|
+
}): TickHealthReading;
|
|
73
110
|
interface RunSimulationOptions extends Partial<SimulateArgs> {
|
|
74
111
|
/** Project root; defaults to `process.cwd()`. */
|
|
75
112
|
readonly cwd?: string;
|
|
@@ -80,9 +117,35 @@ interface RunSimulationOptions extends Partial<SimulateArgs> {
|
|
|
80
117
|
* hardware can burst past the cursor-tuned default without anything being wrong.
|
|
81
118
|
*/
|
|
82
119
|
readonly correctionsPerSecMax?: number;
|
|
120
|
+
/** D36: initial-join bound, passed through to `spawnBots`. */
|
|
121
|
+
readonly joinTimeoutMs?: number;
|
|
122
|
+
/** D36: room-gone grace, passed through to `spawnBots`. */
|
|
123
|
+
readonly roomGoneGraceMs?: number;
|
|
124
|
+
/**
|
|
125
|
+
* D36: the hard wall-clock ceiling on the whole run. Default `seconds × 3 + 30 s`. Reaching it
|
|
126
|
+
* is a fatal run condition, not a clean end.
|
|
127
|
+
*/
|
|
128
|
+
readonly ceilingMs?: number;
|
|
83
129
|
/** @internal Test seam, same as `startDev`'s. */
|
|
84
130
|
readonly irtioPackages?: Record<string, string> | undefined;
|
|
85
131
|
}
|
|
132
|
+
/** D36's wall-clock ceiling: generous enough that no healthy run meets it. */
|
|
133
|
+
declare function ceilingFor(seconds: number): number;
|
|
134
|
+
/**
|
|
135
|
+
* D36: the run could not be performed at all — nobody joined, so there is no report to print and
|
|
136
|
+
* nothing was measured. `irtio simulate` turns this into exit code 2, which is the whole point of
|
|
137
|
+
* having a third code: "clean" and "violation" are both claims about a room, and this is not one.
|
|
138
|
+
*/
|
|
139
|
+
declare class RunNotPerformedError extends Error {
|
|
140
|
+
readonly cause?: unknown | undefined;
|
|
141
|
+
readonly name = "RunNotPerformedError";
|
|
142
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* D36: what ended the run. `scripts`/`duration` are the ordinary ends; `room-gone` and `ceiling`
|
|
146
|
+
* are the two fatal run conditions, and both still print the report and write the trace.
|
|
147
|
+
*/
|
|
148
|
+
type SimulationEnd = RunEnd | 'ceiling';
|
|
86
149
|
interface SimulationRun {
|
|
87
150
|
readonly report: SimulationReport;
|
|
88
151
|
readonly tracePath: string;
|
|
@@ -93,6 +156,12 @@ interface SimulationRun {
|
|
|
93
156
|
/** Result of the build-twice world-builder check: the failure text, or `undefined` if it held
|
|
94
157
|
* (or did not apply). A failure also fails the run. */
|
|
95
158
|
readonly worldError?: string;
|
|
159
|
+
/** D36: what ended the run. */
|
|
160
|
+
readonly endedBy: SimulationEnd;
|
|
161
|
+
/** D36: the fatal run condition in one sentence, when one ended the run. */
|
|
162
|
+
readonly fatal?: string;
|
|
163
|
+
/** D36: `0` clean, `1` a violation was measured, `2` the run was not performed as asked. */
|
|
164
|
+
readonly exitCode: 0 | 1 | 2;
|
|
96
165
|
/** Everything that was printed, in order — the same lines `log` received. */
|
|
97
166
|
readonly output: readonly string[];
|
|
98
167
|
}
|
|
@@ -100,4 +169,4 @@ interface SimulationRun {
|
|
|
100
169
|
declare function runSimulation(options?: RunSimulationOptions): Promise<SimulationRun>;
|
|
101
170
|
declare function simulate(args: readonly string[]): Promise<void>;
|
|
102
171
|
|
|
103
|
-
export { type LoadSchemaOptions, type RunSimulationOptions, type SimulateArgs, type SimulationRun, checkWorldDeterminism, loadProjectSchema, loadProjectWorld, parseSimulateArgs, runSimulation, simulate };
|
|
172
|
+
export { type LoadSchemaOptions, RunNotPerformedError, type RunSimulationOptions, type SimulateArgs, type SimulationEnd, type SimulationRun, USAGE, ceilingFor, checkWorldDeterminism, loadProjectSchema, loadProjectWorld, parseSimulateArgs, readTickCounters, runSimulation, simulate, stateUrlFor, tickHealthFrom };
|
package/dist/simulate.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HelpRequested,
|
|
3
|
+
helpFor,
|
|
4
|
+
helpRequested,
|
|
5
|
+
trailerFor
|
|
6
|
+
} from "./chunk-TV66QHFP.js";
|
|
7
|
+
|
|
1
8
|
// src/simulate.ts
|
|
2
9
|
import { existsSync } from "fs";
|
|
3
10
|
import { mkdir } from "fs/promises";
|
|
@@ -25,7 +32,32 @@ function nonNegativeInt(raw, flag) {
|
|
|
25
32
|
}
|
|
26
33
|
return value;
|
|
27
34
|
}
|
|
35
|
+
var USAGE = `usage: irtio simulate [options]
|
|
36
|
+
|
|
37
|
+
Drives N real clients at a running room and checks the built-in invariants. Run it from a project
|
|
38
|
+
directory so it can load irtio/schema.ts; without one it falls back to a relay run.
|
|
39
|
+
|
|
40
|
+
options:
|
|
41
|
+
--bots <n> how many clients to spawn (default ${DEFAULT_BOTS})
|
|
42
|
+
--seconds <n> how long to play for (default ${DEFAULT_SECONDS})
|
|
43
|
+
--room <code> join this room instead of creating one
|
|
44
|
+
--url <ws://...> where to connect (default ${DEFAULT_URL})
|
|
45
|
+
--key <projectKey> the project key to present
|
|
46
|
+
--trace <path> write the frame trace here
|
|
47
|
+
--cheat send illegal writes and expect corrections
|
|
48
|
+
--misprediction-max <units> fail if one correction snaps a prediction further than this
|
|
49
|
+
--snaps-max <n> fail above this many cap-exceeded reconciliations
|
|
50
|
+
--corrections-max <perSec> fail above this correction rate per bot
|
|
51
|
+
--overruns-max <n> fail above this many server tick overruns in the run window
|
|
52
|
+
(default 0; the count is read from the server, and the run says
|
|
53
|
+
so when it could not be read)
|
|
54
|
+
-h, --help print this
|
|
55
|
+
|
|
56
|
+
exit codes: 0 every invariant held, 1 something was measured and failed, 2 the run could not be
|
|
57
|
+
performed (nobody joined, or it passed its wall-clock ceiling).
|
|
58
|
+
`;
|
|
28
59
|
function parseSimulateArgs(args) {
|
|
60
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
29
61
|
const parsed = { bots: DEFAULT_BOTS, seconds: DEFAULT_SECONDS, cheat: false };
|
|
30
62
|
for (let i = 0; i < args.length; i++) {
|
|
31
63
|
const arg = args[i];
|
|
@@ -72,6 +104,9 @@ function parseSimulateArgs(args) {
|
|
|
72
104
|
case "--corrections-max":
|
|
73
105
|
parsed.correctionsMax = positive(value(), "--corrections-max");
|
|
74
106
|
break;
|
|
107
|
+
case "--overruns-max":
|
|
108
|
+
parsed.overrunsMax = nonNegativeInt(value(), "--overruns-max");
|
|
109
|
+
break;
|
|
75
110
|
default:
|
|
76
111
|
throw new Error(`irtio simulate: unknown option ${JSON.stringify(arg)}`);
|
|
77
112
|
}
|
|
@@ -178,13 +213,91 @@ function everyByteEqual(a, b) {
|
|
|
178
213
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
179
214
|
return true;
|
|
180
215
|
}
|
|
216
|
+
var TICK_HEALTH_TIMEOUT_MS = 2e3;
|
|
217
|
+
function stateUrlFor(wsUrl) {
|
|
218
|
+
let parsed;
|
|
219
|
+
try {
|
|
220
|
+
parsed = new URL(wsUrl);
|
|
221
|
+
} catch {
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
if (parsed.protocol === "ws:") parsed.protocol = "http:";
|
|
225
|
+
else if (parsed.protocol === "wss:") parsed.protocol = "https:";
|
|
226
|
+
else if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
227
|
+
parsed.pathname = "/__irt/state.json";
|
|
228
|
+
parsed.search = "";
|
|
229
|
+
return parsed.toString();
|
|
230
|
+
}
|
|
231
|
+
async function readTickCounters(wsUrl, roomId) {
|
|
232
|
+
const stateUrl = stateUrlFor(wsUrl);
|
|
233
|
+
if (stateUrl === void 0) {
|
|
234
|
+
return { unavailable: `cannot derive an inspector URL from ${wsUrl}` };
|
|
235
|
+
}
|
|
236
|
+
let body;
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch(stateUrl, {
|
|
239
|
+
signal: AbortSignal.timeout(TICK_HEALTH_TIMEOUT_MS)
|
|
240
|
+
});
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
return {
|
|
243
|
+
unavailable: `${stateUrl} answered ${response.status}, so the server's tick counters were not read` + (response.status === 404 ? ". Only `irtio dev` serves the inspector; a deployed tenant reports ticks through `irtio rooms` instead." : "")
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
body = await response.json();
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return {
|
|
249
|
+
unavailable: `could not reach ${stateUrl} (${err instanceof Error ? err.message : String(err)})`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const rooms = body?.rooms;
|
|
253
|
+
const room = Array.isArray(rooms) ? rooms.find((r) => r?.id === roomId) : void 0;
|
|
254
|
+
if (room === void 0) return { unavailable: `${stateUrl} did not report room ${roomId}` };
|
|
255
|
+
const overruns = room.metrics?.overruns;
|
|
256
|
+
if (typeof overruns !== "number") {
|
|
257
|
+
return {
|
|
258
|
+
unavailable: `${stateUrl} reported room ${roomId} without tick counters (no live worker)`,
|
|
259
|
+
source: stateUrl
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
counters: {
|
|
264
|
+
overruns,
|
|
265
|
+
...typeof room.metrics?.maxTickMs === "number" ? { maxTickMs: room.metrics.maxTickMs } : {}
|
|
266
|
+
},
|
|
267
|
+
source: stateUrl
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function tickHealthFrom(before, after) {
|
|
271
|
+
if (after.unavailable !== void 0 || after.counters?.overruns === void 0) {
|
|
272
|
+
return { unavailable: after.unavailable ?? "no overrun count came back from the server" };
|
|
273
|
+
}
|
|
274
|
+
const baseline = before.counters?.overruns ?? 0;
|
|
275
|
+
const overruns = Math.max(0, after.counters.overruns - baseline);
|
|
276
|
+
return {
|
|
277
|
+
overruns,
|
|
278
|
+
...after.counters.maxTickMs !== void 0 ? { maxTickMs: after.counters.maxTickMs } : {},
|
|
279
|
+
...after.source !== void 0 ? { source: after.source } : {}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
var STOP_GRACE_MS = 5e3;
|
|
283
|
+
function ceilingFor(seconds) {
|
|
284
|
+
return seconds * 3e3 + 3e4;
|
|
285
|
+
}
|
|
286
|
+
var RunNotPerformedError = class extends Error {
|
|
287
|
+
constructor(message, cause) {
|
|
288
|
+
super(message);
|
|
289
|
+
this.cause = cause;
|
|
290
|
+
}
|
|
291
|
+
cause;
|
|
292
|
+
name = "RunNotPerformedError";
|
|
293
|
+
};
|
|
181
294
|
function rate(bytesPerSecond) {
|
|
182
295
|
return bytesPerSecond >= 1e3 ? `${(bytesPerSecond / 1e3).toFixed(1)} kB/s` : `${Math.round(bytesPerSecond)} B/s`;
|
|
183
296
|
}
|
|
184
297
|
function formatReport(report, schemaLoaded, cheating) {
|
|
185
298
|
const lines = [];
|
|
186
299
|
for (const invariant of report.invariants) {
|
|
187
|
-
const mark = invariant.ok ? pc.green("ok ") : pc.red("FAIL");
|
|
300
|
+
const mark = invariant.state === "unavailable" ? pc.yellow("n/a ") : invariant.ok ? pc.green("ok ") : pc.red("FAIL");
|
|
188
301
|
lines.push(` ${mark} ${invariant.name.padEnd(17)} ${pc.dim(invariant.detail)}`);
|
|
189
302
|
}
|
|
190
303
|
lines.push("");
|
|
@@ -199,8 +312,13 @@ function formatReport(report, schemaLoaded, cheating) {
|
|
|
199
312
|
if (report.tracePath !== void 0) lines.push(pc.dim(` trace: ${report.tracePath}`));
|
|
200
313
|
lines.push("");
|
|
201
314
|
const failed = report.invariants.filter((i) => !i.ok);
|
|
315
|
+
const unread = report.invariants.filter((i) => i.state === "unavailable");
|
|
202
316
|
if (failed.length === 0) {
|
|
203
|
-
lines.push(
|
|
317
|
+
lines.push(
|
|
318
|
+
pc.green(
|
|
319
|
+
` every invariant held across ${report.bots} bots` + (unread.length === 0 ? "." : `, except ${unread.map((i) => i.name).join(", ")}, which could not be read.`)
|
|
320
|
+
)
|
|
321
|
+
);
|
|
204
322
|
} else {
|
|
205
323
|
lines.push(
|
|
206
324
|
pc.red(` ${failed.length} invariant(s) failed: ${failed.map((i) => i.name).join(", ")}`)
|
|
@@ -253,30 +371,67 @@ async function runSimulation(options = {}) {
|
|
|
253
371
|
...options.room !== void 0 ? { room: options.room } : {},
|
|
254
372
|
...options.correctionsPerSecMax !== void 0 ? { correctionsPerSecMax: options.correctionsPerSecMax } : options.correctionsMax !== void 0 ? { correctionsPerSecMax: options.correctionsMax } : {},
|
|
255
373
|
...options.mispredictionMax !== void 0 ? { mispredictionMagnitudeMax: options.mispredictionMax } : {},
|
|
256
|
-
...options.snapsMax !== void 0 ? { snapsMax: options.snapsMax } : {}
|
|
374
|
+
...options.snapsMax !== void 0 ? { snapsMax: options.snapsMax } : {},
|
|
375
|
+
...options.overrunsMax !== void 0 ? { overrunsMax: options.overrunsMax } : {}
|
|
257
376
|
};
|
|
258
377
|
const predicted = world !== void 0 && worldError === void 0;
|
|
259
|
-
const
|
|
378
|
+
const spawn = {
|
|
260
379
|
...common,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
380
|
+
...options.joinTimeoutMs !== void 0 ? { joinTimeoutMs: options.joinTimeoutMs } : {},
|
|
381
|
+
...options.roomGoneGraceMs !== void 0 ? { roomGoneGraceMs: options.roomGoneGraceMs } : {}
|
|
382
|
+
};
|
|
383
|
+
let runner;
|
|
384
|
+
try {
|
|
385
|
+
runner = loaded ? await spawnBots(bots, {
|
|
386
|
+
...spawn,
|
|
387
|
+
schema: loaded.schema,
|
|
388
|
+
script: randomScript(loaded.schema, { cheat: options.cheat ?? false }),
|
|
389
|
+
...predicted ? {
|
|
390
|
+
physics: {
|
|
391
|
+
gravity: world.gravity,
|
|
392
|
+
...world.timestep !== void 0 ? { timestep: world.timestep } : {},
|
|
393
|
+
...world.setup !== void 0 ? { setup: world.setup } : {},
|
|
394
|
+
...world.bodies !== void 0 ? { bodies: world.bodies } : {},
|
|
395
|
+
...world.intents !== void 0 ? { intents: world.intents } : {}
|
|
396
|
+
}
|
|
397
|
+
} : {}
|
|
398
|
+
}) : await spawnBots(bots, { ...spawn, script: relayEchoScript() });
|
|
399
|
+
} catch (err) {
|
|
400
|
+
throw new RunNotPerformedError(
|
|
401
|
+
`irtio simulate: no bot ever joined ${url}, so nothing was measured.
|
|
402
|
+
` + (err instanceof Error ? err.message : String(err)),
|
|
403
|
+
err
|
|
404
|
+
);
|
|
405
|
+
}
|
|
273
406
|
log("");
|
|
274
407
|
log(
|
|
275
408
|
`${pc.bold("irtio simulate")} \u2014 ${bots} bot${bots === 1 ? "" : "s"} \xD7 ${seconds}s on ${url} (room ${pc.bold(runner.roomId)})${options.cheat ? pc.yellow(" [cheat]") : ""}`
|
|
276
409
|
);
|
|
277
410
|
log("");
|
|
278
|
-
await runner.
|
|
279
|
-
const
|
|
411
|
+
const tickBefore = await readTickCounters(url, runner.roomId);
|
|
412
|
+
const ceilingMs = options.ceilingMs ?? ceilingFor(seconds);
|
|
413
|
+
let ceilingHit = false;
|
|
414
|
+
const ceiling = new Promise((resolve2) => {
|
|
415
|
+
const timer = setTimeout(() => {
|
|
416
|
+
ceilingHit = true;
|
|
417
|
+
resolve2();
|
|
418
|
+
}, ceilingMs);
|
|
419
|
+
void runner.done().catch(() => void 0).finally(() => {
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
resolve2();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
await ceiling;
|
|
425
|
+
runner.recordTickHealth(tickHealthFrom(tickBefore, await readTickCounters(url, runner.roomId)));
|
|
426
|
+
const stopped = await Promise.race([
|
|
427
|
+
runner.stop(),
|
|
428
|
+
new Promise((resolve2) => {
|
|
429
|
+
const timer = setTimeout(() => resolve2(runner.report()), STOP_GRACE_MS);
|
|
430
|
+
timer.unref?.();
|
|
431
|
+
})
|
|
432
|
+
]);
|
|
433
|
+
const endedBy = ceilingHit ? "ceiling" : runner.endedBy ?? "scripts";
|
|
434
|
+
const fatal = endedBy === "ceiling" ? `the run passed its wall-clock ceiling of ${Math.round(ceilingMs / 1e3)}s without ending on its own, so it was cut short here` : endedBy === "room-gone" ? "every bot was disconnected at once and none rejoined, so the room is gone" : void 0;
|
|
280
435
|
await mkdir(outDir, { recursive: true });
|
|
281
436
|
const tracePath = path.resolve(
|
|
282
437
|
cwd,
|
|
@@ -307,12 +462,17 @@ async function runSimulation(options = {}) {
|
|
|
307
462
|
for (const failure of runner.scriptErrors) {
|
|
308
463
|
log(pc.red(` bot ${failure.bot} script failed: ${String(failure.error)}`));
|
|
309
464
|
}
|
|
465
|
+
if (fatal !== void 0) log(pc.red(` run ended early: ${fatal}`));
|
|
466
|
+
const exitCode = endedBy === "ceiling" ? 2 : !report.ok || worldError !== void 0 || runner.scriptErrors.length > 0 ? 1 : 0;
|
|
310
467
|
return {
|
|
311
468
|
report,
|
|
312
469
|
tracePath,
|
|
313
470
|
schema: loaded !== void 0,
|
|
314
471
|
predicted,
|
|
315
472
|
...worldError !== void 0 ? { worldError } : {},
|
|
473
|
+
endedBy,
|
|
474
|
+
...fatal !== void 0 ? { fatal } : {},
|
|
475
|
+
exitCode,
|
|
316
476
|
output
|
|
317
477
|
};
|
|
318
478
|
}
|
|
@@ -321,24 +481,38 @@ async function simulate(args) {
|
|
|
321
481
|
try {
|
|
322
482
|
parsed = parseSimulateArgs(args);
|
|
323
483
|
} catch (err) {
|
|
484
|
+
if (err instanceof HelpRequested) {
|
|
485
|
+
console.log(err.usage);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
324
488
|
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
325
489
|
process.exitCode = 1;
|
|
326
490
|
return;
|
|
327
491
|
}
|
|
328
492
|
try {
|
|
329
493
|
const run = await runSimulation(parsed);
|
|
330
|
-
if (
|
|
494
|
+
if (run.exitCode !== 0) process.exitCode = run.exitCode;
|
|
331
495
|
} catch (err) {
|
|
332
496
|
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
333
|
-
|
|
334
|
-
|
|
497
|
+
const localDefault = parsed.url === void 0 || parsed.url === DEFAULT_URL;
|
|
498
|
+
const trailer = trailerFor(err, [
|
|
499
|
+
...err instanceof RunNotPerformedError && localDefault ? [`nothing is answering ${DEFAULT_URL}. Start the room with \`irtio dev\`, or pass --url.`] : []
|
|
500
|
+
]);
|
|
501
|
+
if (trailer !== void 0) console.error(pc.dim(trailer));
|
|
502
|
+
process.exitCode = err instanceof RunNotPerformedError ? 2 : 1;
|
|
335
503
|
}
|
|
336
504
|
}
|
|
337
505
|
export {
|
|
506
|
+
RunNotPerformedError,
|
|
507
|
+
USAGE,
|
|
508
|
+
ceilingFor,
|
|
338
509
|
checkWorldDeterminism,
|
|
339
510
|
loadProjectSchema,
|
|
340
511
|
loadProjectWorld,
|
|
341
512
|
parseSimulateArgs,
|
|
513
|
+
readTickCounters,
|
|
342
514
|
runSimulation,
|
|
343
|
-
simulate
|
|
515
|
+
simulate,
|
|
516
|
+
stateUrlFor,
|
|
517
|
+
tickHealthFrom
|
|
344
518
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
USAGE,
|
|
3
|
+
parseStaticDeployArgs,
|
|
4
|
+
runStaticDeploy,
|
|
5
|
+
staticDeploy,
|
|
6
|
+
walkStaticDir
|
|
7
|
+
} from "./chunk-GBNHBWES.js";
|
|
8
|
+
import "./chunk-BPE452KF.js";
|
|
9
|
+
import "./chunk-32QTPKVT.js";
|
|
10
|
+
import "./chunk-TV66QHFP.js";
|
|
11
|
+
export {
|
|
12
|
+
USAGE,
|
|
13
|
+
parseStaticDeployArgs,
|
|
14
|
+
runStaticDeploy,
|
|
15
|
+
staticDeploy,
|
|
16
|
+
walkStaticDir
|
|
17
|
+
};
|