@supersoniks/concorde 4.9.0 → 4.9.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.
@@ -13,10 +13,53 @@ Pass an [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint) as the first argument.
13
13
 
14
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).
15
15
 
16
+ ## Options (`GetOptions`)
17
+
18
+ Pass as the **second** argument (scoped config) or **third** (with a configuration key). Same shape as send decorators where applicable:
19
+
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. |
25
+
26
+ ### Manual refetch (`triggerKey`)
27
+
28
+ Same pattern as [@post](#docs/_decorators/post.md/post): a publisher acts as a **signal**, independent of the response payload.
29
+
30
+ ```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;
48
+
49
+ // Same component or anywhere else:
50
+ dp(geoCommunesRefreshKey).invalidate();
51
+ ```
52
+
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.
54
+
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")`).
56
+
16
57
  ## When the GET runs again
17
58
 
18
59
  - A referenced Lit property changes (endpoint path and/or config key contains `${...}`).
19
60
  - `set` on the active configuration publisher (`onInternalMutation`).
61
+ - `triggerKey` publisher `invalidate()`.
62
+ - `refetchEveryMs` polling (after each successful fetch).
20
63
 
21
64
  ## Import
22
65
 
@@ -42,6 +85,8 @@ payload: ApiResult<User> | null = null;
42
85
 
43
86
  ## Live demos
44
87
 
88
+ `@get` with **`triggerKey`** — refresh button + `X-Fetched-At` timestamp in the UI:
89
+
45
90
  <sonic-code>
46
91
  <template>
47
92
  <docs-demo-sources for="demo-api-get"></docs-demo-sources>
@@ -49,12 +94,13 @@ payload: ApiResult<User> | null = null;
49
94
  </template>
50
95
  </sonic-code>
51
96
 
52
- Dynamic config and endpoint path (`demo-api-get-configuration-key` in doc sources):
97
+ Dynamic config and endpoint path. Manual refetch is triggered **from a separate component** (same `triggerKey` publisher):
53
98
 
54
99
  <sonic-code>
55
100
  <template>
56
101
  <docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
57
102
  <demo-api-get-configuration-key></demo-api-get-configuration-key>
103
+ <demo-api-get-refresh-remote></demo-api-get-refresh-remote>
58
104
  </template>
59
105
  </sonic-code>
60
106
 
@@ -255,10 +255,15 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
255
255
  "demo-api-get-configuration-key": [
256
256
  {
257
257
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
258
- line: 126,
258
+ line: 147,
259
259
  label: "demo-api-get-configuration-key",
260
260
  },
261
- { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
261
+ {
262
+ path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
263
+ line: 239,
264
+ label: "demo-api-get-refresh-remote",
265
+ },
266
+ { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API + triggerKey" },
262
267
  ],
263
268
  "demo-api-get-publish-subscribe": [
264
269
  {
@@ -49,3 +49,8 @@ export const geoCommunesApiGetEndpointDynamic = new Endpoint<GeoCommuneRow[]>(
49
49
  export const geoCommunesApiGetPublishKey = new DataProviderKey<
50
50
  ApiResult<GeoCommuneRow[]>
51
51
  >(geoCommunesApiGetEndpoint.path);
52
+
53
+ /** Signal de refetch manuel pour les démos `@get` (voir `triggerKey`). */
54
+ export const docsDemoGeoCommunesRefreshKey = new DataProviderKey<void>(
55
+ "docsDemoGeoCommunesRefresh",
56
+ );
@@ -1,32 +1,37 @@
1
1
  import { html, LitElement } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import { get, handle, publish, subscribe } from "@supersoniks/concorde/decorators";
4
- import type {
5
- APIConfiguration,
6
- ApiResult,
7
- } from "@supersoniks/concorde/core/utils/api";
4
+ import type { ApiResult } from "@supersoniks/concorde/core/utils/api";
8
5
  import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
9
6
  import { sub } from "@supersoniks/concorde/directives";
10
7
  import {
8
+ dp,
11
9
  get as storeGet,
12
10
  set,
13
11
  } from "@supersoniks/concorde/core/utils/PublisherProxy";
14
12
  import { publishDemoKey } from "./docs-provider-keys";
15
- import {
16
- docsDemoDynApiConfAKey,
17
- docsDemoDynApiConfBKey,
18
- } from "./decorators-demo-geo";
19
13
  import { tailwind } from "../tailwind";
20
14
  import "./decorators-demo-init";
21
15
  import {
22
16
  docsDemoDynApiConfKeyTemplate,
23
17
  docsDemoGeoApiConfigurationKey,
18
+ docsDemoGeoCommunesRefreshKey,
24
19
  geoCommunesApiGetEndpoint,
25
20
  geoCommunesApiGetEndpointDynamic,
26
21
  geoCommunesApiGetPublishKey,
27
22
  type GeoCommuneRow,
28
23
  } from "./decorators-demo-geo";
29
24
 
25
+ function formatFetchedAt(response: Response | null | undefined): string {
26
+ const raw = response?.headers.get("X-Fetched-At");
27
+ if (!raw) return "—";
28
+ try {
29
+ return new Date(raw).toLocaleTimeString();
30
+ } catch {
31
+ return raw;
32
+ }
33
+ }
34
+
30
35
  type PublishDemoData = {
31
36
  email: string;
32
37
  message: string;
@@ -90,22 +95,35 @@ export class DemoPublish extends LitElement {
90
95
  export class DemoApiGet extends LitElement {
91
96
  static styles = [tailwind];
92
97
 
93
- @get(geoCommunesApiGetEndpoint, docsDemoGeoApiConfigurationKey)
98
+ @get(geoCommunesApiGetEndpoint, docsDemoGeoApiConfigurationKey, {
99
+ triggerKey: docsDemoGeoCommunesRefreshKey,
100
+ })
94
101
  @state()
95
102
  geoApiPayload?: ApiResult<GeoCommuneRow[]>;
96
103
 
104
+ private refresh() {
105
+ dp(docsDemoGeoCommunesRefreshKey).invalidate();
106
+ }
107
+
97
108
  render() {
98
109
  const rows = this.geoApiPayload?.result;
99
110
  const status = this.geoApiPayload?.response?.status;
111
+ const fetchedAt = formatFetchedAt(this.geoApiPayload?.response ?? null);
100
112
  return html`
101
113
  <div class="space-y-2">
102
114
  <p class="text-sm text-neutral-600 dark:text-neutral-400">
103
115
  <code>@get</code> — même service que
104
116
  <code>sonic-queue</code> (<code>/docs-mock-api/geo/</code>).
105
117
  Propriété typée <code>ApiResult&lt;T&gt;</code>.
118
+ Refetch via <code>triggerKey</code> (<code
119
+ >docsDemoGeoCommunesRefresh</code
120
+ >).
106
121
  </p>
122
+ <sonic-button size="sm" @click=${this.refresh}>Rafraîchir</sonic-button>
107
123
  ${status != null
108
- ? html`<p class="text-xs text-neutral-500">HTTP ${status}</p>`
124
+ ? html`<p class="text-xs text-neutral-500">
125
+ HTTP ${status} · fetch ${fetchedAt}
126
+ </p>`
109
127
  : ""}
110
128
  ${this.geoApiPayload === null
111
129
  ? html`<p class="text-neutral-500">Chargement…</p>`
@@ -139,29 +157,23 @@ export class DemoApiGetConfigurationKey extends LitElement {
139
157
  @property({ type: Number })
140
158
  communeLimit = 5;
141
159
 
142
- @get(geoCommunesApiGetEndpointDynamic, docsDemoDynApiConfKeyTemplate)
160
+ @get(geoCommunesApiGetEndpointDynamic, docsDemoDynApiConfKeyTemplate, {
161
+ triggerKey: docsDemoGeoCommunesRefreshKey,
162
+ })
143
163
  @state()
144
164
  geoApiPayloadDyn?: ApiResult<GeoCommuneRow[]>;
145
165
 
146
- private touchCurrentConfigPublisher() {
147
- const key =
148
- this.configSlot === "A" ? docsDemoDynApiConfAKey : docsDemoDynApiConfBKey;
149
- const cur = storeGet(key) as APIConfiguration;
150
- set(key, {
151
- ...cur,
152
- blockUntilDone: !cur.blockUntilDone,
153
- });
154
- }
155
-
156
166
  render() {
157
167
  const rows = this.geoApiPayloadDyn?.result;
158
168
  const status = this.geoApiPayloadDyn?.response?.status;
169
+ const fetchedAt = formatFetchedAt(this.geoApiPayloadDyn?.response ?? null);
159
170
  return html`
160
171
  <div class="space-y-3">
161
172
  <p class="text-sm text-neutral-600 dark:text-neutral-400">
162
173
  Config dynamique (<code>docsDemoDynApiConf\${configSlot}</code>) +
163
- path d’endpoint avec <code>communeLimit</code>. « Toucher la config »
164
- déclenche un nouveau GET.
174
+ path d’endpoint avec <code>communeLimit</code>. Le refetch manuel
175
+ passe par <code>triggerKey</code> — voir le bouton « ailleurs »
176
+ ci-dessous dans la doc.
165
177
  </p>
166
178
  <div class="flex flex-wrap gap-2 items-center">
167
179
  <span class="text-xs text-neutral-500">Limite API :</span>
@@ -196,16 +208,11 @@ export class DemoApiGetConfigurationKey extends LitElement {
196
208
  }}
197
209
  >docsDemoDynApiConfB</sonic-button
198
210
  >
199
- <sonic-button
200
- size="sm"
201
- @click=${() => this.touchCurrentConfigPublisher()}
202
- >Toucher la config</sonic-button
203
- >
204
211
  </div>
205
212
  ${status != null
206
213
  ? html`<p class="text-xs text-neutral-500">
207
- HTTP ${status} · limit=${this.communeLimit} · conf=
208
- docsDemoDynApiConf${this.configSlot}
214
+ HTTP ${status} · fetch ${fetchedAt} · limit=${this.communeLimit} ·
215
+ conf=docsDemoDynApiConf${this.configSlot}
209
216
  </p>`
210
217
  : ""}
211
218
  ${this.geoApiPayloadDyn === null
@@ -230,6 +237,35 @@ export class DemoApiGetConfigurationKey extends LitElement {
230
237
  }
231
238
  }
232
239
 
240
+ /** Déclenche le refetch d’un `@get` distant via `triggerKey` (sans toucher la config). */
241
+ @customElement("demo-api-get-refresh-remote")
242
+ export class DemoApiGetRefreshRemote extends LitElement {
243
+ static styles = [tailwind];
244
+
245
+ private refreshFromElsewhere() {
246
+ dp(docsDemoGeoCommunesRefreshKey).invalidate();
247
+ }
248
+
249
+ render() {
250
+ return html`
251
+ <div
252
+ class="rounded border border-dashed border-neutral-300 dark:border-neutral-600 p-3 space-y-2"
253
+ >
254
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
255
+ Composant séparé — même publisher
256
+ <code>docsDemoGeoCommunesRefresh</code> :
257
+ </p>
258
+ <pre
259
+ class="text-xs bg-neutral-100 dark:bg-neutral-800 p-2 rounded overflow-x-auto"
260
+ ><code>dp(docsDemoGeoCommunesRefreshKey).invalidate();</code></pre>
261
+ <sonic-button size="sm" variant="secondary" @click=${this.refreshFromElsewhere}>
262
+ Rafraîchir le GET (depuis ailleurs)
263
+ </sonic-button>
264
+ </div>
265
+ `;
266
+ }
267
+ }
268
+
233
269
  @customElement("demo-api-get-publish-subscribe")
234
270
  export class DemoApiGetPublishSubscribe extends LitElement {
235
271
  static styles = [tailwind];
@@ -247,6 +283,9 @@ export class DemoApiGetPublishSubscribe extends LitElement {
247
283
  const rows =
248
284
  this.geoCommunesFromSubscribe ?? this.geoApiPayloadPublished?.result;
249
285
  const status = this.geoApiPayloadPublished?.response?.status;
286
+ const fetchedAt = formatFetchedAt(
287
+ this.geoApiPayloadPublished?.response ?? null,
288
+ );
250
289
  return html`
251
290
  <div class="space-y-2">
252
291
  <p class="text-sm text-neutral-600 dark:text-neutral-400">
@@ -255,7 +294,9 @@ export class DemoApiGetPublishSubscribe extends LitElement {
255
294
  rendu que <code>&lt;demo-api-get&gt;</code> (doc @get).
256
295
  </p>
257
296
  ${status != null
258
- ? html`<p class="text-xs text-neutral-500">HTTP ${status}</p>`
297
+ ? html`<p class="text-xs text-neutral-500">
298
+ HTTP ${status} · fetch ${fetchedAt}
299
+ </p>`
259
300
  : ""}
260
301
  ${this.geoApiPayloadPublished === null
261
302
  ? html`<p class="text-neutral-500">Chargement…</p>`
@@ -242,7 +242,9 @@ export async function handleDocsMockApiRequest(
242
242
  }
243
243
 
244
244
  if (subPath === "/geo/communes" || subPath.startsWith("/geo/communes")) {
245
- return json(geoCommunes(url));
245
+ return json(geoCommunes(url), {
246
+ headers: { "X-Fetched-At": new Date().toISOString() },
247
+ });
246
248
  }
247
249
 
248
250
  const jokeMatch = subPath.match(/^\/jokes\/joke\/([^/]+)$/);
@@ -4030,7 +4030,22 @@
4030
4030
  }
4031
4031
  },
4032
4032
  {
4033
- "search": "Use { dynamic: true } when the decorated property must follow the DOM. The live demo below shows only the resolved attribute value no store, no reparenting.\n",
4033
+ "search": "Use { dynamic: true } when the decorated property must follow the DOM. With Lit, add @state() on the same property so template updates are visible when the observer assigns a new value.\n",
4034
+ "files": {
4035
+ "docs/_decorators/ancestor-attribute.md": {
4036
+ "title": "@ancestorAttribute",
4037
+ "hashes": {
4038
+ "dynamic-mode": {
4039
+ "count": 1,
4040
+ "title": "Dynamic mode",
4041
+ "type": "paragraph"
4042
+ }
4043
+ }
4044
+ }
4045
+ }
4046
+ },
4047
+ {
4048
+ "search": "The live demo below shows only the resolved attribute value — no store, no reparenting.\n",
4034
4049
  "files": {
4035
4050
  "docs/_decorators/ancestor-attribute.md": {
4036
4051
  "title": "@ancestorAttribute",
@@ -4729,6 +4744,21 @@
4729
4744
  }
4730
4745
  }
4731
4746
  },
4747
+ {
4748
+ "search": "Pass as the second argument (scoped config) or third (with a configuration key). Same shape as send decorators where applicable:\n",
4749
+ "files": {
4750
+ "docs/_decorators/get.md": {
4751
+ "title": "@get",
4752
+ "hashes": {
4753
+ "options-getoptions": {
4754
+ "count": 1,
4755
+ "title": "Options (`GetOptions`)",
4756
+ "type": "paragraph"
4757
+ }
4758
+ }
4759
+ }
4760
+ }
4761
+ },
4732
4762
  {
4733
4763
  "search": "Same demo service as sonic-queue (/docs-mock-api/geo/). Publisher setup lives in decorators-demo-geo.ts and decorators-demo-subscribe-publish-get-demos.ts.\n",
4734
4764
  "files": {