arcadible 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/bin/arc-build.js CHANGED
@@ -17,19 +17,16 @@ const build = require("../src/cli/build");
17
17
  const cfgcode = require("../src/config/code");
18
18
  const cfgpackage = require("../src/config/package");
19
19
  const cfgproduct = require("../src/config/product");
20
- const error = require("../src/util/error");
20
+ const checked = require("../src/action/validate/checked");
21
21
  const log = require("../src/util/log");
22
22
  const output = require("../src/util/output");
23
23
  const phrase = require("../src/phrase");
24
- const project = require("../src/parser/project");
25
24
  const rc = require("../src/util/rc");
26
25
  const spinner = require("../src/util/spinner");
27
- const validate = require("../src/cli/validate");
28
26
 
29
27
  // [use]
30
28
 
31
29
  const abort = output.abort;
32
- const ArcError = error.ArcError;
33
30
  const findRc = rc.findRc;
34
31
  const print = output.print;
35
32
  const relative = path.relative;
@@ -62,26 +59,16 @@ const start = function (argv) {
62
59
  let parsed = null;
63
60
  // what --json lists under `warnings`.
64
61
  let warnings = [];
65
- let sw = new SpinnerWheel("Validating... %s");
62
+ let sw = new SpinnerWheel("Validating… %s");
66
63
  sw.start(json || quiet);
67
- return project(root)
64
+ return checked(root, (found) => {
65
+ warnings = found;
66
+ })
68
67
  .then((proj) => {
69
68
  parsed = proj;
70
- return validate({ project: parsed, root });
71
- })
72
- .then((validated) => {
73
- const count = validated.issues.length;
74
- warnings = validated.warnings;
75
- if (count) {
76
- throw new ArcError(
77
- cfgcode.VALIDATION_FAILED,
78
- phrase.issuesFound(count),
79
- { data: { issues: validated.issues } },
80
- );
81
- }
82
69
  sw.stop(true);
83
70
  warn(warnings, { json, quiet });
84
- sw = new SpinnerWheel("Building... %s");
71
+ sw = new SpinnerWheel("Building… %s");
85
72
  sw.start(json || quiet);
86
73
  return build({ project: parsed, root });
87
74
  })
@@ -110,9 +97,7 @@ const start = function (argv) {
110
97
  }
111
98
  if (artifact?.missing) {
112
99
  log.result(phrase.buildSucceeded(), [], { color: "yellow" });
113
- log.nlpw(
114
- `⚠ ${phrase.buildOutputMissing(relative(root, artifact.missing))}`,
115
- );
100
+ log.nlpw(phrase.buildOutputMissing(relative(root, artifact.missing)));
116
101
  return;
117
102
  }
118
103
  log.hs(phrase.nothingToDo());
@@ -34,17 +34,17 @@ const main = function (argv) {
34
34
  yargs(args.normalize(hideBin(argv)))
35
35
  .scriptName(TOOL)
36
36
  .usage("$0 <command> [options]")
37
- .command("init", "Initialize project")
38
- .command("inspect", "Inspect project")
39
- .command("validate", "Validate project")
40
- .command("build", "Build project")
41
- .command("pack", "Package project")
42
- .command("serve", "Serve a game locally")
43
- .command("deploy", "Deploy project")
37
+ .command("init", "Initialize a project")
38
+ .command("inspect", "Print the resolved manifest")
39
+ .command("validate", "Check the project for issues")
40
+ .command("build", "Run the project's build")
41
+ .command("pack", "Pack the game into a zip")
42
+ .command("serve", "Serve the game locally")
43
+ .command("deploy", "Deploy the game")
44
44
  .command("open", "Open a game's store page")
45
45
  .command("play", "Play a game")
46
46
  .command("login", `Log in to ${TITLE}`)
47
- .command("whoami", `Show ${TITLE} account`)
47
+ .command("whoami", "Show the logged-in account")
48
48
  .command("logout", `Log out of ${TITLE}`)
49
49
  .help("help")
50
50
  .alias("help", "h")
package/bin/arc-deploy.js CHANGED
@@ -17,6 +17,7 @@ const build = require("../src/cli/build");
17
17
  const cfgcode = require("../src/config/code");
18
18
  const cfgpackage = require("../src/config/package");
19
19
  const cfgproduct = require("../src/config/product");
20
+ const checked = require("../src/action/validate/checked");
20
21
  const deploy = require("../src/cli/deploy");
21
22
  const error = require("../src/util/error");
22
23
  const links = require("../src/util/links");
@@ -97,7 +98,7 @@ const handler = function ({ json, pb, quiet, sw, warnings }) {
97
98
  if (pb) {
98
99
  pb.stop();
99
100
  }
100
- log.pw(`⚠ ${message}`);
101
+ log.pw(message);
101
102
  sw.start(false);
102
103
  if (pb) {
103
104
  pb.start();
@@ -126,7 +127,11 @@ const report = function (
126
127
  heading = phrase.releasePublished(version.to, channel, built);
127
128
  next = null;
128
129
  }
129
- log.result(heading, [store ? ["Store:", store] : null, ["Play:", url], next]);
130
+ log.result(heading, [
131
+ store ? [phrase.storeLabel(), store] : null,
132
+ [phrase.playLabel(), url],
133
+ next,
134
+ ]);
130
135
  };
131
136
 
132
137
  // [function]
@@ -259,27 +264,19 @@ const start = function (argv) {
259
264
  let parsed = null;
260
265
  // what --json lists under `warnings`: arc validate's, then the deploy's.
261
266
  const warnings = [];
262
- let sw = new SpinnerWheel("Validating... %s");
267
+ let sw = new SpinnerWheel("Validating… %s");
263
268
  sw.start(json || quiet);
264
269
  return checkTimeout(timeout)
265
- .then(() => project(root))
270
+ .then(() =>
271
+ checked(root, (found) => {
272
+ warnings.push(...found);
273
+ }),
274
+ )
266
275
  .then((proj) => {
267
276
  parsed = proj;
268
- return validate({ project: parsed, root });
269
- })
270
- .then((validated) => {
271
- const count = validated.issues.length;
272
- warnings.push(...validated.warnings);
273
- if (count) {
274
- throw new ArcError(
275
- cfgcode.VALIDATION_FAILED,
276
- phrase.issuesFound(count),
277
- { data: { issues: validated.issues } },
278
- );
279
- }
280
277
  sw.stop(true);
281
- warn(validated.warnings, { json, quiet });
282
- sw = new SpinnerWheel("Checking... %s");
278
+ warn(warnings, { json, quiet });
279
+ sw = new SpinnerWheel("Checking… %s");
283
280
  // git deploys need no login: anyone who can push to the repository can
284
281
  // release, and the Arcadible GitHub App publishes the release. Anyone
285
282
  // can wait for it too; the game's owner, logged in, waits among their
@@ -296,7 +293,7 @@ const start = function (argv) {
296
293
  throw new ArcError(cfgcode.NOT_LOGGED_IN, phrase.notLoggedIn());
297
294
  }
298
295
  sw.stop(true);
299
- sw = new SpinnerWheel("Preparing... %s");
296
+ sw = new SpinnerWheel("Preparing… %s");
300
297
  return Promise.all([
301
298
  // whoami
302
299
  current,
@@ -314,7 +311,7 @@ const start = function (argv) {
314
311
  sw.stop(true);
315
312
  owner = predeployed.owner ?? null;
316
313
  held = predeployed.held ?? false;
317
- sw = new SpinnerWheel("Building... %s");
314
+ sw = new SpinnerWheel("Building… %s");
318
315
  sw.start(git || json || quiet);
319
316
  return Promise.all([
320
317
  // whoami
@@ -345,7 +342,7 @@ const start = function (argv) {
345
342
  }
346
343
  parsed = proj;
347
344
  sw.stop(true);
348
- sw = new SpinnerWheel("Packing... %s");
345
+ sw = new SpinnerWheel("Packing… %s");
349
346
  sw.start(git || json || quiet);
350
347
  return Promise.all([
351
348
  // whoami
@@ -441,7 +438,8 @@ const main = function (argv) {
441
438
  })
442
439
  .option("timeout", {
443
440
  default: conf.timeout,
444
- describe: "Seconds to wait [default: 300, or 900 for git]",
441
+ defaultDescription: "300, or 900 for git",
442
+ describe: "Seconds to wait",
445
443
  type: "number",
446
444
  })
447
445
  .option("cwd", args.CWD)
package/bin/arc-init.js CHANGED
@@ -31,6 +31,7 @@ const spinner = require("../src/util/spinner");
31
31
 
32
32
  const abort = output.abort;
33
33
  const ArcError = error.ArcError;
34
+ const exit = output.exit;
34
35
  const findRc = rc.findRc;
35
36
  const pathExists = fs.pathExists;
36
37
  const print = output.print;
@@ -69,7 +70,7 @@ const handler = function ({ json, quiet, sw }) {
69
70
  const start = function (argv) {
70
71
  const json = argv.json;
71
72
  const quiet = argv.quiet;
72
- const sw = new SpinnerWheel("Initializing... %s");
73
+ const sw = new SpinnerWheel("Initializing… %s");
73
74
  return init({
74
75
  handler: handler({ json, quiet, sw }),
75
76
  patch: argv.patch,
@@ -86,7 +87,7 @@ const start = function (argv) {
86
87
  print(project, { nl: true, toml: true });
87
88
  }
88
89
  // degit sometimes hangs, so exit once stdout is written.
89
- process.stdout.write("", () => process.exit(0));
90
+ return exit(0);
90
91
  })
91
92
  .catch((ex) => abort(phrase.aborted(COMMAND), ex, { json, sw }));
92
93
  };
@@ -149,7 +150,7 @@ const main = function (argv) {
149
150
  default: conf.template,
150
151
  describe: [
151
152
  "Project template",
152
- "Public GitHub repository, or a folder in one (user/repo[/folder]).",
153
+ "Public GitHub repository, or a folder in one (user/repo[/folder])",
153
154
  ].join("\n"),
154
155
  type: "string",
155
156
  })
package/bin/arc-login.js CHANGED
@@ -137,12 +137,12 @@ const main = function (argv) {
137
137
  .usage("$0 [options]")
138
138
  .option("browser", {
139
139
  default: conf.browser,
140
- describe: "Open a web browser on Enter (disable with --no-browser)",
140
+ describe: "Open a web browser on Enter",
141
141
  type: "boolean",
142
142
  })
143
143
  .option("qrcode", {
144
144
  default: conf.qrcode,
145
- describe: "Display a qrcode (disable with --no-qrcode)",
145
+ describe: "Show a QR code",
146
146
  type: "boolean",
147
147
  })
148
148
  .help("help")
package/bin/arc-pack.js CHANGED
@@ -13,23 +13,19 @@ const yargs = require("yargs/yargs");
13
13
  // [import]
14
14
 
15
15
  const args = require("../src/util/argv");
16
- const cfgcode = require("../src/config/code");
17
16
  const cfgpackage = require("../src/config/package");
18
17
  const cfgproduct = require("../src/config/product");
19
- const error = require("../src/util/error");
18
+ const checked = require("../src/action/validate/checked");
20
19
  const log = require("../src/util/log");
21
20
  const output = require("../src/util/output");
22
21
  const pack = require("../src/cli/pack");
23
22
  const phrase = require("../src/phrase");
24
- const project = require("../src/parser/project");
25
23
  const rc = require("../src/util/rc");
26
24
  const spinner = require("../src/util/spinner");
27
- const validate = require("../src/cli/validate");
28
25
 
29
26
  // [use]
30
27
 
31
28
  const abort = output.abort;
32
- const ArcError = error.ArcError;
33
29
  const findRc = rc.findRc;
34
30
  const print = output.print;
35
31
  const resolve = path.resolve;
@@ -62,26 +58,16 @@ const start = function (argv) {
62
58
  let parsed = null;
63
59
  // what --json lists under `warnings`.
64
60
  let warnings = [];
65
- let sw = new SpinnerWheel("Validating... %s");
61
+ let sw = new SpinnerWheel("Validating… %s");
66
62
  sw.start(json || quiet);
67
- return project(root)
63
+ return checked(root, (found) => {
64
+ warnings = found;
65
+ })
68
66
  .then((proj) => {
69
67
  parsed = proj;
70
- return validate({ project: parsed, root });
71
- })
72
- .then((validated) => {
73
- const count = validated.issues.length;
74
- warnings = validated.warnings;
75
- if (count) {
76
- throw new ArcError(
77
- cfgcode.VALIDATION_FAILED,
78
- phrase.issuesFound(count),
79
- { data: { issues: validated.issues } },
80
- );
81
- }
82
68
  sw.stop(true);
83
69
  warn(warnings, { json, quiet });
84
- sw = new SpinnerWheel("Packing... %s");
70
+ sw = new SpinnerWheel("Packing… %s");
85
71
  sw.start(json || quiet);
86
72
  return pack({
87
73
  input: argv.input,
@@ -116,13 +102,13 @@ const main = function (argv) {
116
102
  .option("input", {
117
103
  alias: "i",
118
104
  default: conf.input,
119
- describe: "Set the input directory",
105
+ describe: "Pack this directory instead of deploy_dir",
120
106
  type: "string",
121
107
  })
122
108
  .option("output", {
123
109
  alias: "o",
124
110
  default: conf.output,
125
- describe: "Set the output file",
111
+ describe: "Write the zip to this path",
126
112
  type: "string",
127
113
  })
128
114
  .option("json", { ...args.JSON_OUTPUT, default: conf.json })
package/bin/arc-play.js CHANGED
@@ -64,7 +64,7 @@ const handler = function ({ quiet, sw }) {
64
64
  // [function]
65
65
 
66
66
  const start = function (argv) {
67
- const sw = new SpinnerWheel("Opening... %s");
67
+ const sw = new SpinnerWheel("Opening… %s");
68
68
  return play({
69
69
  game: argv.game,
70
70
  handler: handler({ quiet: argv.quiet, sw }),
package/bin/arc-serve.js CHANGED
@@ -111,7 +111,7 @@ const start = function (argv) {
111
111
  // `.arcrc` values are strings, and a flag that isn't a number is NaN.
112
112
  const port = Number(argv.port);
113
113
  const root = resolve(process.cwd(), argv.cwd);
114
- const sw = new SpinnerWheel("Building... %s");
114
+ const sw = new SpinnerWheel("Building… %s");
115
115
  return checkPort(port)
116
116
  .then(() => checkUrl(argv.url))
117
117
  .then((url) =>
@@ -141,7 +141,7 @@ const start = function (argv) {
141
141
  process.once("SIGTERM", stop);
142
142
  if (!quiet) {
143
143
  log.result(phrase.served(served), [
144
- ["Play:", `${served.page}/`],
144
+ [phrase.playLabel(), `${served.page}/`],
145
145
  [phrase.serveAlone(), served.game],
146
146
  served.url
147
147
  ? phrase.serveNextUrl()
@@ -173,7 +173,7 @@ const main = function (argv) {
173
173
  .option("port", {
174
174
  alias: "p",
175
175
  default: conf.port,
176
- describe: "Port to serve on",
176
+ describe: "Serve on this port",
177
177
  type: "number",
178
178
  })
179
179
  .option("build", {
@@ -182,6 +182,7 @@ const main = function (argv) {
182
182
  type: "boolean",
183
183
  })
184
184
  .option("url", {
185
+ default: conf.url,
185
186
  describe: "Frame another server's page instead of deploy_dir",
186
187
  type: "string",
187
188
  })
@@ -26,6 +26,7 @@ const validate = require("../src/cli/validate");
26
26
 
27
27
  const abort = output.abort;
28
28
  const dereference = schema.dereference;
29
+ const exit = output.exit;
29
30
  const findRc = rc.findRc;
30
31
  const print = output.print;
31
32
  const resolve = path.resolve;
@@ -75,7 +76,7 @@ const start = function (argv) {
75
76
  log.hs(phrase.noIssuesFound());
76
77
  }
77
78
  }
78
- process.exit(count ? 1 : 0);
79
+ return exit(count ? 1 : 0);
79
80
  })
80
81
  .catch((ex) => abort(phrase.aborted(COMMAND), ex, { json }));
81
82
  };
package/bin/arc-whoami.js CHANGED
@@ -54,7 +54,7 @@ const handler = function ({ json, sw }) {
54
54
 
55
55
  const start = function (argv) {
56
56
  const json = argv.json;
57
- const sw = new SpinnerWheel("Checking... %s");
57
+ const sw = new SpinnerWheel("Checking… %s");
58
58
  return whoami({ handler: handler({ json, sw }) })
59
59
  .then((current) => {
60
60
  const user = current?.user;
package/package.json CHANGED
@@ -65,5 +65,5 @@
65
65
  "scripts": {
66
66
  "test": "node --test \"test/**/*.test.js\""
67
67
  },
68
- "version": "1.0.2"
68
+ "version": "1.0.5"
69
69
  }
package/src/account.js CHANGED
@@ -102,7 +102,9 @@ const getSaved = function (onRefresh) {
102
102
  return readFile(file, "utf8")
103
103
  .then(JSON.parse)
104
104
  .catch((ex) => {
105
- if (ex.code === "ENOENT") {
105
+ // a session file cut short (a full disk, a crash while saving) is no
106
+ // session: `arc login` writes a new one
107
+ if (ex.code === "ENOENT" || ex instanceof SyntaxError) {
106
108
  return null;
107
109
  }
108
110
  throw ex;
@@ -35,9 +35,13 @@ const execBuild = function (command, { cwdPath }) {
35
35
  if (err) {
36
36
  const { code, signal } = err;
37
37
  reject(
38
- new ArcError(cfgcode.BUILD_FAILED, phrase.buildFailed({ code, signal, stderr }), {
39
- cause: err,
40
- }),
38
+ new ArcError(
39
+ cfgcode.BUILD_FAILED,
40
+ phrase.buildFailed({ code, signal, stderr, stdout }),
41
+ {
42
+ cause: err,
43
+ },
44
+ ),
41
45
  );
42
46
  return;
43
47
  }
@@ -12,7 +12,6 @@ const repo = require("../predeploy/git");
12
12
  // [use]
13
13
 
14
14
  const ArcError = error.ArcError;
15
- const failWith = repo.failWith;
16
15
  const git = repo.git;
17
16
  const tagOf = repo.tagOf;
18
17
 
@@ -25,7 +24,13 @@ const createTag = function (root, version) {
25
24
  : phrase.versionPrerelease(version);
26
25
  return git(root, ["tag", "-a", tagOf(version), "-m", message]).then(
27
26
  () => null,
28
- failWith(cfgcode.GIT_TAG_CREATE_FAILED, phrase.gitTagCreateFailed(version)),
27
+ (err) => {
28
+ throw new ArcError(
29
+ cfgcode.GIT_TAG_CREATE_FAILED,
30
+ phrase.gitTagCreateFailed(version, err.stderr),
31
+ { cause: err },
32
+ );
33
+ },
29
34
  );
30
35
  };
31
36
 
@@ -52,7 +57,7 @@ const pushTag = function (root, version) {
52
57
  version,
53
58
  new ArcError(
54
59
  cfgcode.GIT_TAG_PUSH_FAILED,
55
- phrase.gitTagPushFailed(version),
60
+ phrase.gitTagPushFailed(version, err.stderr),
56
61
  {
57
62
  cause: err,
58
63
  },
@@ -33,18 +33,19 @@ const uploadBuild = function (current, artifact, { id, version }, onProgress) {
33
33
  s3.upload(upload.upload_url, artifact, onProgress),
34
34
  ]),
35
35
  (err) => {
36
- // the API refuses a game that was deleted or is another account's,
37
- // and the same version uploaded again within 5 minutes.
36
+ // the API refuses a game that was deleted or is another account's
37
+ // (403), and the same version deployed again within 5 minutes (429).
38
38
  if (err.status === 403) {
39
39
  throw new ArcError(
40
40
  cfgcode.GAME_UNAVAILABLE,
41
41
  phrase.gameUnavailable(id),
42
+ { cause: err },
42
43
  );
43
44
  }
44
45
  if (err.status === 429) {
45
46
  throw new ArcError(
46
- cfgcode.UPLOAD_RATE_LIMITED,
47
- phrase.uploadRateLimited(version),
47
+ cfgcode.DEPLOY_RATE_LIMITED,
48
+ phrase.deployRateLimited(version),
48
49
  { cause: err },
49
50
  );
50
51
  }
@@ -15,6 +15,7 @@ const cfgcode = require("../../config/code");
15
15
  const error = require("../../util/error");
16
16
  const manifest = require("./manifest");
17
17
  const phrase = require("../../phrase");
18
+ const templates = require("../../config/template");
18
19
 
19
20
  // [use]
20
21
 
@@ -38,10 +39,14 @@ const removeLicense = function (dest) {
38
39
 
39
40
  // [function]
40
41
 
42
+ // a starter's name on its own, such as `blank`, suggests the starter.
41
43
  const unavailable = function (src, cause) {
44
+ const starter = templates.choices.find(
45
+ ({ value }) => value.split("/").pop() === src,
46
+ );
42
47
  return new ArcError(
43
48
  cfgcode.TEMPLATE_UNAVAILABLE,
44
- phrase.templateUnavailable(src),
49
+ phrase.templateUnavailable(src, starter?.value),
45
50
  { cause },
46
51
  );
47
52
  };
@@ -64,8 +69,11 @@ const scaffold = function (src, dest, { onInitBegin }) {
64
69
  if (onInitBegin) {
65
70
  onInitBegin({ dest, src });
66
71
  }
67
- return degit(src, { cache: false, force: true, verbose: false })
68
- .clone(dest)
72
+ // degit throws as it's made when it can't parse `src`.
73
+ return Promise.resolve()
74
+ .then(() =>
75
+ degit(src, { cache: false, force: true, verbose: false }).clone(dest),
76
+ )
69
77
  .catch((ex) => {
70
78
  throw unavailable(src, ex);
71
79
  })
@@ -48,13 +48,20 @@ const SIZE_PARTIAL = cfglimit.ZIP_SIZE;
48
48
 
49
49
  // [function]
50
50
 
51
- // the build's folder named like the reserved one, if it has one.
51
+ // the first path at the build's root that's Arcadible's, if it has one: a
52
+ // folder named like the reserved one, or the manifest or media that packing
53
+ // adds, as `arc validate` checks deploy_dir. A folder ends with "/".
52
54
  const findReserved = function (zip) {
53
- const folder = zip
55
+ return zip
54
56
  .entries()
55
57
  .map((entry) => entry.name.split("/"))
56
- .find((parts) => parts.length > 1 && parts[0].toLowerCase() === RESERVED);
57
- return folder?.[0];
58
+ .map((parts) => (parts.length > 1 ? `${parts[0]}/` : parts[0]))
59
+ .find(
60
+ (name) =>
61
+ name.toLowerCase() === `${RESERVED}/` ||
62
+ name === MANIFEST ||
63
+ name === `${MEDIA}/`,
64
+ );
58
65
  };
59
66
 
60
67
  // [function]
@@ -68,7 +68,10 @@ const ensureHandle = function (current, { handle, id, version }, handler) {
68
68
  );
69
69
  }
70
70
  if (status === "limit") {
71
- warn(cfgcode.WARNING.GAME_HANDLE_LIMIT, phrase.gameHandleLimit(handle));
71
+ warn(
72
+ cfgcode.WARNING.GAME_HANDLE_LIMIT_REACHED,
73
+ phrase.gameHandleLimit(handle),
74
+ );
72
75
  return true;
73
76
  }
74
77
  if (status === "lapsed") {
@@ -35,9 +35,9 @@ const NAME_REPO = /^[A-Za-z0-9._-]{1,100}$/u;
35
35
  // runs git in `root`, and resolves with its stdout.
36
36
  const git = function (root, args) {
37
37
  return new Promise((resolve, reject) => {
38
- execFile("git", ["-C", root, ...args], (err, stdout) => {
38
+ execFile("git", ["-C", root, ...args], (err, stdout, stderr) => {
39
39
  if (err) {
40
- reject(err);
40
+ reject(Object.assign(err, { stderr }));
41
41
  return;
42
42
  }
43
43
  resolve(stdout);
@@ -6,6 +6,7 @@
6
6
  // [import]
7
7
 
8
8
  const path = require("node:path");
9
+ const timers = require("node:timers/promises");
9
10
 
10
11
  // [import]
11
12
 
@@ -39,6 +40,7 @@ const relative = path.relative;
39
40
  const remoteFiles = remote.remoteFiles;
40
41
  const resolvePath = path.resolve;
41
42
  const serve = server.serve;
43
+ const sleep = timers.setTimeout;
42
44
  const watchDir = reload.watchDir;
43
45
 
44
46
  // [declare]
@@ -48,7 +50,7 @@ const CHANNEL = "local";
48
50
  // the saves of a game with no id yet, kept apart from any other's.
49
51
  const UNNAMED = "local";
50
52
 
51
- // [function]
53
+ // [declare]
52
54
 
53
55
  // the headers a page needs for the play page to frame it: the play page is
54
56
  // cross-origin isolated, so a cross-origin frame must be too, and allow it.
@@ -57,19 +59,33 @@ const FRAMEABLE = {
57
59
  "cross-origin-resource-policy": ["cross-origin"],
58
60
  };
59
61
 
62
+ // [declare]
63
+
64
+ // how long `url` has to answer before it counts as unreachable: a server
65
+ // that takes the connection and never answers would otherwise hold `arc
66
+ // serve` for minutes.
67
+ const PROBE_MS = 10000;
68
+
60
69
  // [function]
61
70
 
62
71
  // what `url` says when asked: `{ reachable, frameable }`, where `frameable`
63
72
  // says whether it sends the headers the play page needs to frame it.
64
73
  const probe = async function (url) {
65
74
  try {
66
- const res = await fetch(url, { method: "GET" });
75
+ // null when it hasn't answered in time
76
+ const res = await Promise.race([
77
+ fetch(url, { method: "GET" }),
78
+ sleep(PROBE_MS, null, { ref: false }),
79
+ ]);
80
+ if (!res) {
81
+ return { frameable: false, reachable: false };
82
+ }
67
83
  await res.body?.cancel();
68
84
  const frameable = Object.entries(FRAMEABLE).every(([name, values]) =>
69
85
  values.includes(res.headers.get(name)?.trim().toLowerCase()),
70
86
  );
71
87
  return { frameable, reachable: true };
72
- } catch (ex) {
88
+ } catch {
73
89
  return { frameable: false, reachable: false };
74
90
  }
75
91
  };