@supersoniks/concorde 4.9.7 → 4.9.91

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.
Files changed (72) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +265 -265
  3. package/concorde-core.es.js +4600 -4385
  4. package/dist/concorde-core.bundle.js +265 -265
  5. package/dist/concorde-core.es.js +4600 -4385
  6. package/dist/js/core/components/functional/queue/queue.js +25 -153
  7. package/dist/js/core/components/functional/queue/queue.js.map +1 -1
  8. package/dist/js/core/components/ui/_css/scroll.js +2 -0
  9. package/dist/js/core/components/ui/_css/scroll.js.map +1 -1
  10. package/dist/js/core/decorators/api.js +92 -2
  11. package/dist/js/core/decorators/api.js.map +1 -1
  12. package/dist/js/core/utils/SafariScrollGuard.js +384 -0
  13. package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
  14. package/dist/js/decorators.js +4 -0
  15. package/dist/js/decorators.js.map +1 -1
  16. package/dist/js/utils.js +3 -0
  17. package/dist/js/utils.js.map +1 -1
  18. package/dist/types/core/components/functional/queue/queue.d.ts +8 -24
  19. package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
  20. package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
  21. package/dist/types/core/decorators/api.d.ts +82 -60
  22. package/dist/types/core/decorators/api.d.ts.map +1 -1
  23. package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
  24. package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
  25. package/dist/types/decorators.d.ts +3 -1
  26. package/dist/types/decorators.d.ts.map +1 -1
  27. package/dist/types/utils.d.ts +1 -0
  28. package/dist/types/utils.d.ts.map +1 -1
  29. package/docs/assets/index-BL6WVF7g.css +1 -0
  30. package/docs/assets/{index-hElUgV9_.js → index-DVn8AeyA.js} +572 -555
  31. package/docs/crawl/docs/_decorators/get.html +64 -81
  32. package/docs/crawl/docs/_decorators/patch.html +35 -23
  33. package/docs/crawl/docs/_decorators/post.html +72 -72
  34. package/docs/crawl/docs/_decorators/put.html +36 -21
  35. package/docs/crawl/docs/_getting-started/ai-agents.html +45 -6
  36. package/docs/crawl/hubs/decorators.html +141 -69
  37. package/docs/crawl/hubs/remote-data.html +141 -69
  38. package/docs/index.html +2 -2
  39. package/docs/llms-full.txt +203 -149
  40. package/docs/llms.txt +4 -4
  41. package/docs/src/core/components/functional/queue/queue.md +0 -0
  42. package/docs/src/core/components/ui/button/button.md +0 -0
  43. package/docs/src/docs/_decorators/get.md +55 -81
  44. package/docs/src/docs/_decorators/patch.md +36 -25
  45. package/docs/src/docs/_decorators/post.md +71 -62
  46. package/docs/src/docs/_decorators/put.md +37 -24
  47. package/docs/src/docs/_getting-started/ai-agents.md +24 -4
  48. package/docs/src/docs/search/docs-search.json +151 -1
  49. package/docs/src/tsconfig.json +3 -0
  50. package/package.json +6 -2
  51. package/src/core/components/functional/queue/queue.ts +33 -164
  52. package/src/core/components/ui/_css/scroll.ts +3 -0
  53. package/src/core/decorators/api.post.spec.ts +79 -1
  54. package/src/core/decorators/api.spec.ts +100 -1
  55. package/src/core/decorators/api.ts +252 -138
  56. package/src/core/utils/SafariScrollGuard.ts +429 -0
  57. package/src/decorators.ts +7 -0
  58. package/src/docs/_decorators/get.md +81 -80
  59. package/src/docs/_decorators/patch.md +36 -25
  60. package/src/docs/_decorators/post.md +99 -62
  61. package/src/docs/_decorators/put.md +37 -24
  62. package/src/docs/components/docs-demo-sources.ts +1 -6
  63. package/src/docs/example/decorators-demo-geo.ts +0 -21
  64. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +41 -132
  65. package/src/docs/search/docs-search.json +156 -241
  66. package/src/docs/tailwind/css/tailwind.css +0 -0
  67. package/src/tsconfig.json +3 -0
  68. package/src/tsconfig.tsbuildinfo +1 -1
  69. package/src/utils.ts +13 -0
  70. package/vite.config.mts +4 -0
  71. package/vitest.setup.ts +22 -0
  72. package/docs/assets/index-Bnp7gVaA.css +0 -1
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Garde Safari / WebKit : empêche les resets fantômes de scroll.
3
+ *
4
+ * - stock = dernière position user (intent + settle inertie)
5
+ * - jump brutal vers ~0 → JAMAIS capturé (même mid-gesture), restore
6
+ * - collapse Lit / slot light-DOM : observe aussi le host + sticky restore multi-frames
7
+ * - nested : intent uniquement sur le scroller le plus proche (composedPath)
8
+ */
9
+
10
+ export type ScrollRoot = HTMLElement | Window;
11
+
12
+ const SCROLL_SETTLE_MS = 150;
13
+ const RESTORE_EPS = 10;
14
+ /** Jump vers le haut considéré fantôme (pas une remontée progressive trackpad). */
15
+ const JUMP_EPS = 200;
16
+ /** Combien de frames on re-applique le stock après un fantôme / mutation. */
17
+ const STICKY_RESTORE_FRAMES = 16;
18
+
19
+ type RootState = {
20
+ x: number;
21
+ y: number;
22
+ restoring: boolean;
23
+ watching: boolean;
24
+ userEngaged: boolean;
25
+ settleTimer?: ReturnType<typeof setTimeout>;
26
+ raf: number;
27
+ stickyFrames: number;
28
+ resizeObserver?: ResizeObserver;
29
+ mutationObserver?: MutationObserver;
30
+ };
31
+
32
+ const states = new WeakMap<object, RootState>();
33
+ let installed = false;
34
+
35
+ export function isSafariScrollEngine(): boolean {
36
+ if (typeof navigator === "undefined") return false;
37
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
38
+ }
39
+
40
+ function read(root: ScrollRoot): { x: number; y: number } {
41
+ if (root === window) {
42
+ return { x: window.scrollX, y: window.scrollY };
43
+ }
44
+ const el = root as HTMLElement;
45
+ return { x: el.scrollLeft, y: el.scrollTop };
46
+ }
47
+
48
+ function write(root: ScrollRoot, x: number, y: number): void {
49
+ if (root === window) {
50
+ window.scrollTo(x, y);
51
+ return;
52
+ }
53
+ const el = root as HTMLElement;
54
+ // scrollTo coupe mieux l’inertie iOS que le setter seul
55
+ if (typeof el.scrollTo === "function") {
56
+ el.scrollTo(x, y);
57
+ } else {
58
+ el.scrollLeft = x;
59
+ el.scrollTop = y;
60
+ }
61
+ }
62
+
63
+ function maxScrollY(root: ScrollRoot): number {
64
+ if (root === window) {
65
+ const doc = document.documentElement;
66
+ return Math.max(0, doc.scrollHeight - window.innerHeight);
67
+ }
68
+ const el = root as HTMLElement;
69
+ return Math.max(0, el.scrollHeight - el.clientHeight);
70
+ }
71
+
72
+ function getState(root: ScrollRoot): RootState {
73
+ let s = states.get(root);
74
+ if (!s) {
75
+ const pos = read(root);
76
+ s = {
77
+ x: pos.x,
78
+ y: pos.y,
79
+ restoring: false,
80
+ watching: false,
81
+ userEngaged: false,
82
+ raf: 0,
83
+ stickyFrames: 0,
84
+ };
85
+ states.set(root, s);
86
+ }
87
+ return s;
88
+ }
89
+
90
+ function markUserEngaged(root: ScrollRoot): void {
91
+ getState(root).userEngaged = true;
92
+ }
93
+
94
+ function armScrollSettle(root: ScrollRoot): void {
95
+ const s = getState(root);
96
+ clearTimeout(s.settleTimer);
97
+ s.settleTimer = setTimeout(() => {
98
+ s.userEngaged = false;
99
+ s.settleTimer = undefined;
100
+ }, SCROLL_SETTLE_MS);
101
+ }
102
+
103
+ /**
104
+ * Saut brutal vers le haut (pas seulement ~0) : Safari clampe souvent à une
105
+ * valeur intermédiaire (ex. 1716→225). Un `cap` sans `rst` = on avait accepté
106
+ * ce drop comme scroll user.
107
+ */
108
+ function looksLikePhantomDrop(currentY: number, storedY: number): boolean {
109
+ return storedY - currentY >= JUMP_EPS;
110
+ }
111
+
112
+ /**
113
+ * Mémorise la position courante — refuse tout gros drop d’un coup.
114
+ */
115
+ export function captureScrollRoot(root: ScrollRoot): void {
116
+ if (!isSafariScrollEngine()) return;
117
+ const s = getState(root);
118
+ if (s.restoring) return;
119
+ const pos = read(root);
120
+ if (looksLikePhantomDrop(pos.y, s.y)) {
121
+ stickyRestore(root);
122
+ return;
123
+ }
124
+ s.x = pos.x;
125
+ s.y = pos.y;
126
+ }
127
+
128
+ /**
129
+ * Ré-applique le stock. Ne pollue jamais le stock avec le scroll courant.
130
+ *
131
+ * Important : on peut clamper la *cible d’écriture* à maxScroll (le navigateur
132
+ * le ferait de toute façon), mais on ne doit JAMAIS abaisser `s.y` pour autant —
133
+ * pendant un re-render Lit maxScroll plonge souvent un instant (ex. 215→75) puis
134
+ * revient ; baisser le stock là, c’est perdre la position pour de bon.
135
+ */
136
+ export function restoreScrollRoot(root: ScrollRoot): void {
137
+ if (!isSafariScrollEngine()) return;
138
+ const s = getState(root);
139
+ const maxY = maxScrollY(root);
140
+ const targetX = s.x;
141
+ // maxY===0 : collapse total transitoire → tenter quand même s.y (no-op clamp)
142
+ const targetY = maxY > 0 ? Math.min(s.y, maxY) : s.y;
143
+
144
+ const { x, y } = read(root);
145
+ if (
146
+ Math.abs(targetX - x) <= RESTORE_EPS &&
147
+ Math.abs(targetY - y) <= RESTORE_EPS
148
+ ) {
149
+ return;
150
+ }
151
+
152
+ s.restoring = true;
153
+ try {
154
+ write(root, targetX, targetY);
155
+ } finally {
156
+ requestAnimationFrame(() => {
157
+ s.restoring = false;
158
+ });
159
+ }
160
+ }
161
+
162
+ /** @deprecated alias */
163
+ export function syncScrollRoot(root: ScrollRoot): void {
164
+ restoreScrollRoot(root);
165
+ }
166
+
167
+ export function resetScrollRoot(
168
+ root: ScrollRoot,
169
+ position: { x?: number; y?: number } | "current" = { x: 0, y: 0 },
170
+ ): void {
171
+ if (!isSafariScrollEngine()) return;
172
+ const s = getState(root);
173
+ if (position === "current") {
174
+ const pos = read(root);
175
+ s.x = pos.x;
176
+ s.y = pos.y;
177
+ return;
178
+ }
179
+ s.x = position.x ?? 0;
180
+ s.y = position.y ?? 0;
181
+ }
182
+
183
+ function stickyRestore(root: ScrollRoot): void {
184
+ const s = getState(root);
185
+ s.stickyFrames = STICKY_RESTORE_FRAMES;
186
+ if (s.raf) return;
187
+ const tick = () => {
188
+ s.raf = 0;
189
+ restoreScrollRoot(root);
190
+ if (s.stickyFrames > 0) {
191
+ s.stickyFrames--;
192
+ s.raf = requestAnimationFrame(tick);
193
+ }
194
+ };
195
+ s.raf = requestAnimationFrame(tick);
196
+ }
197
+
198
+ function scheduleRestore(root: ScrollRoot): void {
199
+ stickyRestore(root);
200
+ }
201
+
202
+ /**
203
+ * Branche écoute intent + scroll + resize (+ mutations).
204
+ * Idempotent.
205
+ */
206
+ export function watchScrollRoot(root: ScrollRoot): void {
207
+ if (!isSafariScrollEngine()) return;
208
+ const s = getState(root);
209
+ if (s.watching) return;
210
+ s.watching = true;
211
+
212
+ const target: EventTarget = root === window ? window : root;
213
+
214
+ const onScroll = () => {
215
+ if (s.restoring) return;
216
+ const { y } = read(root);
217
+
218
+ // Gros drop d’un coup (y compris vers une valeur intermédiaire) → restore
219
+ if (looksLikePhantomDrop(y, s.y)) {
220
+ stickyRestore(root);
221
+ return;
222
+ }
223
+
224
+ if (s.userEngaged) {
225
+ captureScrollRoot(root);
226
+ armScrollSettle(root);
227
+ return;
228
+ }
229
+
230
+ // Pas d’intent taggé : adopter la position courante (pas un gros drop).
231
+ captureScrollRoot(root);
232
+ };
233
+
234
+ const isGestureForThisRoot = (e: Event) =>
235
+ nearestScrollRootFromEvent(e) === root;
236
+
237
+ const onUserIntent = (e: Event) => {
238
+ if (!isGestureForThisRoot(e)) return;
239
+ // Snapshot immédiat avant mutation DOM déclenchée par le clic.
240
+ captureScrollRoot(root);
241
+ markUserEngaged(root);
242
+ };
243
+
244
+ const onUserIntentEnd = (e: Event) => {
245
+ if (!isGestureForThisRoot(e)) return;
246
+ armScrollSettle(root);
247
+ };
248
+
249
+ const onWheel = (e: Event) => {
250
+ if (!isGestureForThisRoot(e)) return;
251
+ captureScrollRoot(root);
252
+ markUserEngaged(root);
253
+ armScrollSettle(root);
254
+ };
255
+
256
+ target.addEventListener("scroll", onScroll, { passive: true });
257
+ target.addEventListener("touchstart", onUserIntent, { passive: true });
258
+ target.addEventListener("touchend", onUserIntentEnd, { passive: true });
259
+ target.addEventListener("touchcancel", onUserIntentEnd, { passive: true });
260
+ target.addEventListener("wheel", onWheel, { passive: true });
261
+ target.addEventListener("pointerdown", onUserIntent, { passive: true });
262
+ target.addEventListener("pointerup", onUserIntentEnd, { passive: true });
263
+ target.addEventListener("pointercancel", onUserIntentEnd, { passive: true });
264
+
265
+ const pos = read(root);
266
+ s.x = pos.x;
267
+ s.y = pos.y;
268
+
269
+ if (root === window) {
270
+ const ro = new ResizeObserver(() => scheduleRestore(window));
271
+ ro.observe(document.documentElement);
272
+ s.resizeObserver = ro;
273
+ return;
274
+ }
275
+
276
+ const el = root as HTMLElement;
277
+ const ro = new ResizeObserver(() => scheduleRestore(el));
278
+ ro.observe(el);
279
+ for (const child of Array.from(el.children)) {
280
+ ro.observe(child);
281
+ }
282
+ s.resizeObserver = ro;
283
+
284
+ const onDomChange = () => scheduleRestore(el);
285
+ const mo = new MutationObserver((mutations) => {
286
+ for (const mutation of mutations) {
287
+ if (mutation.type !== "childList") continue;
288
+ for (const node of Array.from(mutation.addedNodes)) {
289
+ if (node instanceof Element) ro.observe(node);
290
+ }
291
+ }
292
+ onDomChange();
293
+ });
294
+ // Shadow children
295
+ mo.observe(el, { childList: true, subtree: true, characterData: true });
296
+ // Light DOM du host (contenu <slot>) — sinon on rate suppressions / updates
297
+ const rootNode = el.getRootNode();
298
+ if (rootNode instanceof ShadowRoot && rootNode.host) {
299
+ mo.observe(rootNode.host, {
300
+ childList: true,
301
+ subtree: true,
302
+ characterData: true,
303
+ });
304
+ }
305
+ s.mutationObserver = mo;
306
+ }
307
+
308
+ function nearestScrollRootFromEvent(e: Event): ScrollRoot {
309
+ for (const node of e.composedPath()) {
310
+ if (node === window || node === document) continue;
311
+ if (node instanceof HTMLElement && isScrollContainer(node)) {
312
+ return node;
313
+ }
314
+ }
315
+ return window;
316
+ }
317
+
318
+ function isScrollContainer(el: Element): boolean {
319
+ if (!(el instanceof HTMLElement)) return false;
320
+ if (el === document.documentElement || el === document.body) return false;
321
+ const oy = getComputedStyle(el).overflowY;
322
+ return oy === "auto" || oy === "scroll" || oy === "overlay";
323
+ }
324
+
325
+ function discoverFromEvent(e: Event): void {
326
+ for (const node of e.composedPath()) {
327
+ if (node === window || node === document) continue;
328
+ if (node instanceof HTMLElement && isScrollContainer(node)) {
329
+ watchScrollRoot(node);
330
+ }
331
+ }
332
+
333
+ const engageRoot = nearestScrollRootFromEvent(e);
334
+ watchScrollRoot(engageRoot);
335
+
336
+ if (e.type === "wheel") {
337
+ captureScrollRoot(engageRoot);
338
+ markUserEngaged(engageRoot);
339
+ armScrollSettle(engageRoot);
340
+ } else if (e.type === "touchstart" || e.type === "pointerdown") {
341
+ captureScrollRoot(engageRoot);
342
+ markUserEngaged(engageRoot);
343
+ }
344
+ }
345
+
346
+ function watchScrollContainersInTree(root: ParentNode): void {
347
+ if (root instanceof Element && root.classList?.contains("custom-scroll")) {
348
+ watchScrollRoot(root as HTMLElement);
349
+ }
350
+ root.querySelectorAll?.(".custom-scroll").forEach((el) => {
351
+ if (el instanceof HTMLElement) watchScrollRoot(el);
352
+ });
353
+ }
354
+
355
+ function observeShadowRoot(shadow: ShadowRoot): void {
356
+ watchScrollContainersInTree(shadow);
357
+ const mo = new MutationObserver((mutations) => {
358
+ for (const mutation of mutations) {
359
+ for (const node of Array.from(mutation.addedNodes)) {
360
+ if (node instanceof Element) watchScrollContainersInTree(node);
361
+ }
362
+ }
363
+ });
364
+ mo.observe(shadow, { childList: true, subtree: true });
365
+ }
366
+
367
+ function walkExistingShadows(node: ParentNode): void {
368
+ node.querySelectorAll?.("*").forEach((el) => {
369
+ if (el.shadowRoot) {
370
+ observeShadowRoot(el.shadowRoot);
371
+ walkExistingShadows(el.shadowRoot);
372
+ }
373
+ });
374
+ }
375
+
376
+ function patchAttachShadow(): void {
377
+ const proto = Element.prototype as Element & {
378
+ __safariScrollGuardPatched?: boolean;
379
+ };
380
+ if (proto.__safariScrollGuardPatched) return;
381
+ proto.__safariScrollGuardPatched = true;
382
+ const nativeAttachShadow = proto.attachShadow;
383
+ proto.attachShadow = function attachShadowPatched(
384
+ this: Element,
385
+ init: ShadowRootInit,
386
+ ): ShadowRoot {
387
+ const shadow = nativeAttachShadow.call(this, init);
388
+ observeShadowRoot(shadow);
389
+ return shadow;
390
+ };
391
+ }
392
+
393
+ export function installSafariScrollGuard(): void {
394
+ if (typeof window === "undefined" || installed) return;
395
+ installed = true;
396
+
397
+ if (!isSafariScrollEngine()) return;
398
+
399
+ watchScrollRoot(window);
400
+ patchAttachShadow();
401
+
402
+ const opts: AddEventListenerOptions = { capture: true, passive: true };
403
+ window.addEventListener("wheel", discoverFromEvent, opts);
404
+ window.addEventListener("touchstart", discoverFromEvent, opts);
405
+ window.addEventListener("pointerdown", discoverFromEvent, opts);
406
+
407
+ const scan = () => {
408
+ watchScrollContainersInTree(document);
409
+ walkExistingShadows(document);
410
+ };
411
+
412
+ if (document.readyState === "loading") {
413
+ document.addEventListener("DOMContentLoaded", scan, { once: true });
414
+ } else {
415
+ scan();
416
+ }
417
+
418
+ const mo = new MutationObserver((mutations) => {
419
+ for (const mutation of mutations) {
420
+ for (const node of Array.from(mutation.addedNodes)) {
421
+ if (!(node instanceof Element)) continue;
422
+ watchScrollContainersInTree(node);
423
+ if (node.shadowRoot) observeShadowRoot(node.shadowRoot);
424
+ walkExistingShadows(node);
425
+ }
426
+ }
427
+ });
428
+ mo.observe(document.documentElement, { childList: true, subtree: true });
429
+ }
package/src/decorators.ts CHANGED
@@ -19,6 +19,8 @@ export const get = api.get;
19
19
  export const post = api.post;
20
20
  export const put = api.put;
21
21
  export const patch = api.patch;
22
+ export const refetch = api.refetch;
23
+ export const send = api.send;
22
24
  export {
23
25
  DataProviderKey,
24
26
  type DataProviderKeyHost,
@@ -27,6 +29,9 @@ export { Endpoint };
27
29
  export type { ApiResult, ApiGetResult } from "./core/utils/api";
28
30
  export type {
29
31
  ApiSendOptions,
32
+ ApiSendDecoratorHandle,
33
+ ApiMethodActionOptions,
34
+ GetDecoratorHandle,
30
35
  GetOptions,
31
36
  PostOptions,
32
37
  PutOptions,
@@ -57,4 +62,6 @@ window["concorde-decorator-subscriber"] = {
57
62
  post: api.post,
58
63
  put: api.put,
59
64
  patch: api.patch,
65
+ refetch: api.refetch,
66
+ send: api.send,
60
67
  };
@@ -1,123 +1,124 @@
1
1
  # @get
2
2
 
3
- Loads data through [`API.getDetailed`](../../core/utils/api.ts). The decorated property is **`ApiResult<T> | null`**: `request`, `response` (or `null` for `dataProvider(...)` resolution without HTTP), and typed `result`.
3
+ `@get` loads data and stores the result in an optional `ApiResult<T>` property.
4
+ The property is `undefined` until a result is available.
4
5
 
5
- Pass an [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint) as the first argument. Import `get` and `ApiResult` from `@supersoniks/concorde/decorators`, and `Endpoint` from `@supersoniks/concorde/utils/endpoint`.
6
+ - `payload.result`: typed data;
7
+ - `payload.response`: the HTTP response;
8
+ - `payload.request`: the request that was sent.
6
9
 
7
- ## Configuration
10
+ ## Minimal usage
8
11
 
9
- - **Default:** `HTML.getApiConfiguration(host)` (ancestor `serviceURL`, etc.).
10
- - **Second argument:** `DataProviderKey<APIConfiguration>` — config is read from the publisher at the resolved path; internal mutations trigger another GET. See [API configuration](#docs/_misc/api-configuration.md/api-configuration) for mock demos.
12
+ ```typescript
13
+ const usersEndpoint = new Endpoint<User[]>("users");
11
14
 
12
- ## Dynamic path
15
+ @get(usersEndpoint)
16
+ @state()
17
+ payload?: ApiResult<User[]>;
18
+ ```
13
19
 
14
- `${prop}` on the endpoint or config key is resolved on the host. While a placeholder is `null` or `undefined`, no GET runs. Optional `skipEmptyPlaceholder: true` also blocks `""` (empty string only). Details: [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
20
+ The first argument is always an
21
+ [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint). API configuration is read
22
+ from the HTML scope (`serviceURL`, token, etc.), so the component is usually
23
+ placed below an element that provides this configuration.
15
24
 
16
- ## Options (`GetOptions`)
25
+ ## Refetching a GET
17
26
 
18
- Pass as the **second** argument (scoped config) or **third** (with a configuration key). Same shape as send decorators where applicable:
27
+ ### From the component
19
28
 
20
- | Option | Description |
21
- |--------|-------------|
22
- | `skipEmptyPlaceholder` | Treat `""` as not ready (see [Dynamic path](#docs/_misc/dynamic-path.md/dynamic-path)). |
23
- | `refetchEveryMs` | Automatic polling interval (ms). `0` or omitted = no polling. |
24
- | `triggerKey` | `DataProviderKey` — `PublisherManager.get(...).invalidate()` on that publisher re-runs the GET. |
29
+ Keep the decorator in a constant to access `refetch(component)`. Refetch only
30
+ targets the instance passed as the argument, even before it has received a
31
+ result.
25
32
 
26
- ### Manual refetch (`triggerKey`)
33
+ ```typescript
34
+ const usersGet = get(new Endpoint<User[]>("users"));
27
35
 
28
- Same pattern as [@post](#docs/_decorators/post.md/post): a publisher acts as a **signal**, independent of the response payload.
36
+ @usersGet
37
+ @state()
38
+ payload?: ApiResult<User[]>;
39
+
40
+ refresh() {
41
+ usersGet.refetch(this);
42
+ }
43
+ ```
44
+
45
+ ### From a method decorator
46
+
47
+ The refetch can decorate a local method. The default is `after`; if the
48
+ method returns a Promise, the GET waits for its resolution.
29
49
 
30
50
  ```typescript
31
- import { get, type ApiResult } from "@supersoniks/concorde/decorators";
32
- import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
33
- import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
34
- import { dp } from "@supersoniks/concorde/core/utils/PublisherProxy";
35
-
36
- const geoCommunesEndpoint = new Endpoint<GeoCommuneRow[]>(
37
- "communes?limit=5&fields=nom,code",
38
- );
39
- const geoCommunesRefreshKey = new DataProviderKey<void>(
40
- "docsDemoGeoCommunesRefresh",
41
- );
42
-
43
- @get(geoCommunesEndpoint, apiConfigurationKey, {
44
- triggerKey: geoCommunesRefreshKey,
45
- })
46
- @state()
47
- payload: ApiResult<GeoCommuneRow[]> | null = null;
51
+ import { refetch } from "@supersoniks/concorde/decorators";
52
+
53
+ @refetch(usersGet)
54
+ async reload() {
55
+ this.userId = "42";
56
+ }
57
+ ```
48
58
 
49
- // Same component or anywhere else:
50
- dp(geoCommunesRefreshKey).invalidate();
59
+ Use `{ when: "before" }` when the GET must be scheduled before the method:
60
+
61
+ ```typescript
62
+ @refetch(usersGet, { when: "before" })
63
+ reloadBefore() {
64
+ // The refetch is scheduled before this method runs.
65
+ }
51
66
  ```
52
67
 
53
- Prefer `triggerKey` over mutating the API configuration publisher when you only want to **reload the same URL** — no need to touch `serviceURL`, tokens, etc.
68
+ The method decorator adds an explicit trigger; changes to dynamic endpoint
69
+ properties and configuration publishers keep their existing automatic behavior.
70
+
71
+ ## Options (`GetOptions`)
72
+
73
+ Options are the second argument, or the third argument when an API
74
+ configuration key is provided.
75
+
76
+ - `skipEmptyPlaceholder`: blocks the GET when `${prop}` is `""`.
77
+ - `refetchEveryMs`: automatically runs the GET again after this interval
78
+ (milliseconds). Omit the option, or use `0`, to disable polling.
54
79
 
55
- The docs mock adds an **`X-Fetched-At`** response header on `/docs-mock-api/geo/communes` so live demos can show when the last fetch happened (`response.headers.get("X-Fetched-At")`).
80
+ With a configuration key, use:
81
+
82
+ ```typescript
83
+ @get(usersEndpoint, apiConfigurationKey)
84
+ ```
56
85
 
57
86
  ## When the GET runs again
58
87
 
59
- - A referenced Lit property changes (endpoint path and/or config key contains `${...}`).
60
- - `set` on the active configuration publisher (`onInternalMutation`).
61
- - `triggerKey` publisher `invalidate()`.
62
- - `refetchEveryMs` polling (after each successful fetch).
88
+ - when the component connects and configuration is available;
89
+ - when a property used in `${...}` changes;
90
+ - when a configuration publisher changes;
91
+ - after each response when `refetchEveryMs` is set.
92
+
93
+ See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path) for
94
+ dynamic path details.
63
95
 
64
96
  ## Import
65
97
 
66
98
  <sonic-code language="typescript">
67
99
  <template>
68
100
  import { get, type ApiResult } from "@supersoniks/concorde/decorators";
69
- import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
70
101
  import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
102
+ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
71
103
  </template>
72
104
  </sonic-code>
73
105
 
74
- ## Minimal example
75
-
76
- Same demo service as [`sonic-queue`](../../core/components/functional/queue/queue.demo.ts) (`/docs-mock-api/geo/`). Publisher setup lives in `decorators-demo-geo.ts` and `decorators-demo-subscribe-publish-get-demos.ts`.
77
-
78
- <sonic-code language="typescript">
79
- <template>
80
- @get(new Endpoint<User>("users/${userId}"))
81
- @state()
82
- payload: ApiResult<User> | null = null;
83
- </template>
84
- </sonic-code>
85
-
86
- ## Live demos
87
-
88
- `@get` with **`triggerKey`** — refresh button + `X-Fetched-At` timestamp in the UI:
106
+ ## Demos
89
107
 
90
108
  <sonic-code>
91
109
  <template>
92
110
  <docs-demo-sources for="demo-api-get"></docs-demo-sources>
93
111
  <demo-api-get></demo-api-get>
94
- </template>
95
- </sonic-code>
96
-
97
- Dynamic config and endpoint path. Manual refetch is triggered **from a separate component** (same `triggerKey` publisher):
98
-
99
- <sonic-code>
100
- <template>
101
112
  <docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
102
113
  <demo-api-get-configuration-key></demo-api-get-configuration-key>
103
- <demo-api-get-refresh-remote></demo-api-get-refresh-remote>
104
- </template>
105
- </sonic-code>
106
-
107
- Scoped `@get` with `@publish` / `@subscribe` on the payload (see [@publish](#docs/_decorators/publish.md/publish) and [@subscribe](#docs/_decorators/subscribe.md/subscribe)) — wrap under an ancestor with `serviceURL="/docs-mock-api/geo/"`:
108
-
109
- <sonic-code>
110
- <template>
111
- <div serviceURL="/docs-mock-api/geo/">
112
- <docs-demo-sources for="demo-api-get-publish-subscribe"></docs-demo-sources>
113
- <demo-api-get-publish-subscribe></demo-api-get-publish-subscribe>
114
- </div>
115
114
  </template>
116
115
  </sonic-code>
117
116
 
118
- Stale responses are ignored if the path or generation changed before the request finished.
117
+ See [@post](#docs/_decorators/post.md/post),
118
+ [@put](#docs/_decorators/put.md/put), and
119
+ [@patch](#docs/_decorators/patch.md/patch) for write operations.
119
120
 
120
- ## See also
121
+ ## Advanced
121
122
 
122
- - [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path)
123
- - [@post](#docs/_decorators/post.md/post) · [@put](#docs/_decorators/put.md/put) · [@patch](#docs/_decorators/patch.md/patch) — send decorators (same path rules)
123
+ External invalidation is currently available through `triggerKey`; it is kept
124
+ out of the main usage examples.