Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ would only work in one of those places.
## CLI

```sh
moshpit-resolve <name> [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--json]
moshpit-resolve <name...> [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N] [--json]
```

```
Expand All @@ -84,6 +84,17 @@ every navigation, and the thing you need when a name goes somewhere unexpected.
Use `--json` when another tool needs the registry answer, decision, reason, and
destination without parsing the human-readable summary.

Pass more than one name to inspect a batch in a single process. Results stay in
input order, normalized duplicates share one lookup, and no more than eight
registry requests run at once by default. Use `--concurrency` to tune that
limit for a self-hosted registry. Multi-name JSON output is an array, while the
existing single-name JSON object is unchanged.

```sh
moshpit-resolve blue.eggs red.eggs missing.eggs --json
moshpit-resolve one.eggs two.eggs three.eggs --concurrency 2
```

Registry lookups use an eight-second deadline by default. Scripts and
self-hosted deployments can lower it without changing the resolution policy:

Expand Down
129 changes: 110 additions & 19 deletions bin/moshpit-resolve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,31 @@
// reason — which is the part you need when a name goes somewhere unexpected.

import {
DEFAULT_LOOKUP_TIMEOUT_MS, parseRegistryName, resolutionFor,
DEFAULT_CONCURRENCY, DEFAULT_LOOKUP_TIMEOUT_MS, parseRegistryName, resolutionFor,
} from "../lib/index.mjs";

const USAGE = `moshpit-resolve — where a Moshpit name would send you

moshpit-resolve <name> [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS]
moshpit-resolve <name...> [--moshpit] [--clearnet-resolves] [--registry URL] [--console URL] [--parking URL] [--timeout MS] [--concurrency N]

--moshpit let a registered name beat a clearnet answer
--clearnet-resolves pretend the real internet has an answer for this name
--registry URL a self-hosted pit
--console URL a custom namespace management console
--parking URL a custom base for unpointed names
--timeout MS registry request deadline (default: ${DEFAULT_LOOKUP_TIMEOUT_MS})
--concurrency N maximum simultaneous batch lookups (default: ${DEFAULT_CONCURRENCY})
--json print a machine-readable resolution decision

Prints the destination and the reason for it. No browser, no navigation.`;

const args = process.argv.slice(2);
const valueFlags = new Set(["--registry", "--console", "--parking", "--timeout"]);
const valueFlags = new Set([
"--registry", "--console", "--parking", "--timeout", "--concurrency",
]);
const positional = args.filter((a, i) => !a.startsWith("--") && !valueFlags.has(args[i - 1]));
const name = positional[0];
const names = positional;
const name = names[0];
if (!name || args.includes("--help")) { console.log(USAGE); process.exit(name ? 0 : 1); }

const flag = (n) => args.includes(`--${n}`);
Expand All @@ -36,11 +40,24 @@ const value = (n, d) => {
};
const raw = flag("json");
const timeoutValue = value("timeout", null);
const concurrencyValue = value("concurrency", null);
const jsonError = (error) => names.length === 1
? { name, error }
: names.map((requestedName) => ({ name: requestedName, error }));
const printJsonError = (error) => new Promise((resolve, reject) => {
const output = `${JSON.stringify(
jsonError(error), null, names.length > 1 ? 2 : undefined,
)}\n`;
process.stdout.write(output, (writeError) => {
if (writeError) reject(writeError);
else resolve();
});
});

for (const option of ["registry", "console", "parking"]) {
if (flag(option) && value(option, null) === null) {
const error = `--${option} requires a URL`;
if (raw) console.log(JSON.stringify({ name, error }));
if (raw) await printJsonError(error);
else console.error(`moshpit-resolve: ${error}`);
process.exit(1);
}
Expand All @@ -52,14 +69,19 @@ if (args.includes("--timeout") && (
|| Number(timeoutValue) < 1
)) {
const error = "--timeout must be a positive integer in milliseconds";
if (raw) console.log(JSON.stringify({ name, error }));
if (raw) await printJsonError(error);
else console.error(`moshpit-resolve: ${error}`);
process.exit(1);
}

if (!parseRegistryName(name)) {
const error = "not a Moshpit name (one label and one ending)";
console.log(raw ? JSON.stringify({ name, error }) : `${name} — ${error}`);
if (args.includes("--concurrency") && (
!/^\d+$/.test(String(concurrencyValue ?? ""))
|| !Number.isSafeInteger(Number(concurrencyValue))
|| Number(concurrencyValue) < 1
)) {
const error = "--concurrency must be a positive integer";
if (raw) await printJsonError(error);
else console.error(`moshpit-resolve: ${error}`);
process.exit(1);
}

Expand All @@ -70,16 +92,85 @@ const config = {
parkingBase: value("parking", undefined),
timeoutMs: timeoutValue === null ? DEFAULT_LOOKUP_TIMEOUT_MS : Number(timeoutValue),
};
const {
registry: moshpit, decision, destination: url,
} = await resolutionFor(name, flag("clearnet-resolves"), config);

if (raw) {
console.log(JSON.stringify({ name, registry: moshpit, decision, destination: url }, null, 2));
const resolutions = new Map();

async function resolveOne(requestedName) {
const parsed = parseRegistryName(requestedName);
if (!parsed) {
return {
name: requestedName,
error: "not a Moshpit name (one label and one ending)",
};
}

const normalized = `${parsed.label}.${parsed.tld}`;
let resolution = resolutions.get(normalized);
if (!resolution) {
resolution = resolutionFor(normalized, flag("clearnet-resolves"), config);
resolutions.set(normalized, resolution);
}

const {
registry, decision, destination,
} = await resolution;
return { name: requestedName, registry, decision, destination };
}

async function mapWithConcurrency(items, limit, mapper) {
const results = new Array(items.length);
let next = 0;

const workers = Array.from(
{ length: Math.min(limit, items.length) },
async () => {
while (next < items.length) {
const index = next++;
results[index] = await mapper(items[index]);
}
},
);

await Promise.all(workers);
return results;
}

function printHuman(result) {
if (result.error) {
console.log(`${result.name} — ${result.error}`);
return;
}

console.log(`${result.name}`);
console.log(` registry ${result.registry ? JSON.stringify(result.registry) : "unreachable"}`);
console.log(` decision ${result.decision.use}`);
console.log(` reason ${result.decision.reason}`);
console.log(` goes to ${result.destination ?? "(nowhere — the browser keeps its own answer)"}`);
}

const concurrency = concurrencyValue === null
? DEFAULT_CONCURRENCY
: Number(concurrencyValue);
const results = await mapWithConcurrency(names, concurrency, resolveOne);

if (names.length === 1) {
const result = results[0];
if (result.error) {
console.log(raw ? JSON.stringify(result) : `${result.name} — ${result.error}`);
process.exit(1);
}

if (raw) console.log(JSON.stringify(result, null, 2));
else printHuman(result);
} else {
console.log(`${name}`);
console.log(` registry ${moshpit ? JSON.stringify(moshpit) : "unreachable"}`);
console.log(` decision ${decision.use}`);
console.log(` reason ${decision.reason}`);
console.log(` goes to ${url ?? "(nowhere — the browser keeps its own answer)"}`);
if (raw) {
console.log(JSON.stringify(results, null, 2));
} else {
results.forEach((result, index) => {
if (index > 0) console.log("");
printHuman(result);
});
}

if (results.some((result) => result.error)) process.exitCode = 1;
}
1 change: 1 addition & 0 deletions lib/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

export const DEFAULT_REGISTRY_BASE = 'https://pit.moshcode.sh';
export const DEFAULT_CONSOLE_BASE = 'https://app.moshcode.sh';
export const DEFAULT_CONCURRENCY = 8;

// The one label meaning "manage this namespace" rather than "visit a name".
// Reserved, not claimable — otherwise whoever holds `.eggs` could register
Expand Down
Loading
Loading