wawesome 0.0.11 → 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 +159 -2
- 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
|
@@ -130,7 +130,12 @@ async function buildJs(entryInput, options) {
|
|
|
130
130
|
bundle: true,
|
|
131
131
|
format: "esm",
|
|
132
132
|
outfile: outPath,
|
|
133
|
-
platform: "
|
|
133
|
+
platform: "browser",
|
|
134
|
+
conditions: [
|
|
135
|
+
"workerd",
|
|
136
|
+
"worker",
|
|
137
|
+
"browser"
|
|
138
|
+
],
|
|
134
139
|
target: "es2022",
|
|
135
140
|
treeShaking: true,
|
|
136
141
|
minify: true,
|
|
@@ -161,7 +166,7 @@ async function buildJs(entryInput, options) {
|
|
|
161
166
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
162
167
|
* project pins — reads it here, so a release bumps one file.
|
|
163
168
|
*/
|
|
164
|
-
const CLI_VERSION = "0.0.
|
|
169
|
+
const CLI_VERSION = "0.0.13";
|
|
165
170
|
//#endregion
|
|
166
171
|
//#region src/prompt.ts
|
|
167
172
|
/**
|
|
@@ -544,6 +549,148 @@ async function whoami() {
|
|
|
544
549
|
console.log(` Gateway: ${creds.gateway_url}\n`);
|
|
545
550
|
}
|
|
546
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
|
|
547
694
|
//#region ../shared/public-address.ts
|
|
548
695
|
const INVOCATION_PREFIX = "/x";
|
|
549
696
|
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
@@ -663,6 +810,12 @@ async function deploy(entryInput, options) {
|
|
|
663
810
|
} catch (err) {
|
|
664
811
|
if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
|
|
665
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
|
+
}
|
|
666
819
|
console.log("\n======================================================");
|
|
667
820
|
console.log("🚀 \x1B[32mDEPLOYED SUCCESSFULLY!\x1B[0m");
|
|
668
821
|
console.log("======================================================");
|
|
@@ -673,6 +826,10 @@ async function deploy(entryInput, options) {
|
|
|
673
826
|
console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
|
|
674
827
|
console.log(` ${SUBTREE_NOTE}`);
|
|
675
828
|
}
|
|
829
|
+
if (headroom) {
|
|
830
|
+
console.log("");
|
|
831
|
+
for (const line of headroom) console.log(line);
|
|
832
|
+
}
|
|
676
833
|
console.log("======================================================\n");
|
|
677
834
|
return {
|
|
678
835
|
app,
|