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 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: "neutral",
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.11";
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {