@revenexx/studio-integrations 3.6.2 → 3.7.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @revenexx/studio-integrations
2
2
 
3
+ ## 3.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 16300f0: A setting the platform refused after it had answered is asked again when its dialog is opened again, instead of showing the earlier answer (IN-82).
8
+
9
+ ## 3.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - f447c9c: A node's settings dialog that is closed and opened again shows what the platform last answered for its dynamic settings at once, instead of asking again and waiting (IN-82). The answers are kept for the tab, per tenant, node, version and the settings they depend on: one younger than 30 seconds is not asked for again, an older one is shown while it is asked for again behind it, and a refusal is never kept.
14
+
3
15
  ## 3.6.2
4
16
 
5
17
  ### Patch Changes
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
3
  "configKey": "studioIntegrations",
4
- "version": "3.6.2",
4
+ "version": "3.7.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -1,5 +1,7 @@
1
1
  export declare function useIntegrationsApi(): {
2
2
  baseUrl: import("vue").ComputedRef<string>;
3
+ /** The active tenant's slug, empty until it is known. */
4
+ tenant: import("vue").ComputedRef<any>;
3
5
  get: <T>(path: string, opts?: {
4
6
  signal?: AbortSignal;
5
7
  }) => Promise<T>;
@@ -42,6 +42,8 @@ export function useIntegrationsApi() {
42
42
  }
43
43
  return {
44
44
  baseUrl,
45
+ /** The active tenant's slug, empty until it is known. */
46
+ tenant: computed(() => onlyTenantSlug.value ?? ""),
45
47
  get: (path, opts) => request("GET", path, void 0, opts),
46
48
  post: (path, body, opts) => request("POST", path, body ?? {}, opts),
47
49
  put: (path, body) => request("PUT", path, body ?? {}),
@@ -1,6 +1,7 @@
1
1
  import { computed, onScopeDispose, ref, toValue, watch } from "vue";
2
2
  import { isVersionExecutable } from "../data/integrationNodeCatalog.js";
3
3
  import { useIntegrationsApi } from "./integrationsApi.js";
4
+ import { forgetAnswer, keepAnswer, keptAnswer } from "../data/resolveAnswerStore.js";
4
5
  import { IntegrationsApiError, errorReason } from "../utils/apiError.js";
5
6
  export function isResolvableValue(v) {
6
7
  if (v === void 0 || v === null) return false;
@@ -79,6 +80,11 @@ export function useNodeConfigResolve(opts) {
79
80
  let controller = null;
80
81
  let lastFetchedKey = null;
81
82
  let lastFetchedAt = 0;
83
+ function storeKey(key) {
84
+ const tenant = toValue(api.tenant);
85
+ if (!tenant) return void 0;
86
+ return JSON.stringify([tenant, toValue(opts.slug), toValue(opts.version), toValue(opts.target), key]);
87
+ }
82
88
  async function run(key) {
83
89
  if (controller) controller.abort();
84
90
  controller = new AbortController();
@@ -99,9 +105,13 @@ export function useNodeConfigResolve(opts) {
99
105
  error.value = null;
100
106
  lastFetchedKey = key;
101
107
  lastFetchedAt = Date.now();
108
+ const kept = storeKey(key);
109
+ if (kept) keepAnswer(kept, result, lastFetchedAt);
102
110
  } catch (err) {
103
111
  if (isAbortError(err) || signal.aborted) return;
104
112
  error.value = classifyError(err);
113
+ const kept = storeKey(key);
114
+ if (kept) forgetAnswer(kept);
105
115
  } finally {
106
116
  if (!signal.aborted) isFetching.value = false;
107
117
  }
@@ -112,8 +122,23 @@ export function useNodeConfigResolve(opts) {
112
122
  if (!force && lastFetchedKey === key && Date.now() - lastFetchedAt < STALE_MS && !error.value) {
113
123
  return;
114
124
  }
125
+ const kept = !force && lastFetchedKey !== key ? keptStored(key) : void 0;
126
+ if (kept) {
127
+ data.value = kept.result;
128
+ hasData.value = true;
129
+ error.value = null;
130
+ if (kept.age < STALE_MS) {
131
+ lastFetchedKey = key;
132
+ lastFetchedAt = Date.now() - kept.age;
133
+ return;
134
+ }
135
+ }
115
136
  void run(key);
116
137
  }
138
+ function keptStored(key) {
139
+ const stored = storeKey(key);
140
+ return stored ? keptAnswer(stored) : void 0;
141
+ }
117
142
  watch([debouncedKey, isEnabled], () => maybeFetch(false), { immediate: true });
118
143
  function refetch() {
119
144
  maybeFetch(true);
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The answers of `config:resolve` kept for the tab (IN-82), so a node's settings
3
+ * dialog that is closed and opened again shows what the platform last said
4
+ * instead of asking it again and waiting.
5
+ *
6
+ * A resolve can be expensive on the platform side — a SOAP Request reads and
7
+ * builds its whole WSDL for each one — and the dialog's own memory goes with the
8
+ * dialog. This store outlives it. It is a module-level map, so it lives exactly as
9
+ * long as the tab: a reload starts empty.
10
+ *
11
+ * Only answers are kept, never a refusal: a refusal is usually temporary (a
12
+ * bundle still building), and keeping it would show it again after it is gone.
13
+ *
14
+ * The key is built by the caller and has to name everything the answer depends
15
+ * on — tenant, node, version, target and the config the query is keyed by.
16
+ */
17
+ import type { NodeConfigResolveResult } from '../types/integrations.js';
18
+ /** How long an answer may still be shown while it is asked for again. */
19
+ export declare const KEEP_MS: number;
20
+ /** How many answers the tab keeps; the oldest goes first. */
21
+ export declare const MAX_ANSWERS = 50;
22
+ /** The kept answer and its age in ms, or `undefined` when there is none young enough to show. */
23
+ export declare function keptAnswer(key: string, now?: number): {
24
+ result: NodeConfigResolveResult;
25
+ age: number;
26
+ } | undefined;
27
+ export declare function keepAnswer(key: string, result: NodeConfigResolveResult, now?: number): void;
28
+ /** Forgets the answer kept under `key`: the platform refused it since, so it is not shown again. */
29
+ export declare function forgetAnswer(key: string): void;
30
+ /** Test hook: forget every answer. */
31
+ export declare function _clearKeptAnswers(): void;
@@ -0,0 +1,24 @@
1
+ export const KEEP_MS = 10 * 6e4;
2
+ export const MAX_ANSWERS = 50;
3
+ const answers = /* @__PURE__ */ new Map();
4
+ export function keptAnswer(key, now = Date.now()) {
5
+ const hit = answers.get(key);
6
+ if (!hit) return void 0;
7
+ const age = now - hit.at;
8
+ if (age >= KEEP_MS) {
9
+ answers.delete(key);
10
+ return void 0;
11
+ }
12
+ return { result: hit.result, age };
13
+ }
14
+ export function keepAnswer(key, result, now = Date.now()) {
15
+ answers.delete(key);
16
+ answers.set(key, { result, at: now });
17
+ while (answers.size > MAX_ANSWERS) answers.delete(answers.keys().next().value);
18
+ }
19
+ export function forgetAnswer(key) {
20
+ answers.delete(key);
21
+ }
22
+ export function _clearKeptAnswers() {
23
+ answers.clear();
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "3.6.2",
3
+ "version": "3.7.1",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {