@klodd/ds 5.2.7 → 5.4.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/SKILL.md
CHANGED
|
@@ -42,6 +42,65 @@ Denna Skill fångar både reglerna ("vad") och resonemanget ("varför").
|
|
|
42
42
|
- `references/04-locked-decisions/` — låsta beslut. Förklara, ändra inte.
|
|
43
43
|
- `references/05-open-decisions/` — öppna punkter. Presentera alternativen.
|
|
44
44
|
|
|
45
|
+
## Asynkrona operationer
|
|
46
|
+
|
|
47
|
+
Apps med polling-driven progress (AI-analys, bank-synk, batch-jobs)
|
|
48
|
+
delar `KloddDS.AsyncProgress` (5.3.0+).
|
|
49
|
+
|
|
50
|
+
**CSS-komponent**: `.async-progress` (i `css/components/async-progress.css`).
|
|
51
|
+
BEM-element: `__header`, `__bar-track`, `__bar`, `__hint`, `__label`,
|
|
52
|
+
`__pct`. Döljs via `[hidden]`-attributet i alla states utom under
|
|
53
|
+
pågående polling. Bar-fyll via `--async-progress-accent`-token
|
|
54
|
+
(default `--accent-9`) — app/scope kan override:a utan att forka.
|
|
55
|
+
|
|
56
|
+
**JS-klass**: `window.KloddDS.AsyncProgress` (i `js/async-progress.js`).
|
|
57
|
+
Konfigurerbar via konstruktor-objekt:
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const progress = new KloddDS.AsyncProgress({
|
|
61
|
+
progressEl: HTMLElement, // .async-progress-elementet
|
|
62
|
+
pollUrl: String, // GET-endpoint som returnerar JSON-kontrakt
|
|
63
|
+
pollInterval: 3000, // ms (default)
|
|
64
|
+
initialPct: 5,
|
|
65
|
+
initialLabel: "Startar...",
|
|
66
|
+
stageLabels: { // map: stage → string ELLER function(data, elapsedMs)
|
|
67
|
+
fetching: "Hämtar data...",
|
|
68
|
+
processing: (data, elapsedMs) =>
|
|
69
|
+
elapsedMs > 15000 ? "Slutför..." : "Bearbetar..."
|
|
70
|
+
},
|
|
71
|
+
simulation: { // valfri smooth-progress när backend pinnar pct
|
|
72
|
+
triggerStage: "processing",
|
|
73
|
+
startPct: 75, cap: 95,
|
|
74
|
+
ratePerSec: 0.25
|
|
75
|
+
},
|
|
76
|
+
transform: (data) => ({...}), // valfri: normalisera app-specifik payload
|
|
77
|
+
onComplete: (data) => {},
|
|
78
|
+
onError: (msg) => {},
|
|
79
|
+
maxErrors: 5,
|
|
80
|
+
doneLabel: "Klar"
|
|
81
|
+
});
|
|
82
|
+
progress.start(); // visar progressEl + startar polling
|
|
83
|
+
progress.stop(); // stoppar interval (terminal states gör det automatiskt)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Backend-kontrakt**: `pollUrl` ska returnera JSON med
|
|
87
|
+
`{ status, stage, pct, label }`. Status-domän:
|
|
88
|
+
|
|
89
|
+
- `pending` — inte startad
|
|
90
|
+
- `done` — klar (triggar onComplete)
|
|
91
|
+
- `failed` — fel (triggar onError efter maxErrors)
|
|
92
|
+
- Anything else (app-definierad) — pågående; app kontrollerar stage-labels
|
|
93
|
+
|
|
94
|
+
Apps vars endpoint redan returnerar exakt kontraktet behöver inte
|
|
95
|
+
`transform`-callbacken. Apps med prefix-payload (t.ex. Jubb's
|
|
96
|
+
`analysis_*`/`application_*`-fält i samma endpoint) använder
|
|
97
|
+
`transform` för att normalisera klient-sidigt — undviker fult
|
|
98
|
+
fetch-patch-mönster.
|
|
99
|
+
|
|
100
|
+
**Använd när**: en operation tar >2s och användaren ska se att den
|
|
101
|
+
händer. Apps idag: Jubb bolagsanalys + brevgenerering (5.3.0
|
|
102
|
+
migration). Ekonom bank-synk har endpoint, UI kommer i nästa sprint.
|
|
103
|
+
|
|
45
104
|
## Komponent-val
|
|
46
105
|
1. Kolla `references/02-components.md` för befintlig komponent
|
|
47
106
|
2. Föredra komposition över fork
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* ================================================================
|
|
2
|
+
components/async-progress.css
|
|
3
|
+
|
|
4
|
+
Generisk polling-driven progress-UI for asynkrona operationer.
|
|
5
|
+
Backend-kontrakt: { status, stage, pct, label }.
|
|
6
|
+
|
|
7
|
+
Anvands av:
|
|
8
|
+
- Jubb /jobb/{id} (bolagsanalys + brevgenerering)
|
|
9
|
+
- Ekonom bank-synk (planerad)
|
|
10
|
+
- Framtida appar med asynkrona operationer
|
|
11
|
+
|
|
12
|
+
Ersatter .brief-progress (Jubb-specifik) som lyftes till
|
|
13
|
+
generaliserad .async-progress under 5.3.0-sprinten.
|
|
14
|
+
|
|
15
|
+
Samma visuella formspraket som .pipeline-progress (6px-bar,
|
|
16
|
+
0.6s ease-in-out width-transition, sunken-track) men sibling-
|
|
17
|
+
block i stallet for shared - tydligare BEM-namn per use-case.
|
|
18
|
+
|
|
19
|
+
Blocks:
|
|
20
|
+
.async-progress - root, dolj med [hidden] vid pending/done/failed
|
|
21
|
+
|
|
22
|
+
BEM-element:
|
|
23
|
+
__header - flex-row: label vänster, pct hoger
|
|
24
|
+
__bar-track - bakgrund-track 6px hojd
|
|
25
|
+
__bar - accent-fyllning, width transition
|
|
26
|
+
__hint - hint-text under bar (valfri)
|
|
27
|
+
__label - inline label-element
|
|
28
|
+
__pct - inline pct-element
|
|
29
|
+
|
|
30
|
+
Tokens: --surface-sunken, --async-progress-accent (default
|
|
31
|
+
--accent-9), --text-default, --text-muted, --space-6/-8/-12/-16,
|
|
32
|
+
--radius-full, --fs-12/-13.
|
|
33
|
+
|
|
34
|
+
--async-progress-accent-token tillater app/scope-override av
|
|
35
|
+
bar-fillfargen utan att forka komponenten. Default = --accent-9
|
|
36
|
+
(app:s accent via data-app-theming).
|
|
37
|
+
================================================================ */
|
|
38
|
+
|
|
39
|
+
.async-progress {
|
|
40
|
+
margin: var(--space-12) 0 var(--space-16);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.async-progress__header {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
align-items: baseline;
|
|
47
|
+
margin-bottom: var(--space-8);
|
|
48
|
+
font-size: var(--fs-13);
|
|
49
|
+
color: var(--text-default);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.async-progress__bar-track {
|
|
53
|
+
height: 6px;
|
|
54
|
+
background: var(--surface-sunken);
|
|
55
|
+
border-radius: var(--radius-full);
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.async-progress__bar {
|
|
60
|
+
height: 100%;
|
|
61
|
+
background: var(--async-progress-accent, var(--accent-9));
|
|
62
|
+
border-radius: inherit;
|
|
63
|
+
transition: width 0.6s ease-in-out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.async-progress__hint {
|
|
67
|
+
margin: var(--space-6) 0 0;
|
|
68
|
+
font-size: var(--fs-12);
|
|
69
|
+
color: var(--text-muted);
|
|
70
|
+
}
|
|
@@ -1,37 +1,28 @@
|
|
|
1
1
|
/* ================================================================
|
|
2
2
|
components/progress-bar.css
|
|
3
3
|
|
|
4
|
-
Två sibling-blocks for polling-driven progress-UI:
|
|
5
|
-
|
|
6
4
|
.pipeline-progress - overst pa /korningar nar DigestRun pagar
|
|
7
|
-
(cron-morning/cron-alert/cron-signals)
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
(cron-morning/cron-alert/cron-signals i Jubb)
|
|
6
|
+
|
|
7
|
+
Visuellt formspraket: 6px-bar, 0.6s ease-in-out width-transition,
|
|
8
|
+
accent-9-fill, sunken-track.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
modifiers, men kept separata for tydligare BEM-block per use-case.
|
|
10
|
+
Sibling-block .async-progress lever i async-progress.css for
|
|
11
|
+
generiska asynkrona operationer (Jubb bolagsanalys, Ekonom bank-
|
|
12
|
+
synk, framtida appar). Bagge delar visuellt språk men har egna
|
|
13
|
+
block-namn for tydligare BEM-blockning per use-case.
|
|
16
14
|
|
|
17
15
|
Blocks:
|
|
18
16
|
.pipeline-progress - root, /korningar
|
|
19
|
-
.brief-progress - root, /jobb/{id}
|
|
20
17
|
|
|
21
|
-
BEM-element
|
|
18
|
+
BEM-element:
|
|
22
19
|
__header - flex-row: label vänster, pct hoger
|
|
23
20
|
__bar-track - bakgrund-track 6px hojd
|
|
24
21
|
__bar - accent-fyllning, width transition
|
|
25
|
-
__sub - sub-text under bar
|
|
26
|
-
__hint - hint-text under bar (.brief-progress)
|
|
22
|
+
__sub - sub-text under bar
|
|
27
23
|
__label - inline label-element (anvandbart pa /design)
|
|
28
24
|
__pct - inline pct-element (anvandbart pa /design)
|
|
29
25
|
|
|
30
|
-
Lyftad fran jubb.css 2026-05-18 till komponent-mappen sa /design
|
|
31
|
-
kan ladda bara denna fil utan att dra in hela Jubb-domain-CSS:n
|
|
32
|
-
(per Calles brief 2026-05-07: ingenting i styleguiden far paverka
|
|
33
|
-
appens overiga CSS).
|
|
34
|
-
|
|
35
26
|
Tokens: --surface-sunken, --accent-9, --text-default, --text-muted,
|
|
36
27
|
--space-6/-8/-12/-16, --radius-full, --fs-12/-13.
|
|
37
28
|
================================================================ */
|
|
@@ -72,39 +63,3 @@
|
|
|
72
63
|
font-size: var(--fs-12);
|
|
73
64
|
color: var(--text-muted);
|
|
74
65
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/* ─── .brief-progress (/jobb/{id}) ─────────────────────────────── */
|
|
78
|
-
|
|
79
|
-
.brief-progress {
|
|
80
|
-
margin: var(--space-12) 0 var(--space-16);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.brief-progress__header {
|
|
84
|
-
display: flex;
|
|
85
|
-
justify-content: space-between;
|
|
86
|
-
align-items: baseline;
|
|
87
|
-
margin-bottom: var(--space-8);
|
|
88
|
-
font-size: var(--fs-13);
|
|
89
|
-
color: var(--text-default);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.brief-progress__bar-track {
|
|
93
|
-
height: 6px;
|
|
94
|
-
background: var(--surface-sunken);
|
|
95
|
-
border-radius: var(--radius-full);
|
|
96
|
-
overflow: hidden;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.brief-progress__bar {
|
|
100
|
-
height: 100%;
|
|
101
|
-
background: var(--accent-9);
|
|
102
|
-
border-radius: inherit;
|
|
103
|
-
transition: width 0.6s ease-in-out;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.brief-progress__hint {
|
|
107
|
-
margin: var(--space-6) 0 0;
|
|
108
|
-
font-size: var(--fs-12);
|
|
109
|
-
color: var(--text-muted);
|
|
110
|
-
}
|
package/css/index.css
CHANGED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/* @klodd/ds - AsyncProgress
|
|
2
|
+
*
|
|
3
|
+
* Generisk polling-driven progress-controller for asynkrona
|
|
4
|
+
* operationer. Backend-kontrakt: GET pollUrl → JSON
|
|
5
|
+
* { status, stage, pct, label }.
|
|
6
|
+
*
|
|
7
|
+
* Status-domän:
|
|
8
|
+
* pending - inte startat
|
|
9
|
+
* <ev. app> - app-specifika status under generating
|
|
10
|
+
* done - klart (triggar onComplete)
|
|
11
|
+
* failed - fel (triggar onError efter maxErrors)
|
|
12
|
+
*
|
|
13
|
+
* Lyftad fran Jubb's brief-progress.js (sprint 5.3.0). Vet inte
|
|
14
|
+
* om AI, Claude, banker eller specifika apps - bara om kontraktet.
|
|
15
|
+
*
|
|
16
|
+
* Anvandning:
|
|
17
|
+
*
|
|
18
|
+
* const progress = new KloddDS.AsyncProgress({
|
|
19
|
+
* progressEl: document.getElementById("my-progress"),
|
|
20
|
+
* pollUrl: "/api/foo/123/progress",
|
|
21
|
+
* stageLabels: {
|
|
22
|
+
* step_a: "Forsta steget...",
|
|
23
|
+
* step_b: (data) => data.pct < 50 ? "Forsta..." : "Slutfor...",
|
|
24
|
+
* },
|
|
25
|
+
* simulation: { triggerStage: "step_b", startPct: 75, cap: 95, ratePerSec: 0.25 },
|
|
26
|
+
* onComplete: () => openModal(),
|
|
27
|
+
* onError: (msg) => showToast(msg, "error"),
|
|
28
|
+
* });
|
|
29
|
+
* progress.start();
|
|
30
|
+
*
|
|
31
|
+
* Krav pa progressEl (BEM .async-progress):
|
|
32
|
+
* - id="..." (en barn-element querySelector via id)
|
|
33
|
+
* - innehåller __label, __pct och __bar med data-element-attribut
|
|
34
|
+
* ELLER querySelectable via .async-progress__label etc.
|
|
35
|
+
* - kan ha [hidden] satt - start() tar bort attributet
|
|
36
|
+
*/
|
|
37
|
+
(function (root) {
|
|
38
|
+
"use strict";
|
|
39
|
+
|
|
40
|
+
const DEFAULTS = {
|
|
41
|
+
pollInterval: 3000,
|
|
42
|
+
initialPct: 5,
|
|
43
|
+
initialLabel: "Startar...",
|
|
44
|
+
stageLabels: {},
|
|
45
|
+
simulation: null,
|
|
46
|
+
onComplete: null,
|
|
47
|
+
onError: null,
|
|
48
|
+
maxErrors: 5,
|
|
49
|
+
doneLabel: "Klar",
|
|
50
|
+
// transform(data) -> { status, stage, pct, label, ...passthrough }.
|
|
51
|
+
// Anvands nar backend-payload har app-specifika faltnamn (prefix
|
|
52
|
+
// som analysis_*/application_*) och behover normaliseras till
|
|
53
|
+
// kontraktet. Default = identity.
|
|
54
|
+
transform: null,
|
|
55
|
+
// fetchFn() -> Promise<{ status, stage, pct, label, ... }>.
|
|
56
|
+
// Overskriver intern fetch(pollUrl). Anvands av demo-sidor (mock-
|
|
57
|
+
// poller utan riktigt endpoint) och tester (deterministisk
|
|
58
|
+
// injection utan global fetch-patch). Per ADR 0016. Default
|
|
59
|
+
// = null -> intern fetch.
|
|
60
|
+
fetchFn: null,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
class AsyncProgress {
|
|
64
|
+
constructor(config) {
|
|
65
|
+
if (!config || !config.progressEl) {
|
|
66
|
+
throw new Error("AsyncProgress: progressEl ar obligatorisk");
|
|
67
|
+
}
|
|
68
|
+
if (!config.pollUrl && !config.fetchFn) {
|
|
69
|
+
throw new Error("AsyncProgress: pollUrl eller fetchFn maste anges");
|
|
70
|
+
}
|
|
71
|
+
this.cfg = Object.assign({}, DEFAULTS, config);
|
|
72
|
+
this.progressEl = this.cfg.progressEl;
|
|
73
|
+
this.barEl = this.progressEl.querySelector(".async-progress__bar");
|
|
74
|
+
this.labelEl = this.progressEl.querySelector(".async-progress__label");
|
|
75
|
+
this.pctEl = this.progressEl.querySelector(".async-progress__pct");
|
|
76
|
+
|
|
77
|
+
this.pollTimer = null;
|
|
78
|
+
this.consecutiveErrors = 0;
|
|
79
|
+
this.simulationStart = null;
|
|
80
|
+
this.lastStage = null;
|
|
81
|
+
this.completed = false;
|
|
82
|
+
this.failed = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
start() {
|
|
86
|
+
this.progressEl.removeAttribute("hidden");
|
|
87
|
+
this._setBar(this.cfg.initialPct, this.cfg.initialLabel);
|
|
88
|
+
this.simulationStart = null;
|
|
89
|
+
this.lastStage = null;
|
|
90
|
+
this.completed = false;
|
|
91
|
+
this.failed = false;
|
|
92
|
+
this.consecutiveErrors = 0;
|
|
93
|
+
if (this.pollTimer !== null) return;
|
|
94
|
+
// Forsta poll efter 100ms sa ev. fetch-patches (auth-tokens etc)
|
|
95
|
+
// hinner appliceras.
|
|
96
|
+
setTimeout(() => this._poll(), 100);
|
|
97
|
+
this.pollTimer = setInterval(() => this._poll(), this.cfg.pollInterval);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
stop() {
|
|
101
|
+
if (this.pollTimer !== null) {
|
|
102
|
+
clearInterval(this.pollTimer);
|
|
103
|
+
this.pollTimer = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_setBar(pct, label) {
|
|
108
|
+
const clampedPct = Math.max(0, Math.min(100, Math.round(pct)));
|
|
109
|
+
if (this.barEl) this.barEl.style.width = clampedPct + "%";
|
|
110
|
+
if (this.pctEl) this.pctEl.textContent = clampedPct + "%";
|
|
111
|
+
if (this.labelEl && label) this.labelEl.textContent = label;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_getLabel(data, elapsedSim) {
|
|
115
|
+
if (data.status === "done") return this.cfg.doneLabel;
|
|
116
|
+
const stageEntry = this.cfg.stageLabels[data.stage];
|
|
117
|
+
if (typeof stageEntry === "function") {
|
|
118
|
+
return stageEntry(data, elapsedSim);
|
|
119
|
+
}
|
|
120
|
+
if (typeof stageEntry === "string") {
|
|
121
|
+
return stageEntry;
|
|
122
|
+
}
|
|
123
|
+
// Fallback: backend's label
|
|
124
|
+
return data.label || this.cfg.initialLabel;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_applySimulation(realPct, data) {
|
|
128
|
+
const sim = this.cfg.simulation;
|
|
129
|
+
if (!sim || data.stage !== sim.triggerStage) return realPct;
|
|
130
|
+
if (this.simulationStart == null) return realPct;
|
|
131
|
+
const elapsedSec = (Date.now() - this.simulationStart) / 1000;
|
|
132
|
+
const simulated = sim.startPct + elapsedSec * sim.ratePerSec;
|
|
133
|
+
const capped = Math.min(simulated, sim.cap);
|
|
134
|
+
return Math.max(realPct, capped);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async _poll() {
|
|
138
|
+
try {
|
|
139
|
+
// fetchFn overskriver intern fetch (ADR 0016). Konsumenten
|
|
140
|
+
// ansvarar for HTTP-errors -> rejicera promise. Intern fetch
|
|
141
|
+
// mappar !res.ok till _handleFetchError som vanligt.
|
|
142
|
+
let data;
|
|
143
|
+
if (typeof this.cfg.fetchFn === "function") {
|
|
144
|
+
data = await this.cfg.fetchFn();
|
|
145
|
+
} else {
|
|
146
|
+
const res = await fetch(this.cfg.pollUrl);
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
this._handleFetchError(`HTTP ${res.status}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
data = await res.json();
|
|
152
|
+
}
|
|
153
|
+
if (typeof this.cfg.transform === "function") {
|
|
154
|
+
data = this.cfg.transform(data);
|
|
155
|
+
}
|
|
156
|
+
this.consecutiveErrors = 0;
|
|
157
|
+
|
|
158
|
+
// Stage-byte: trigga simulation-timer
|
|
159
|
+
const sim = this.cfg.simulation;
|
|
160
|
+
if (sim) {
|
|
161
|
+
if (data.stage === sim.triggerStage && this.lastStage !== sim.triggerStage) {
|
|
162
|
+
this.simulationStart = Date.now();
|
|
163
|
+
} else if (data.stage !== sim.triggerStage) {
|
|
164
|
+
this.simulationStart = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.lastStage = data.stage;
|
|
168
|
+
|
|
169
|
+
// Terminal states
|
|
170
|
+
if (data.status === "failed") {
|
|
171
|
+
if (this.failed) return;
|
|
172
|
+
this.failed = true;
|
|
173
|
+
this.stop();
|
|
174
|
+
this.progressEl.setAttribute("hidden", "");
|
|
175
|
+
if (typeof this.cfg.onError === "function") {
|
|
176
|
+
this.cfg.onError(data.label || "Operationen misslyckades.");
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (data.status === "done") {
|
|
182
|
+
if (this.completed) return;
|
|
183
|
+
this.completed = true;
|
|
184
|
+
this._setBar(100, this.cfg.doneLabel);
|
|
185
|
+
this.stop();
|
|
186
|
+
if (typeof this.cfg.onComplete === "function") {
|
|
187
|
+
this.cfg.onComplete(data);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Pågående: render pct + label
|
|
193
|
+
const realPct = data.pct || 0;
|
|
194
|
+
const elapsedSim = this.simulationStart
|
|
195
|
+
? Date.now() - this.simulationStart
|
|
196
|
+
: null;
|
|
197
|
+
const effectivePct = this._applySimulation(realPct, data);
|
|
198
|
+
const label = this._getLabel(data, elapsedSim);
|
|
199
|
+
this._setBar(effectivePct, label);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this._handleFetchError(err.message || String(err));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_handleFetchError(msg) {
|
|
206
|
+
this.consecutiveErrors += 1;
|
|
207
|
+
if (this.consecutiveErrors >= this.cfg.maxErrors) {
|
|
208
|
+
if (this.failed) return;
|
|
209
|
+
this.failed = true;
|
|
210
|
+
this.stop();
|
|
211
|
+
this.progressEl.setAttribute("hidden", "");
|
|
212
|
+
if (typeof this.cfg.onError === "function") {
|
|
213
|
+
this.cfg.onError(`Polling-fel efter ${this.cfg.maxErrors} forsok: ${msg}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Exponera pa KloddDS-namespace. Skapa objektet om det inte finns
|
|
220
|
+
// (defensiv mot oklar laddningsordning - om async-progress.js laddas
|
|
221
|
+
// fore index.js).
|
|
222
|
+
root.KloddDS = root.KloddDS || {};
|
|
223
|
+
root.KloddDS.AsyncProgress = AsyncProgress;
|
|
224
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# 0016 - AsyncProgress fetchFn-option
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Locked (2026-05-19).
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`KloddDS.AsyncProgress` (introducerad 5.3.0) använder intern `fetch()`
|
|
10
|
+
mot `pollUrl` för att hämta progress-payload. Konstruktor-validering
|
|
11
|
+
kastar fel om `pollUrl` saknas. Det fungerar för app-konsumenter
|
|
12
|
+
(Jubb's bolagsanalys, Ekonom's bank-synk) men blockerar två scenarier:
|
|
13
|
+
|
|
14
|
+
1. **Demo-sidor (`/design` i Jubb)**: ingen riktig polling-endpoint
|
|
15
|
+
existerar. För att demonstrera komponenten visuellt har vi tidigare
|
|
16
|
+
tvingats köra parallell setTimeout-driven simulering vid sidan av
|
|
17
|
+
AsyncProgress-klassen - dupplicerar logik och visar inte hur
|
|
18
|
+
konsumenten faktiskt använder klassen.
|
|
19
|
+
|
|
20
|
+
2. **Enhetstester**: tester måste idag mocka `window.fetch` globalt
|
|
21
|
+
eller köra ett riktigt test-endpoint för att verifiera
|
|
22
|
+
stage-byten, simulation-curve, done/failed-callbacks etc.
|
|
23
|
+
fetch-monkey-patch är fult och påverkar andra tester i samma
|
|
24
|
+
suite.
|
|
25
|
+
|
|
26
|
+
Båda problemen löser sig genom en opt-in `fetchFn`-option som
|
|
27
|
+
överskuggar den interna fetch-mekanismen.
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
Lägg till `fetchFn`-option på AsyncProgress-konstruktor:
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
new KloddDS.AsyncProgress({
|
|
35
|
+
// Ny option:
|
|
36
|
+
fetchFn: null, // default: använd intern fetch(pollUrl)
|
|
37
|
+
|
|
38
|
+
// Existerande:
|
|
39
|
+
progressEl, pollUrl, pollInterval, ...
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Signatur**: `fetchFn: () => Promise<PollPayload>` där
|
|
44
|
+
`PollPayload = { status, stage, pct, label, ...passthrough }`
|
|
45
|
+
(samma kontrakt som backend-endpoint).
|
|
46
|
+
|
|
47
|
+
**Använding i poll-loopen**:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const data = this._options.fetchFn
|
|
51
|
+
? await this._options.fetchFn()
|
|
52
|
+
: await fetch(this._options.pollUrl).then(r => r.json());
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Validering**: konstruktor kastar fel om **både** `pollUrl` och
|
|
56
|
+
`fetchFn` saknas. Tidigare räckte det med pollUrl - nu accepteras
|
|
57
|
+
endera.
|
|
58
|
+
|
|
59
|
+
## Consequences
|
|
60
|
+
|
|
61
|
+
**Positiva**:
|
|
62
|
+
- Design-sidan kan använda den faktiska AsyncProgress-komponenten
|
|
63
|
+
med mock-poller istället för parallell setTimeout-simulering. En
|
|
64
|
+
källa till sanning för komponentens beteende.
|
|
65
|
+
- Enhetstester kan injicera deterministisk mock-poller utan att
|
|
66
|
+
monkey-patcha global `fetch`.
|
|
67
|
+
- API-tillägget är opt-in: existerande konsumenter (Jubb's
|
|
68
|
+
brief-progress.js wrapper, framtida Ekonom bank-synk UI) påverkas
|
|
69
|
+
inte - de fortsätter använda `pollUrl` och får default-beteende.
|
|
70
|
+
|
|
71
|
+
**Negativa**:
|
|
72
|
+
- Publik API-utökning kräver minor-bump (5.3.0 → 5.4.0).
|
|
73
|
+
- Två kodvägar i poll-loopen (intern fetch vs fetchFn) - liten
|
|
74
|
+
komplexitetsökning. Defensiv if-branch.
|
|
75
|
+
|
|
76
|
+
## Implementation notes
|
|
77
|
+
|
|
78
|
+
- `transform`-callbacken (introducerad 5.3.0) appliceras på resultatet
|
|
79
|
+
oavsett om data kommer från fetchFn eller intern fetch. Konsekvens:
|
|
80
|
+
apps kan kombinera fetchFn (mock) + transform (normalize app-payload)
|
|
81
|
+
i samma instans.
|
|
82
|
+
- Vid HTTP-fel via `fetchFn` ansvarar konsumenten för att kasta
|
|
83
|
+
Error/rejicera promise. AsyncProgress räknar konsekutiva
|
|
84
|
+
fel mot `maxErrors` som vanligt.
|
|
85
|
+
|
|
86
|
+
## Användning
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
function createMockPoller(steps) {
|
|
90
|
+
let i = 0;
|
|
91
|
+
return () => Promise.resolve(
|
|
92
|
+
i < steps.length
|
|
93
|
+
? steps[i++]
|
|
94
|
+
: { status: "done", stage: null, pct: 100, label: "Klar" }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
new KloddDS.AsyncProgress({
|
|
99
|
+
progressEl: document.getElementById("demo-progress"),
|
|
100
|
+
fetchFn: createMockPoller([
|
|
101
|
+
{ status: "generating", stage: null, pct: 25, label: "Steg 1..." },
|
|
102
|
+
{ status: "generating", stage: null, pct: 50, label: "Steg 2..." },
|
|
103
|
+
{ status: "generating", stage: null, pct: 75, label: "Steg 3..." },
|
|
104
|
+
{ status: "done", stage: null, pct: 100, label: "Klar" },
|
|
105
|
+
]),
|
|
106
|
+
pollInterval: 1500,
|
|
107
|
+
onComplete: () => console.log("done!"),
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Skapad ihop med deploy 5.4.0 (2026-05-19). Diskussion i sprint
|
|
112
|
+
"async-progress-fetch-fn-deploy".
|