@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
|
@@ -4,14 +4,24 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
bundleRoom
|
|
6
6
|
} from "./chunk-KRQUAEN2.js";
|
|
7
|
+
import {
|
|
8
|
+
logEnsured,
|
|
9
|
+
runStaticDeploy
|
|
10
|
+
} from "./chunk-GBNHBWES.js";
|
|
11
|
+
import "./chunk-BPE452KF.js";
|
|
12
|
+
import {
|
|
13
|
+
ensureProject,
|
|
14
|
+
readProjectConfig
|
|
15
|
+
} from "./chunk-32QTPKVT.js";
|
|
7
16
|
import {
|
|
8
17
|
ApiClientError,
|
|
18
|
+
HelpRequested,
|
|
9
19
|
createApiClient,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
helpFor,
|
|
21
|
+
helpRequested,
|
|
22
|
+
isLoginRequired,
|
|
13
23
|
resolveControlUrlForUser
|
|
14
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-TV66QHFP.js";
|
|
15
25
|
|
|
16
26
|
// src/deploy.ts
|
|
17
27
|
import { existsSync } from "fs";
|
|
@@ -23,7 +33,30 @@ import { diffSchemas, schemaFromCanonical } from "@irtio/schema";
|
|
|
23
33
|
import pc from "picocolors";
|
|
24
34
|
var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
|
|
25
35
|
var MIGRATIONS_DIR = "irtio/migrations";
|
|
36
|
+
var USAGE = `usage: irtio deploy [options]
|
|
37
|
+
irtio deploy --static [<dir>] [options]
|
|
38
|
+
|
|
39
|
+
Bundles the room, classifies the schema change against the last deployment, and uploads. A
|
|
40
|
+
breaking change is refused unless a migration covers it. With "static" in the project file the
|
|
41
|
+
client directory ships too, after the room.
|
|
42
|
+
|
|
43
|
+
options:
|
|
44
|
+
--allow-breaking accept a breaking schema change that a migration covers
|
|
45
|
+
--strategy drain|migrate drain (the default) lets running rooms finish on the old version;
|
|
46
|
+
migrate moves live rooms onto the new one now
|
|
47
|
+
--room <file> the room entry
|
|
48
|
+
--project <id> project id (default: the project file)
|
|
49
|
+
--name <name> the name a first deploy registers the project under
|
|
50
|
+
--no-static deploy the room alone, even with "static" configured
|
|
51
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
52
|
+
--url <control> control plane (default: your stored login)
|
|
53
|
+
-h, --help print this
|
|
54
|
+
|
|
55
|
+
irtio deploy --static uploads a BUILT client directory and nothing else. It takes --label <label>
|
|
56
|
+
plus --project, --name, --config and --url; the directory and label default to the project file.
|
|
57
|
+
`;
|
|
26
58
|
function parseDeployArgs(args) {
|
|
59
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
27
60
|
const parsed = {
|
|
28
61
|
allowBreaking: false
|
|
29
62
|
};
|
|
@@ -33,12 +66,32 @@ function parseDeployArgs(args) {
|
|
|
33
66
|
case "--allow-breaking":
|
|
34
67
|
parsed.allowBreaking = true;
|
|
35
68
|
break;
|
|
69
|
+
case "--no-static":
|
|
70
|
+
parsed.noStatic = true;
|
|
71
|
+
break;
|
|
36
72
|
case "--project": {
|
|
37
73
|
const value = args[++i];
|
|
38
74
|
if (value === void 0) throw new Error("irtio deploy: --project needs a value");
|
|
39
75
|
parsed.project = value;
|
|
40
76
|
break;
|
|
41
77
|
}
|
|
78
|
+
case "--name": {
|
|
79
|
+
const value = args[++i];
|
|
80
|
+
if (value === void 0 || value === "") {
|
|
81
|
+
throw new Error("irtio deploy: --name needs a value");
|
|
82
|
+
}
|
|
83
|
+
parsed.name = value;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "-c":
|
|
87
|
+
case "--config": {
|
|
88
|
+
const value = args[++i];
|
|
89
|
+
if (value === void 0 || value === "") {
|
|
90
|
+
throw new Error("irtio deploy: --config needs a value");
|
|
91
|
+
}
|
|
92
|
+
parsed.config = value;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
42
95
|
case "--url": {
|
|
43
96
|
const value = args[++i];
|
|
44
97
|
if (value === void 0) throw new Error("irtio deploy: --url needs a value");
|
|
@@ -74,7 +127,7 @@ async function defaultAsk(question) {
|
|
|
74
127
|
rl.close();
|
|
75
128
|
}
|
|
76
129
|
}
|
|
77
|
-
function
|
|
130
|
+
function findRoomEntry(cwd, room) {
|
|
78
131
|
if (room !== void 0) {
|
|
79
132
|
const file = path.resolve(cwd, room);
|
|
80
133
|
if (!existsSync(file)) throw new Error(`irtio deploy: no room file at ${file}`);
|
|
@@ -84,46 +137,19 @@ function resolveEntry(cwd, room) {
|
|
|
84
137
|
const file = path.resolve(cwd, candidate);
|
|
85
138
|
if (existsSync(file)) return file;
|
|
86
139
|
}
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
function resolveEntry(cwd, room) {
|
|
143
|
+
const entry = findRoomEntry(cwd, room);
|
|
144
|
+
if (entry !== void 0) return entry;
|
|
87
145
|
throw new Error(
|
|
88
146
|
`irtio deploy: no room file found in ${cwd}
|
|
89
147
|
looked for: ${ROOM_CANDIDATES.join(", ")}
|
|
90
148
|
pass --room <file> if it lives elsewhere`
|
|
91
149
|
);
|
|
92
150
|
}
|
|
93
|
-
async function
|
|
94
|
-
const
|
|
95
|
-
if (!existsSync(file)) return void 0;
|
|
96
|
-
let parsed;
|
|
97
|
-
try {
|
|
98
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
99
|
-
} catch (err) {
|
|
100
|
-
throw new Error(`irtio deploy: ${file} is not valid JSON: ${String(err)}`);
|
|
101
|
-
}
|
|
102
|
-
const project = parsed?.project;
|
|
103
|
-
if (project === void 0) return void 0;
|
|
104
|
-
if (typeof project !== "string" || project.length === 0) {
|
|
105
|
-
throw new Error(`irtio deploy: ${file} has a "project" that is not a non-empty string`);
|
|
106
|
-
}
|
|
107
|
-
return project;
|
|
108
|
-
}
|
|
109
|
-
async function readIrtioJsonClient(cwd) {
|
|
110
|
-
const file = path.join(cwd, "irtio.json");
|
|
111
|
-
if (!existsSync(file)) return void 0;
|
|
112
|
-
let parsed;
|
|
113
|
-
try {
|
|
114
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
115
|
-
} catch (err) {
|
|
116
|
-
throw new Error(`irtio deploy: ${file} is not valid JSON: ${String(err)}`);
|
|
117
|
-
}
|
|
118
|
-
const client = parsed?.client;
|
|
119
|
-
if (client === void 0) return void 0;
|
|
120
|
-
if (typeof client !== "string" || client.length === 0) {
|
|
121
|
-
throw new Error(`irtio deploy: ${file} has a "client" that is not a non-empty string`);
|
|
122
|
-
}
|
|
123
|
-
return client;
|
|
124
|
-
}
|
|
125
|
-
async function warnIfClientImportsRoom(cwd, entry, log) {
|
|
126
|
-
const client = await readIrtioJsonClient(cwd);
|
|
151
|
+
async function warnIfClientImportsRoom(cwd, entry, config, log) {
|
|
152
|
+
const client = config.client;
|
|
127
153
|
if (client === void 0) return;
|
|
128
154
|
const clientFile = path.resolve(cwd, client);
|
|
129
155
|
if (await clientImportsRoom(clientFile, entry)) {
|
|
@@ -162,8 +188,9 @@ async function runDeploy(options = {}) {
|
|
|
162
188
|
const log = options.log ?? ((line) => console.log(line));
|
|
163
189
|
const ask = options.ask ?? defaultAsk;
|
|
164
190
|
const controlUrl = await resolveControlUrlForUser(options.controlUrl);
|
|
191
|
+
const config = await readProjectConfig(cwd, "irtio deploy", options.config);
|
|
165
192
|
const entry = resolveEntry(cwd, options.room);
|
|
166
|
-
await warnIfClientImportsRoom(cwd, entry, log);
|
|
193
|
+
await warnIfClientImportsRoom(cwd, entry, config, log);
|
|
167
194
|
const outDir = path.join(cwd, DEPLOY_BUILD_DIR);
|
|
168
195
|
await mkdir(outDir, { recursive: true });
|
|
169
196
|
let result;
|
|
@@ -179,26 +206,19 @@ async function runDeploy(options = {}) {
|
|
|
179
206
|
log(pc.green(`bundled ${path.basename(result.file)} (${result.hash.slice(0, 12)})`));
|
|
180
207
|
const bundled = await loadBundledSchema(result.file);
|
|
181
208
|
const client = options.client ?? await createApiClient(controlUrl);
|
|
182
|
-
const projectId = options.project ??
|
|
209
|
+
const projectId = options.project ?? config.project ?? bundled.project;
|
|
183
210
|
if (projectId === void 0) {
|
|
184
211
|
throw new Error(
|
|
185
212
|
'irtio deploy: no project id \u2014 pass --project, add "project" to irtio.json, or set it in defineSchema(...)'
|
|
186
213
|
);
|
|
187
214
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!projectExists) {
|
|
196
|
-
const defaultName = path.basename(cwd);
|
|
197
|
-
const answer = (await ask(`project ${projectId} does not exist yet \u2014 name it [${defaultName}]: `)).trim();
|
|
198
|
-
const name = answer.length > 0 ? answer : defaultName;
|
|
199
|
-
await client.post("/v1/projects", { name, id: projectId });
|
|
200
|
-
log(pc.dim(`created project ${projectId} (${name})`));
|
|
201
|
-
}
|
|
215
|
+
const ensured = await ensureProject(client, projectId, {
|
|
216
|
+
cwd,
|
|
217
|
+
config,
|
|
218
|
+
command: "irtio deploy",
|
|
219
|
+
...options.name !== void 0 ? { name: options.name } : {}
|
|
220
|
+
});
|
|
221
|
+
logEnsured(log, projectId, ensured);
|
|
202
222
|
const deployments = await client.get(`/v1/projects/${projectId}/deployments`);
|
|
203
223
|
const newestOf = (list) => list.reduce(
|
|
204
224
|
(best, d) => best === void 0 || d.version > best.version ? d : best,
|
|
@@ -339,6 +359,19 @@ async function runDeploy(options = {}) {
|
|
|
339
359
|
log(pc.dim(`rooms already running finish on v${previous.version}`));
|
|
340
360
|
}
|
|
341
361
|
}
|
|
362
|
+
if (config.static !== void 0 && options.noStatic !== true) {
|
|
363
|
+
const site = await runStaticDeploy({
|
|
364
|
+
cwd,
|
|
365
|
+
project: projectId,
|
|
366
|
+
log,
|
|
367
|
+
client,
|
|
368
|
+
controlUrl,
|
|
369
|
+
dir: config.static.dir,
|
|
370
|
+
...config.static.label !== void 0 ? { label: config.static.label } : {},
|
|
371
|
+
...options.config !== void 0 ? { config: options.config } : {}
|
|
372
|
+
});
|
|
373
|
+
return { ...response, site };
|
|
374
|
+
}
|
|
342
375
|
return response;
|
|
343
376
|
}
|
|
344
377
|
var DeployRefusedError = class extends Error {
|
|
@@ -352,10 +385,30 @@ async function deploy(args, deps = {}) {
|
|
|
352
385
|
const errorLog = deps.errorLog ?? ((line) => console.error(line));
|
|
353
386
|
try {
|
|
354
387
|
const parsed = parseDeployArgs(args);
|
|
388
|
+
const cwd = path.resolve(deps.cwd ?? process.cwd());
|
|
389
|
+
const config = await readProjectConfig(cwd, "irtio deploy", parsed.config);
|
|
390
|
+
if (parsed.room === void 0 && parsed.noStatic !== true && config.static !== void 0 && findRoomEntry(cwd, void 0) === void 0) {
|
|
391
|
+
log(pc.dim("no room file \u2014 deploying the static site only"));
|
|
392
|
+
await runStaticDeploy({
|
|
393
|
+
cwd,
|
|
394
|
+
log,
|
|
395
|
+
dir: config.static.dir,
|
|
396
|
+
...config.static.label !== void 0 ? { label: config.static.label } : {},
|
|
397
|
+
...parsed.project !== void 0 ? { project: parsed.project } : {},
|
|
398
|
+
...parsed.name !== void 0 ? { name: parsed.name } : {},
|
|
399
|
+
...parsed.config !== void 0 ? { config: parsed.config } : {},
|
|
400
|
+
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
401
|
+
...deps.client !== void 0 ? { client: deps.client } : {}
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
355
405
|
await runDeploy({
|
|
356
406
|
allowBreaking: parsed.allowBreaking,
|
|
357
407
|
log,
|
|
358
408
|
...parsed.project !== void 0 ? { project: parsed.project } : {},
|
|
409
|
+
...parsed.name !== void 0 ? { name: parsed.name } : {},
|
|
410
|
+
...parsed.config !== void 0 ? { config: parsed.config } : {},
|
|
411
|
+
...parsed.noStatic !== void 0 ? { noStatic: parsed.noStatic } : {},
|
|
359
412
|
...parsed.url !== void 0 ? { controlUrl: parsed.url } : {},
|
|
360
413
|
...parsed.room !== void 0 ? { room: parsed.room } : {},
|
|
361
414
|
...parsed.strategy !== void 0 ? { strategy: parsed.strategy } : {},
|
|
@@ -364,6 +417,10 @@ async function deploy(args, deps = {}) {
|
|
|
364
417
|
...deps.irtioPackages !== void 0 ? { irtioPackages: deps.irtioPackages } : {}
|
|
365
418
|
});
|
|
366
419
|
} catch (err) {
|
|
420
|
+
if (err instanceof HelpRequested) {
|
|
421
|
+
log(err.usage);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
367
424
|
if (err instanceof DeployRefusedError) {
|
|
368
425
|
process.exitCode = 1;
|
|
369
426
|
return;
|
|
@@ -380,7 +437,9 @@ async function deploy(args, deps = {}) {
|
|
|
380
437
|
}
|
|
381
438
|
export {
|
|
382
439
|
DeployRefusedError,
|
|
440
|
+
USAGE,
|
|
383
441
|
deploy,
|
|
442
|
+
findRoomEntry,
|
|
384
443
|
parseDeployArgs,
|
|
385
444
|
runDeploy
|
|
386
445
|
};
|
|
@@ -16,11 +16,19 @@ import {
|
|
|
16
16
|
pruneSaves,
|
|
17
17
|
saveKey
|
|
18
18
|
} from "./chunk-BPE452KF.js";
|
|
19
|
+
import {
|
|
20
|
+
readProjectConfig
|
|
21
|
+
} from "./chunk-32QTPKVT.js";
|
|
22
|
+
import {
|
|
23
|
+
HelpRequested,
|
|
24
|
+
helpFor,
|
|
25
|
+
helpRequested
|
|
26
|
+
} from "./chunk-TV66QHFP.js";
|
|
19
27
|
|
|
20
28
|
// src/dev.ts
|
|
21
29
|
import { existsSync } from "fs";
|
|
22
30
|
import { watch } from "fs";
|
|
23
|
-
import { mkdir
|
|
31
|
+
import { mkdir } from "fs/promises";
|
|
24
32
|
import { createRequire } from "module";
|
|
25
33
|
import * as path from "path";
|
|
26
34
|
import { fileURLToPath, pathToFileURL as pathToFileURL3 } from "url";
|
|
@@ -43,6 +51,46 @@ var DEFAULT_LIMITS = {
|
|
|
43
51
|
saveRetain: DEFAULT_SAVE_RETAIN
|
|
44
52
|
};
|
|
45
53
|
|
|
54
|
+
// ../supervisor/src/codes.ts
|
|
55
|
+
import { randomInt } from "crypto";
|
|
56
|
+
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
57
|
+
var ROOM_ID_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
58
|
+
var TENANT_ROOM_ID = "__tenant";
|
|
59
|
+
var CODE_SHAPE_RE = /^[A-Za-z0-9]{4,5}$/;
|
|
60
|
+
function pick(n) {
|
|
61
|
+
let out = "";
|
|
62
|
+
for (let i = 0; i < n; i++) out += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function newRoomCode(taken) {
|
|
66
|
+
for (let len = 4; len <= 8; len++) {
|
|
67
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
68
|
+
const code = pick(len);
|
|
69
|
+
if (!taken(code)) return code;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error("newRoomCode: exhausted the room code space");
|
|
73
|
+
}
|
|
74
|
+
function newClientId(taken) {
|
|
75
|
+
for (let attempt = 0; attempt < 128; attempt++) {
|
|
76
|
+
const id = `c${pick(10)}`;
|
|
77
|
+
if (!taken(id)) return id;
|
|
78
|
+
}
|
|
79
|
+
throw new Error("newClientId: could not allocate a unique client id");
|
|
80
|
+
}
|
|
81
|
+
function looksLikeRoomCode(id) {
|
|
82
|
+
if (!CODE_SHAPE_RE.test(id)) return false;
|
|
83
|
+
for (const ch of id.toUpperCase()) {
|
|
84
|
+
if (!CODE_ALPHABET.includes(ch)) return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function normalizeRoomId(raw) {
|
|
89
|
+
if (!ROOM_ID_RE.test(raw)) return void 0;
|
|
90
|
+
if (raw === TENANT_ROOM_ID) return void 0;
|
|
91
|
+
return looksLikeRoomCode(raw) ? raw.toUpperCase() : raw;
|
|
92
|
+
}
|
|
93
|
+
|
|
46
94
|
// ../supervisor/src/resilience.ts
|
|
47
95
|
var SNAPSHOT_FLUSH_MAX_RETRIES = 3;
|
|
48
96
|
var SNAPSHOT_FLUSH_RETRY_DELAYS_MS = [100, 200, 400];
|
|
@@ -409,44 +457,6 @@ function verifyResume(secret, token, now = Date.now()) {
|
|
|
409
457
|
return { clientId, roomId, role, expMs };
|
|
410
458
|
}
|
|
411
459
|
|
|
412
|
-
// ../supervisor/src/codes.ts
|
|
413
|
-
import { randomInt } from "crypto";
|
|
414
|
-
var CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
415
|
-
var ROOM_ID_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
416
|
-
var CODE_SHAPE_RE = /^[A-Za-z0-9]{4,5}$/;
|
|
417
|
-
function pick(n) {
|
|
418
|
-
let out = "";
|
|
419
|
-
for (let i = 0; i < n; i++) out += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
|
|
420
|
-
return out;
|
|
421
|
-
}
|
|
422
|
-
function newRoomCode(taken) {
|
|
423
|
-
for (let len = 4; len <= 8; len++) {
|
|
424
|
-
for (let attempt = 0; attempt < 64; attempt++) {
|
|
425
|
-
const code = pick(len);
|
|
426
|
-
if (!taken(code)) return code;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
throw new Error("newRoomCode: exhausted the room code space");
|
|
430
|
-
}
|
|
431
|
-
function newClientId(taken) {
|
|
432
|
-
for (let attempt = 0; attempt < 128; attempt++) {
|
|
433
|
-
const id = `c${pick(10)}`;
|
|
434
|
-
if (!taken(id)) return id;
|
|
435
|
-
}
|
|
436
|
-
throw new Error("newClientId: could not allocate a unique client id");
|
|
437
|
-
}
|
|
438
|
-
function looksLikeRoomCode(id) {
|
|
439
|
-
if (!CODE_SHAPE_RE.test(id)) return false;
|
|
440
|
-
for (const ch of id.toUpperCase()) {
|
|
441
|
-
if (!CODE_ALPHABET.includes(ch)) return false;
|
|
442
|
-
}
|
|
443
|
-
return true;
|
|
444
|
-
}
|
|
445
|
-
function normalizeRoomId(raw) {
|
|
446
|
-
if (!ROOM_ID_RE.test(raw)) return void 0;
|
|
447
|
-
return looksLikeRoomCode(raw) ? raw.toUpperCase() : raw;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
460
|
// ../supervisor/src/relay.ts
|
|
451
461
|
import { PRESENCE_COLLECTION, relaySchema } from "@irtio/protocol";
|
|
452
462
|
import {
|
|
@@ -1180,7 +1190,7 @@ var WorkerHost = class {
|
|
|
1180
1190
|
}
|
|
1181
1191
|
}
|
|
1182
1192
|
onMessage(msg) {
|
|
1183
|
-
if ((msg.t === "serialized" || msg.t === "inspected") && this.pending.has(msg.reqId)) {
|
|
1193
|
+
if ((msg.t === "serialized" || msg.t === "inspected" || msg.t === "stats") && msg.reqId !== void 0 && this.pending.has(msg.reqId)) {
|
|
1184
1194
|
const resolve2 = this.pending.get(msg.reqId);
|
|
1185
1195
|
this.pending.delete(msg.reqId);
|
|
1186
1196
|
resolve2?.(msg);
|
|
@@ -1251,6 +1261,19 @@ var WorkerHost = class {
|
|
|
1251
1261
|
);
|
|
1252
1262
|
return msg?.t === "inspected" ? msg : void 0;
|
|
1253
1263
|
}
|
|
1264
|
+
/**
|
|
1265
|
+
* D36: the room's own tick counters — `overruns` and `maxTickMs` above all. `undefined` when
|
|
1266
|
+
* the worker died or did not answer in time, and the caller must report that as *unavailable*
|
|
1267
|
+
* rather than as zero: a room shedding ticks and a room nobody asked look identical otherwise,
|
|
1268
|
+
* and that is exactly the lie the `tick-health` invariant exists to prevent.
|
|
1269
|
+
*/
|
|
1270
|
+
async stats(timeoutMs = 2e3) {
|
|
1271
|
+
const msg = await this.request(
|
|
1272
|
+
(reqId) => ({ t: "stats", reqId }),
|
|
1273
|
+
timeoutMs
|
|
1274
|
+
);
|
|
1275
|
+
return msg?.t === "stats" ? msg.stats : void 0;
|
|
1276
|
+
}
|
|
1254
1277
|
/** Intentional shutdown: `onDead` is not fired. */
|
|
1255
1278
|
async terminate() {
|
|
1256
1279
|
if (this.disposed) return;
|
|
@@ -1370,6 +1393,11 @@ var SupervisorImpl = class {
|
|
|
1370
1393
|
limits;
|
|
1371
1394
|
store;
|
|
1372
1395
|
processMetrics = newProcessMetrics();
|
|
1396
|
+
/**
|
|
1397
|
+
* D38: the ring behind the reserved `__tenant` room id. Same bounded ring every room gets, so
|
|
1398
|
+
* a noisy boot costs the same as a noisy room and nothing here can grow without limit.
|
|
1399
|
+
*/
|
|
1400
|
+
tenantLogs = new LogRing();
|
|
1373
1401
|
/** The newest runnable deployment; every new room is created under it. */
|
|
1374
1402
|
bundle;
|
|
1375
1403
|
schemaHash;
|
|
@@ -1601,13 +1629,31 @@ var SupervisorImpl = class {
|
|
|
1601
1629
|
// -------------------------------------------------------------------------
|
|
1602
1630
|
// Logging
|
|
1603
1631
|
// -------------------------------------------------------------------------
|
|
1632
|
+
/**
|
|
1633
|
+
* D38: a tenant-level event now enters the log pipeline instead of only reaching the console.
|
|
1634
|
+
*
|
|
1635
|
+
* `/admin/logs` reads per-room rings, so before this every event that was not about a room —
|
|
1636
|
+
* boot, resume, deploy, placement, alarm recovery, storage failures — was invisible to the host
|
|
1637
|
+
* agent, to the control plane, and to `irtio logs`. Those are precisely the lines somebody
|
|
1638
|
+
* reaches for when a project is not working, and the only way to see them was to be on the box
|
|
1639
|
+
* reading the guest console.
|
|
1640
|
+
*
|
|
1641
|
+
* They go into a ring under the reserved id `__tenant` (D38), so the whole existing pipeline
|
|
1642
|
+
* carries them with no new route and no new frame.
|
|
1643
|
+
*/
|
|
1604
1644
|
log(level, ...args) {
|
|
1645
|
+
this.tenantLogs.push(level, args);
|
|
1646
|
+
this.emit(level, args);
|
|
1647
|
+
}
|
|
1648
|
+
/** The console/config sink, with no ring behind it. */
|
|
1649
|
+
emit(level, args) {
|
|
1605
1650
|
if (this.config.log) this.config.log(level, ...args);
|
|
1606
1651
|
else console[level === "info" ? "log" : level]("[irtio]", ...args);
|
|
1607
1652
|
}
|
|
1653
|
+
/** A room event: it belongs in that room's ring, and nowhere near the `__tenant` one. */
|
|
1608
1654
|
roomLog(room, level, ...args) {
|
|
1609
1655
|
room.log(level, ...args);
|
|
1610
|
-
this.
|
|
1656
|
+
this.emit(level, [`[${room.id}]`, ...args]);
|
|
1611
1657
|
}
|
|
1612
1658
|
/**
|
|
1613
1659
|
* Week-5 snapshot keys carry the deployment version (`@v3`); version `0` keeps the week-3 key
|
|
@@ -1654,11 +1700,18 @@ var SupervisorImpl = class {
|
|
|
1654
1700
|
json({ rooms: this.rooms(), deploymentVersion: this.deploymentVersion });
|
|
1655
1701
|
return true;
|
|
1656
1702
|
case "/admin/metrics":
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1703
|
+
void this.refreshTickHealth().then(
|
|
1704
|
+
() => json({
|
|
1705
|
+
at: Date.now(),
|
|
1706
|
+
metrics: this.metrics(),
|
|
1707
|
+
rooms: this.rooms().map((r) => r.metrics)
|
|
1708
|
+
}),
|
|
1709
|
+
() => json({
|
|
1710
|
+
at: Date.now(),
|
|
1711
|
+
metrics: this.metrics(),
|
|
1712
|
+
rooms: this.rooms().map((r) => r.metrics)
|
|
1713
|
+
})
|
|
1714
|
+
);
|
|
1662
1715
|
return true;
|
|
1663
1716
|
// D31 (week 7): the ONE write on this otherwise read-only surface, and a deliberate
|
|
1664
1717
|
// exception to its "the agent must not be able to change anything" rule: after a
|
|
@@ -1800,13 +1853,14 @@ var SupervisorImpl = class {
|
|
|
1800
1853
|
const since = Number(url.searchParams.get("since") ?? 0) || 0;
|
|
1801
1854
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 200) || 200, 1e3);
|
|
1802
1855
|
const entries = [];
|
|
1803
|
-
|
|
1804
|
-
|
|
1856
|
+
const streams = [TENANT_ROOM_ID, ...this.registry.values().map((r) => r.id)];
|
|
1857
|
+
for (const roomId of streams) {
|
|
1858
|
+
for (const entry of this.logs(roomId)) {
|
|
1805
1859
|
if (entry.at <= since) continue;
|
|
1806
1860
|
entries.push({
|
|
1807
1861
|
at: entry.at,
|
|
1808
1862
|
level: entry.level,
|
|
1809
|
-
roomId
|
|
1863
|
+
roomId,
|
|
1810
1864
|
message: entry.args.map((x) => typeof x === "string" ? x : String(x)).join(" ")
|
|
1811
1865
|
});
|
|
1812
1866
|
}
|
|
@@ -3127,8 +3181,35 @@ var SupervisorImpl = class {
|
|
|
3127
3181
|
};
|
|
3128
3182
|
}
|
|
3129
3183
|
logs(roomId) {
|
|
3184
|
+
if (roomId === TENANT_ROOM_ID) return this.tenantLogs.list();
|
|
3130
3185
|
return this.registry.get(roomId)?.logs.list() ?? [];
|
|
3131
3186
|
}
|
|
3187
|
+
/**
|
|
3188
|
+
* D36: pull every live room's tick counters out of its worker and into `RoomMetrics`.
|
|
3189
|
+
*
|
|
3190
|
+
* The counters exist already — `RoomCore.stats.overruns` has been incremented next to the
|
|
3191
|
+
* `tick overrun` log line since week 2 — but they never left the worker thread, so the one
|
|
3192
|
+
* signal that says "this room is shedding work" reached nothing a client, a CLI or an operator
|
|
3193
|
+
* could read. Every `overruns` a report shows now came through here.
|
|
3194
|
+
*
|
|
3195
|
+
* A room that does not answer is left exactly as it was, with `overruns` still `undefined` if
|
|
3196
|
+
* it has never answered. That is the whole point of the field being optional: `tick-health`
|
|
3197
|
+
* reports `unavailable` for it, rather than a zero nobody measured.
|
|
3198
|
+
*/
|
|
3199
|
+
async refreshTickHealth(timeoutMs = 2e3) {
|
|
3200
|
+
const live = this.registry.values().filter((r) => r.worker?.alive === true);
|
|
3201
|
+
const answered = await Promise.all(
|
|
3202
|
+
live.map(async (room) => {
|
|
3203
|
+
const stats = await room.worker?.stats(timeoutMs);
|
|
3204
|
+
if (!stats) return void 0;
|
|
3205
|
+
room.metrics.ticks = stats.ticks;
|
|
3206
|
+
room.metrics.maxTickMs = stats.maxTickMs;
|
|
3207
|
+
room.metrics.overruns = stats.overruns;
|
|
3208
|
+
return room.id;
|
|
3209
|
+
})
|
|
3210
|
+
);
|
|
3211
|
+
return answered.filter((id) => id !== void 0);
|
|
3212
|
+
}
|
|
3132
3213
|
async inspect(roomId) {
|
|
3133
3214
|
const room = this.registry.get(roomId);
|
|
3134
3215
|
if (!room) return void 0;
|
|
@@ -3402,40 +3483,8 @@ function resolveEntry(cwd, room) {
|
|
|
3402
3483
|
create one (export default defineRoom(schema, {...})) or pass --room <file>`
|
|
3403
3484
|
);
|
|
3404
3485
|
}
|
|
3405
|
-
async function
|
|
3406
|
-
const
|
|
3407
|
-
if (!existsSync(file)) return "dev";
|
|
3408
|
-
let parsed;
|
|
3409
|
-
try {
|
|
3410
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
3411
|
-
} catch (err) {
|
|
3412
|
-
throw new Error(`irtio dev: ${file} is not valid JSON: ${String(err)}`);
|
|
3413
|
-
}
|
|
3414
|
-
const project = parsed?.project;
|
|
3415
|
-
if (project === void 0) return "dev";
|
|
3416
|
-
if (typeof project !== "string" || project.length === 0) {
|
|
3417
|
-
throw new Error(`irtio dev: ${file} has a "project" that is not a non-empty string`);
|
|
3418
|
-
}
|
|
3419
|
-
return project;
|
|
3420
|
-
}
|
|
3421
|
-
async function readClientEntry(cwd) {
|
|
3422
|
-
const file = path.join(cwd, "irtio.json");
|
|
3423
|
-
if (!existsSync(file)) return void 0;
|
|
3424
|
-
let parsed;
|
|
3425
|
-
try {
|
|
3426
|
-
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
3427
|
-
} catch (err) {
|
|
3428
|
-
throw new Error(`irtio dev: ${file} is not valid JSON: ${String(err)}`);
|
|
3429
|
-
}
|
|
3430
|
-
const client = parsed?.client;
|
|
3431
|
-
if (client === void 0) return void 0;
|
|
3432
|
-
if (typeof client !== "string" || client.length === 0) {
|
|
3433
|
-
throw new Error(`irtio dev: ${file} has a "client" that is not a non-empty string`);
|
|
3434
|
-
}
|
|
3435
|
-
return client;
|
|
3436
|
-
}
|
|
3437
|
-
async function warnIfClientImportsRoom(cwd, entry, log) {
|
|
3438
|
-
const client = await readClientEntry(cwd);
|
|
3486
|
+
async function warnIfClientImportsRoom(cwd, entry, config, log) {
|
|
3487
|
+
const client = config.client;
|
|
3439
3488
|
if (client === void 0) return;
|
|
3440
3489
|
const clientFile = path.resolve(cwd, client);
|
|
3441
3490
|
if (await clientImportsRoom(clientFile, entry)) {
|
|
@@ -3508,13 +3557,14 @@ async function resolveWorkerEntry2(outDir) {
|
|
|
3508
3557
|
return outfile;
|
|
3509
3558
|
}
|
|
3510
3559
|
async function buildState(supervisor, projectId, bundle) {
|
|
3560
|
+
await supervisor.refreshTickHealth().catch(() => []);
|
|
3511
3561
|
const infos = supervisor.rooms();
|
|
3512
3562
|
const inspects = await Promise.all(
|
|
3513
3563
|
infos.map((room) => supervisor.inspect(room.id).catch(() => void 0))
|
|
3514
3564
|
);
|
|
3515
3565
|
const logs = [];
|
|
3516
|
-
for (const
|
|
3517
|
-
for (const entry of supervisor.logs(
|
|
3566
|
+
for (const roomId of [TENANT_ROOM_ID, ...infos.map((r) => r.id)]) {
|
|
3567
|
+
for (const entry of supervisor.logs(roomId)) logs.push({ ...entry, roomId });
|
|
3518
3568
|
}
|
|
3519
3569
|
logs.sort((a, b) => a.at - b.at);
|
|
3520
3570
|
return {
|
|
@@ -3550,11 +3600,12 @@ async function startDev(options = {}) {
|
|
|
3550
3600
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
3551
3601
|
const log = options.log ?? ((line) => console.log(line));
|
|
3552
3602
|
const entry = resolveEntry(cwd, options.room);
|
|
3553
|
-
const
|
|
3603
|
+
const config = await readProjectConfig(cwd, "irtio dev", options.config);
|
|
3604
|
+
const projectId = config.project ?? "dev";
|
|
3554
3605
|
const outDir = path.join(cwd, ".irtio", "dev");
|
|
3555
3606
|
const port = options.port ?? DEFAULT_PORT;
|
|
3556
3607
|
await mkdir(outDir, { recursive: true });
|
|
3557
|
-
await warnIfClientImportsRoom(cwd, entry, log);
|
|
3608
|
+
await warnIfClientImportsRoom(cwd, entry, config, log);
|
|
3558
3609
|
const bundleOptions = {
|
|
3559
3610
|
entry,
|
|
3560
3611
|
outDir,
|
|
@@ -3685,7 +3736,20 @@ async function startDev(options = {}) {
|
|
|
3685
3736
|
}
|
|
3686
3737
|
};
|
|
3687
3738
|
}
|
|
3739
|
+
var USAGE = `usage: irtio dev [options]
|
|
3740
|
+
|
|
3741
|
+
Bundles the room file, runs a real supervisor on it locally, and serves the inspector on the same
|
|
3742
|
+
port. Rebuilds and resets the rooms whenever the room code changes.
|
|
3743
|
+
|
|
3744
|
+
options:
|
|
3745
|
+
--room <file> the room entry (default: irtio/room.ts, irtio/room.js, room.ts)
|
|
3746
|
+
--port <n> port to listen on (default 7070; 0 picks a free one)
|
|
3747
|
+
--no-watch do not rebuild on change
|
|
3748
|
+
-c, --config <f> the project file to read (default irtio.json)
|
|
3749
|
+
-h, --help print this
|
|
3750
|
+
`;
|
|
3688
3751
|
function parseDevArgs(args) {
|
|
3752
|
+
if (helpRequested(args)) throw helpFor(USAGE);
|
|
3689
3753
|
const parsed = { watch: true };
|
|
3690
3754
|
for (let i = 0; i < args.length; i++) {
|
|
3691
3755
|
const arg = args[i];
|
|
@@ -3712,6 +3776,10 @@ function parseDevArgs(args) {
|
|
|
3712
3776
|
case "--no-watch":
|
|
3713
3777
|
parsed.watch = false;
|
|
3714
3778
|
break;
|
|
3779
|
+
case "-c":
|
|
3780
|
+
case "--config":
|
|
3781
|
+
parsed.config = value();
|
|
3782
|
+
break;
|
|
3715
3783
|
default:
|
|
3716
3784
|
throw new Error(`irtio dev: unknown option ${JSON.stringify(arg)}`);
|
|
3717
3785
|
}
|
|
@@ -3719,15 +3787,21 @@ function parseDevArgs(args) {
|
|
|
3719
3787
|
return parsed;
|
|
3720
3788
|
}
|
|
3721
3789
|
async function dev(args) {
|
|
3722
|
-
|
|
3790
|
+
let parsed;
|
|
3723
3791
|
let server;
|
|
3724
3792
|
try {
|
|
3793
|
+
parsed = parseDevArgs(args);
|
|
3725
3794
|
server = await startDev({
|
|
3726
3795
|
watch: parsed.watch,
|
|
3727
3796
|
...parsed.room !== void 0 ? { room: parsed.room } : {},
|
|
3728
|
-
...parsed.port !== void 0 ? { port: parsed.port } : {}
|
|
3797
|
+
...parsed.port !== void 0 ? { port: parsed.port } : {},
|
|
3798
|
+
...parsed.config !== void 0 ? { config: parsed.config } : {}
|
|
3729
3799
|
});
|
|
3730
3800
|
} catch (err) {
|
|
3801
|
+
if (err instanceof HelpRequested) {
|
|
3802
|
+
console.log(err.usage);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3731
3805
|
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
3732
3806
|
process.exitCode = 1;
|
|
3733
3807
|
return;
|
|
@@ -3754,6 +3828,7 @@ async function dev(args) {
|
|
|
3754
3828
|
});
|
|
3755
3829
|
}
|
|
3756
3830
|
export {
|
|
3831
|
+
USAGE,
|
|
3757
3832
|
dev,
|
|
3758
3833
|
parseDevArgs,
|
|
3759
3834
|
startDev
|