wawesome 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.mjs +153 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,7 +213,7 @@ Three headers arrive or leave on it, and the stripping is what makes them worth
|
|
|
213
213
|
| --- | --- | --- |
|
|
214
214
|
| `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
|
|
215
215
|
| `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
216
|
-
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours. |
|
|
216
|
+
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours — up to the moment your response is committed, and no further. |
|
|
217
217
|
|
|
218
218
|
### Local Development / Gateway Overrides
|
|
219
219
|
|
package/dist/index.mjs
CHANGED
|
@@ -166,7 +166,7 @@ async function buildJs(entryInput, options) {
|
|
|
166
166
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
167
167
|
* project pins — reads it here, so a release bumps one file.
|
|
168
168
|
*/
|
|
169
|
-
const CLI_VERSION = "0.0.
|
|
169
|
+
const CLI_VERSION = "0.0.13";
|
|
170
170
|
//#endregion
|
|
171
171
|
//#region src/prompt.ts
|
|
172
172
|
/**
|
|
@@ -549,6 +549,148 @@ async function whoami() {
|
|
|
549
549
|
console.log(` Gateway: ${creds.gateway_url}\n`);
|
|
550
550
|
}
|
|
551
551
|
//#endregion
|
|
552
|
+
//#region src/usage.ts
|
|
553
|
+
const USAGE_TIMEOUT_MS = 2e3;
|
|
554
|
+
/**
|
|
555
|
+
* Bounded by its own timeout: every caller treats this as an aside to something
|
|
556
|
+
* that already succeeded, so a management API that hangs must not be able to
|
|
557
|
+
* hold that something open.
|
|
558
|
+
*/
|
|
559
|
+
async function fetchTenantUsage(creds, timeoutMs = USAGE_TIMEOUT_MS) {
|
|
560
|
+
const controller = new AbortController();
|
|
561
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
562
|
+
try {
|
|
563
|
+
const res = await fetch(`${creds.gateway_url}/v1/tenant/usage`, {
|
|
564
|
+
headers: { Authorization: `Bearer ${creds.tenant_jwt}` },
|
|
565
|
+
signal: controller.signal
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok) throw await asGatewayError(res, `Failed to read usage (HTTP ${res.status}).`);
|
|
568
|
+
return asTenantUsage(await res.json());
|
|
569
|
+
} finally {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* A payload that is missing any of this is one the plan could not be resolved
|
|
575
|
+
* for, and is refused here rather than rendered as half a block.
|
|
576
|
+
*/
|
|
577
|
+
function asTenantUsage(value) {
|
|
578
|
+
const usage = value;
|
|
579
|
+
if (!(!!usage && typeof usage.plan?.name === "string" && typeof usage.plan?.limits?.app_slots === "number" && typeof usage.occupied_app_slots === "number" && !!usage.allowances && typeof usage.allowances === "object" && isInstant(usage.period?.start) && isInstant(usage.period?.end))) throw new Error("Usage payload does not carry a resolved plan.");
|
|
580
|
+
return usage;
|
|
581
|
+
}
|
|
582
|
+
function isInstant(value) {
|
|
583
|
+
return typeof value === "string" && !isNaN(new Date(value).getTime());
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/headroom.ts
|
|
587
|
+
const KNOWN_LABELS = {
|
|
588
|
+
invocations: "Invocations",
|
|
589
|
+
caller_facing_bytes: "Caller-facing bytes"
|
|
590
|
+
};
|
|
591
|
+
const COUNT_UNITS = [
|
|
592
|
+
{
|
|
593
|
+
threshold: 1e9,
|
|
594
|
+
suffix: "B"
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
threshold: 1e6,
|
|
598
|
+
suffix: "M"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
threshold: 1e3,
|
|
602
|
+
suffix: "k"
|
|
603
|
+
}
|
|
604
|
+
];
|
|
605
|
+
const BYTE_UNITS = [
|
|
606
|
+
"B",
|
|
607
|
+
"KiB",
|
|
608
|
+
"MiB",
|
|
609
|
+
"GiB",
|
|
610
|
+
"TiB"
|
|
611
|
+
];
|
|
612
|
+
const MONTHS = [
|
|
613
|
+
"January",
|
|
614
|
+
"February",
|
|
615
|
+
"March",
|
|
616
|
+
"April",
|
|
617
|
+
"May",
|
|
618
|
+
"June",
|
|
619
|
+
"July",
|
|
620
|
+
"August",
|
|
621
|
+
"September",
|
|
622
|
+
"October",
|
|
623
|
+
"November",
|
|
624
|
+
"December"
|
|
625
|
+
];
|
|
626
|
+
/** Indented to sit inside the receipt the deploy already prints. */
|
|
627
|
+
function headroomLines(usage) {
|
|
628
|
+
const lines = [
|
|
629
|
+
` Plan: ${usage.plan.name} — this deploy does not change your bill.`,
|
|
630
|
+
` Apps: ${usage.occupied_app_slots} / ${usage.plan.limits.app_slots} slots`,
|
|
631
|
+
` Usage: ${periodLabel(usage.period.start, usage.period.end)}`
|
|
632
|
+
];
|
|
633
|
+
const entries = Object.entries(usage.allowances);
|
|
634
|
+
const labelWidth = widest(entries.map(([key]) => allowanceLabel(key)));
|
|
635
|
+
const amountWidth = widest(entries.map(([key, allowance]) => amount(key, allowance.used, allowance.limit)));
|
|
636
|
+
for (const [key, allowance] of entries) {
|
|
637
|
+
const percent = allowance.consumed_percent === null ? "" : ` (${allowance.consumed_percent}%)`;
|
|
638
|
+
const line = ` ${allowanceLabel(key).padEnd(labelWidth)} ${amount(key, allowance.used, allowance.limit).padEnd(amountWidth)}${percent}`;
|
|
639
|
+
lines.push(line.trimEnd());
|
|
640
|
+
}
|
|
641
|
+
return lines;
|
|
642
|
+
}
|
|
643
|
+
function allowanceLabel(key) {
|
|
644
|
+
if (KNOWN_LABELS[key]) return KNOWN_LABELS[key];
|
|
645
|
+
const words = key.replace(/[_-]+/g, " ").trim();
|
|
646
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
647
|
+
}
|
|
648
|
+
function formatCount(value) {
|
|
649
|
+
for (let i = 0; i < COUNT_UNITS.length; i++) {
|
|
650
|
+
const { threshold, suffix } = COUNT_UNITS[i];
|
|
651
|
+
if (value < threshold) continue;
|
|
652
|
+
const scaled = round(value / threshold, 1);
|
|
653
|
+
if (scaled >= 1e3 && i > 0) {
|
|
654
|
+
const bigger = COUNT_UNITS[i - 1];
|
|
655
|
+
return `${round(value / bigger.threshold, 1)}${bigger.suffix}`;
|
|
656
|
+
}
|
|
657
|
+
return `${scaled}${suffix}`;
|
|
658
|
+
}
|
|
659
|
+
return String(Math.round(value));
|
|
660
|
+
}
|
|
661
|
+
function formatBytes(value) {
|
|
662
|
+
let scaled = value;
|
|
663
|
+
let unit = 0;
|
|
664
|
+
while (scaled >= 1024 && unit < BYTE_UNITS.length - 1) {
|
|
665
|
+
scaled /= 1024;
|
|
666
|
+
unit++;
|
|
667
|
+
}
|
|
668
|
+
return `${unit === 0 ? Math.round(scaled) : round(scaled, 2)} ${BYTE_UNITS[unit]}`;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Name the period rather than print its bounds. The end is exclusive, so a
|
|
672
|
+
* partial period names the last day inside it and not the instant after it.
|
|
673
|
+
*/
|
|
674
|
+
function periodLabel(start, end) {
|
|
675
|
+
const from = new Date(start);
|
|
676
|
+
const to = new Date(end);
|
|
677
|
+
if (from.getTime() === Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), 1) && to.getTime() === Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + 1, 1)) return `${MONTHS[from.getUTCMonth()]} ${from.getUTCFullYear()} (UTC)`;
|
|
678
|
+
return `${dayLabel(from)} – ${dayLabel(/* @__PURE__ */ new Date(to.getTime() - 1))} (UTC)`;
|
|
679
|
+
}
|
|
680
|
+
function dayLabel(date) {
|
|
681
|
+
return `${date.getUTCDate()} ${MONTHS[date.getUTCMonth()].slice(0, 3)} ${date.getUTCFullYear()}`;
|
|
682
|
+
}
|
|
683
|
+
function amount(key, used, limit) {
|
|
684
|
+
const format = key.includes("bytes") ? formatBytes : formatCount;
|
|
685
|
+
return `${format(used)} / ${format(limit)}`;
|
|
686
|
+
}
|
|
687
|
+
function round(value, places) {
|
|
688
|
+
return Number(value.toFixed(places));
|
|
689
|
+
}
|
|
690
|
+
function widest(values) {
|
|
691
|
+
return values.reduce((longest, value) => Math.max(longest, value.length), 0);
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
552
694
|
//#region ../shared/public-address.ts
|
|
553
695
|
const INVOCATION_PREFIX = "/x";
|
|
554
696
|
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
@@ -668,6 +810,12 @@ async function deploy(entryInput, options) {
|
|
|
668
810
|
} catch (err) {
|
|
669
811
|
if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
|
|
670
812
|
}
|
|
813
|
+
let headroom = null;
|
|
814
|
+
try {
|
|
815
|
+
headroom = headroomLines(await fetchTenantUsage(creds));
|
|
816
|
+
} catch (err) {
|
|
817
|
+
if (isVerbose) console.log(`[wawesome:verbose] Could not read the plan's usage: ${errorText(err)}`);
|
|
818
|
+
}
|
|
671
819
|
console.log("\n======================================================");
|
|
672
820
|
console.log("🚀 \x1B[32mDEPLOYED SUCCESSFULLY!\x1B[0m");
|
|
673
821
|
console.log("======================================================");
|
|
@@ -678,6 +826,10 @@ async function deploy(entryInput, options) {
|
|
|
678
826
|
console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
|
|
679
827
|
console.log(` ${SUBTREE_NOTE}`);
|
|
680
828
|
}
|
|
829
|
+
if (headroom) {
|
|
830
|
+
console.log("");
|
|
831
|
+
for (const line of headroom) console.log(line);
|
|
832
|
+
}
|
|
681
833
|
console.log("======================================================\n");
|
|
682
834
|
return {
|
|
683
835
|
app,
|