@oxy-hq/sdk 2.1.0 → 2.3.0

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/dist/index.mjs CHANGED
@@ -147,6 +147,52 @@ async function getCustomerAppDebug(resolved) {
147
147
 
148
148
  //#endregion
149
149
  //#region src/customer-app/errors.ts
150
+ /**
151
+ * Error thrown by all customer-app hooks when an API call returns a
152
+ * non-2xx response. Carries the structured `code` + `hint` the server
153
+ * emits so bundle UIs can render an actionable message instead of
154
+ * "404: { ...json... }".
155
+ */
156
+ var OxyApiError = class extends Error {
157
+ constructor(opts) {
158
+ const base = opts.message || `HTTP ${opts.status}`;
159
+ const code = opts.code ? ` [${opts.code}]` : "";
160
+ const hint = opts.hint ? `\n\n${opts.hint}` : "";
161
+ super(`${base}${code}${hint}`);
162
+ this.name = "OxyApiError";
163
+ this.status = opts.status;
164
+ this.code = opts.code ?? null;
165
+ this.hint = opts.hint ?? null;
166
+ }
167
+ };
168
+ /**
169
+ * Read a non-2xx response from oxy and return an `OxyApiError`.
170
+ * Parses the JSON envelope when present; falls back to raw text
171
+ * (truncated to 240 chars so a runaway HTML error page doesn't
172
+ * dominate the bundle UI).
173
+ */
174
+ async function apiErrorFromResponse(resp) {
175
+ let body;
176
+ let raw = "";
177
+ try {
178
+ raw = await resp.text();
179
+ body = raw ? JSON.parse(raw) : void 0;
180
+ } catch {}
181
+ if (body && typeof body === "object") {
182
+ const b = body;
183
+ return new OxyApiError({
184
+ status: resp.status,
185
+ message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
186
+ code: typeof b.code === "string" ? b.code : null,
187
+ hint: typeof b.hint === "string" ? b.hint : null
188
+ });
189
+ }
190
+ const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
191
+ return new OxyApiError({
192
+ status: resp.status,
193
+ message: snippet || `HTTP ${resp.status}`
194
+ });
195
+ }
150
196
  const ARCH_DOC = "internal-docs/customer-apps.md";
151
197
  /** Interpret a thrown error as a structured report for UI display. */
152
198
  function interpretCustomerAppError(err) {
@@ -393,6 +439,24 @@ function validateFunctions(raw) {
393
439
  fn.cache = { ttlSeconds: ttl };
394
440
  }
395
441
  }
442
+ if (value.retries !== void 0) {
443
+ const r = value.retries;
444
+ if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
445
+ const retries = {};
446
+ for (const key of [
447
+ "maxAttempts",
448
+ "minTimeoutMs",
449
+ "maxTimeoutMs"
450
+ ]) {
451
+ const n = r[key];
452
+ if (n !== void 0) {
453
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
454
+ retries[key] = n;
455
+ }
456
+ }
457
+ fn.retries = retries;
458
+ }
459
+ if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
396
460
  const hasSchedule = fn.schedule !== void 0;
397
461
  const hasAirway = fn.airwayStep !== void 0;
398
462
  if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
@@ -406,7 +470,7 @@ function isRecord(v) {
406
470
 
407
471
  //#endregion
408
472
  //#region src/customer-app/function-invoke.ts
409
- const inflight = /* @__PURE__ */ new Map();
473
+ const inflight$1 = /* @__PURE__ */ new Map();
410
474
  /**
411
475
  * Dedup key for an invocation: function name + its (stable-serialized) body,
412
476
  * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
@@ -421,12 +485,12 @@ function functionInvokeKey(name, body) {
421
485
  * this dedups concurrency only, it does NOT memoize the result.
422
486
  */
423
487
  function sharedFunctionInvoke(key, run) {
424
- const existing = inflight.get(key);
488
+ const existing = inflight$1.get(key);
425
489
  if (existing) return existing;
426
490
  const p = run().finally(() => {
427
- inflight.delete(key);
491
+ inflight$1.delete(key);
428
492
  });
429
- inflight.set(key, p);
493
+ inflight$1.set(key, p);
430
494
  return p;
431
495
  }
432
496
 
@@ -551,6 +615,50 @@ function isSafeLinkHref(raw) {
551
615
  return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
552
616
  }
553
617
 
618
+ //#endregion
619
+ //#region src/customer-app/query-cache.ts
620
+ const SWR_TTL_MS = 3e4;
621
+ const inflight = /* @__PURE__ */ new Map();
622
+ const cache = /* @__PURE__ */ new Map();
623
+ function queryKey(projectId, db, sql) {
624
+ return `${projectId} ${db ?? ""} ${sql}`;
625
+ }
626
+ function getCached(projectId, sql, db) {
627
+ const e = cache.get(queryKey(projectId, db, sql));
628
+ return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
629
+ }
630
+ /** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
631
+ * short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
632
+ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
633
+ const key = queryKey(projectId, db, sql);
634
+ if (!opts.force) {
635
+ const fresh = getCached(projectId, sql, db);
636
+ if (fresh) return fresh;
637
+ }
638
+ const existing = inflight.get(key);
639
+ if (existing) return existing;
640
+ const body = JSON.stringify({
641
+ sql,
642
+ ...db ? { database: db } : {}
643
+ });
644
+ const p = (async () => {
645
+ const resp = await fetcher(`/api/projects/${projectId}/query`, {
646
+ method: "POST",
647
+ headers: { "content-type": "application/json" },
648
+ body
649
+ });
650
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
651
+ const data = await resp.json();
652
+ cache.set(key, {
653
+ at: Date.now(),
654
+ data
655
+ });
656
+ return data;
657
+ })().finally(() => inflight.delete(key));
658
+ inflight.set(key, p);
659
+ return p;
660
+ }
661
+
554
662
  //#endregion
555
663
  //#region src/customer-app/react.tsx
556
664
  function defaultFetcher(input, init) {
@@ -655,58 +763,6 @@ function warnBetaOnce(name) {
655
763
  if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
656
764
  }
657
765
  /**
658
- * Error thrown by all customer-app hooks when an API call returns a
659
- * non-2xx response. Carries the structured `code` + `hint` the server
660
- * emits so bundle UIs can render an actionable message instead of
661
- * "404: { ...json... }".
662
- *
663
- * The server contract is documented in
664
- * `crates/app/src/server/api/projects/agent_ask.rs` and
665
- * `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
666
- * Hooks that previously wrapped the raw text in `new Error()` now
667
- * throw this type instead.
668
- */
669
- var OxyApiError = class extends Error {
670
- constructor(opts) {
671
- const base = opts.message || `HTTP ${opts.status}`;
672
- const code = opts.code ? ` [${opts.code}]` : "";
673
- const hint = opts.hint ? `\n\n${opts.hint}` : "";
674
- super(`${base}${code}${hint}`);
675
- this.name = "OxyApiError";
676
- this.status = opts.status;
677
- this.code = opts.code ?? null;
678
- this.hint = opts.hint ?? null;
679
- }
680
- };
681
- /**
682
- * Read a non-2xx response from oxy and return an `OxyApiError`.
683
- * Parses the JSON envelope when present; falls back to raw text
684
- * (truncated to 240 chars so a runaway HTML error page doesn't
685
- * dominate the bundle UI).
686
- */
687
- async function apiErrorFromResponse(resp) {
688
- let body;
689
- let raw = "";
690
- try {
691
- raw = await resp.text();
692
- body = raw ? JSON.parse(raw) : void 0;
693
- } catch {}
694
- if (body && typeof body === "object") {
695
- const b = body;
696
- return new OxyApiError({
697
- status: resp.status,
698
- message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
699
- code: typeof b.code === "string" ? b.code : null,
700
- hint: typeof b.hint === "string" ? b.hint : null
701
- });
702
- }
703
- const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
704
- return new OxyApiError({
705
- status: resp.status,
706
- message: snippet || `HTTP ${resp.status}`
707
- });
708
- }
709
- /**
710
766
  * Read the resolved manifest from context. Throws if called outside
711
767
  * `<OxyAppProvider>` — that's a programmer error worth surfacing
712
768
  * loudly, not silently swallowing.
@@ -760,36 +816,35 @@ function useQuery(input, opts = {}) {
760
816
  } : s);
761
817
  return;
762
818
  }
763
- const ctrl = new AbortController();
764
819
  let cancelled = false;
820
+ const cached = getCached(projectId, sqlWithParams, input.database);
821
+ if (cached && nonce === 0) {
822
+ const { columns, rows } = cached;
823
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
824
+ setState({
825
+ rows: objects,
826
+ columns,
827
+ loading: false,
828
+ error: null
829
+ });
830
+ return;
831
+ }
765
832
  setState((s) => ({
766
833
  ...s,
767
834
  loading: true,
768
835
  error: null
769
836
  }));
770
- const body = JSON.stringify({
771
- sql: sqlWithParams,
772
- ...input.database ? { database: input.database } : {}
773
- });
774
- fetcher(`/api/projects/${projectId}/query`, {
775
- method: "POST",
776
- headers: { "content-type": "application/json" },
777
- body,
778
- signal: ctrl.signal
779
- }).then(async (resp) => {
780
- if (!resp.ok) throw await apiErrorFromResponse(resp);
781
- return resp.json();
782
- }).then(({ columns, rows }) => {
837
+ sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
783
838
  if (cancelled) return;
839
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
784
840
  setState({
785
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
841
+ rows: objects,
786
842
  columns,
787
843
  loading: false,
788
844
  error: null
789
845
  });
790
846
  }).catch((err) => {
791
847
  if (cancelled) return;
792
- if (err instanceof DOMException && err.name === "AbortError") return;
793
848
  setState((s) => ({
794
849
  ...s,
795
850
  loading: false,
@@ -798,7 +853,6 @@ function useQuery(input, opts = {}) {
798
853
  });
799
854
  return () => {
800
855
  cancelled = true;
801
- ctrl.abort();
802
856
  };
803
857
  }, [
804
858
  enabled,
@@ -939,7 +993,8 @@ function useSemanticQuery(input, opts = {}) {
939
993
  filters: input.filters ?? [],
940
994
  ...input.limit != null ? { limit: input.limit } : {}
941
995
  });
942
- fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
996
+ const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
997
+ fetcher(url, {
943
998
  method: "POST",
944
999
  headers: { "content-type": "application/json" },
945
1000
  body,
@@ -949,8 +1004,9 @@ function useSemanticQuery(input, opts = {}) {
949
1004
  return resp.json();
950
1005
  }).then(({ columns, rows, truncated, sql }) => {
951
1006
  if (cancelled) return;
1007
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
952
1008
  setState({
953
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
1009
+ rows: objects,
954
1010
  columns,
955
1011
  truncated,
956
1012
  sql: sql ?? null,
@@ -2277,5 +2333,5 @@ var MetricTreeClient = class {
2277
2333
  };
2278
2334
 
2279
2335
  //#endregion
2280
- export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
2336
+ export { AnomaliesClient, MetricTreeClient, OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, apiErrorFromResponse, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useFunction, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery, useTrackEvent };
2281
2337
  //# sourceMappingURL=index.mjs.map