@supersoniks/concorde 4.9.91 → 4.9.93

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 (48) hide show
  1. package/ai/cursor/rules/concorde.mdc +7 -0
  2. package/ai/jetbrains/rules/concorde.md +6 -0
  3. package/ai/skills/concorde/SKILL.md +48 -0
  4. package/build-infos.json +1 -1
  5. package/concorde-core.bundle.js +257 -257
  6. package/concorde-core.es.js +3174 -3086
  7. package/dist/concorde-core.bundle.js +257 -257
  8. package/dist/concorde-core.es.js +3174 -3086
  9. package/dist/js/core/decorators/api.js +168 -63
  10. package/dist/js/core/decorators/api.js.map +1 -1
  11. package/dist/js/core/utils/api.js +29 -8
  12. package/dist/js/core/utils/api.js.map +1 -1
  13. package/dist/types/core/decorators/api.d.ts +30 -16
  14. package/dist/types/core/decorators/api.d.ts.map +1 -1
  15. package/dist/types/core/utils/api.d.ts +1 -1
  16. package/dist/types/core/utils/api.d.ts.map +1 -1
  17. package/docs/assets/{index-BL6WVF7g.css → index-D2Q3JyQ6.css} +1 -1
  18. package/docs/assets/{index-DVn8AeyA.js → index-DURr6O71.js} +466 -568
  19. package/docs/crawl/docs/_decorators/get.html +35 -10
  20. package/docs/crawl/docs/_decorators/patch.html +13 -5
  21. package/docs/crawl/docs/_decorators/post.html +39 -10
  22. package/docs/crawl/docs/_decorators/put.html +13 -5
  23. package/docs/crawl/hubs/decorators.html +43 -40
  24. package/docs/crawl/hubs/remote-data.html +43 -40
  25. package/docs/index.html +2 -2
  26. package/docs/llms-full.txt +98 -23
  27. package/docs/llms.txt +1 -1
  28. package/docs/src/docs/_decorators/get.md +39 -5
  29. package/docs/src/docs/_decorators/patch.md +8 -3
  30. package/docs/src/docs/_decorators/post.md +39 -6
  31. package/docs/src/docs/_decorators/put.md +8 -3
  32. package/docs/src/docs/search/docs-search.json +103 -183
  33. package/docs/src/tsconfig.json +268 -268
  34. package/package.json +1 -1
  35. package/src/core/decorators/api.post.spec.ts +101 -6
  36. package/src/core/decorators/api.spec.ts +137 -6
  37. package/src/core/decorators/api.ts +304 -148
  38. package/src/core/utils/api.spec.ts +73 -0
  39. package/src/core/utils/api.ts +44 -11
  40. package/src/docs/_decorators/get.md +43 -5
  41. package/src/docs/_decorators/patch.md +16 -2
  42. package/src/docs/_decorators/post.md +23 -7
  43. package/src/docs/_decorators/put.md +16 -2
  44. package/src/docs/components/docs-demo-sources.ts +22 -8
  45. package/src/docs/example/decorators-demo-post.ts +70 -0
  46. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +61 -2
  47. package/src/docs/search/docs-search.json +122 -7
  48. package/src/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,73 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import API, { type APIConfiguration } from "./api";
3
+
4
+ const baseConfig: APIConfiguration = {
5
+ serviceURL: "https://api.example.test",
6
+ token: null,
7
+ userName: null,
8
+ password: null,
9
+ authToken: null,
10
+ tokenProvider: null,
11
+ };
12
+
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ vi.unstubAllGlobals();
16
+ });
17
+
18
+ describe("API GET synchronization", () => {
19
+ it("deduplicates fetch while sharing the response metadata", async () => {
20
+ const response = new Response(JSON.stringify({ items: [1, 2] }), {
21
+ status: 200,
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ const fetchMock = vi.fn().mockResolvedValue(response);
25
+ vi.stubGlobal("fetch", fetchMock);
26
+
27
+ const firstApi = new API({
28
+ ...baseConfig,
29
+ serviceURL: "https://api.example.test/concurrent",
30
+ });
31
+ const secondApi = new API({
32
+ ...baseConfig,
33
+ serviceURL: "https://api.example.test/concurrent",
34
+ });
35
+
36
+ const [firstPayload, secondPayload] = await Promise.all([
37
+ firstApi.getDetailed<{ items: number[] }>("items"),
38
+ secondApi.getDetailed<{ items: number[] }>("items"),
39
+ ]);
40
+
41
+ expect(fetchMock).toHaveBeenCalledTimes(1);
42
+ expect(firstPayload?.response?.status).toBe(200);
43
+ expect(secondPayload?.response?.status).toBe(200);
44
+ expect(firstPayload?.result).toEqual({ items: [1, 2] });
45
+ expect(secondPayload?.result).toEqual({ items: [1, 2] });
46
+ });
47
+
48
+ it("permet un nouvel appel après une erreur réseau", async () => {
49
+ const response = new Response(JSON.stringify({ ok: true }), {
50
+ status: 200,
51
+ headers: { "Content-Type": "application/json" },
52
+ });
53
+ const fetchMock = vi
54
+ .fn()
55
+ .mockRejectedValueOnce(new Error("offline"))
56
+ .mockResolvedValueOnce(response);
57
+ vi.stubGlobal("fetch", fetchMock);
58
+
59
+ const api = new API({
60
+ ...baseConfig,
61
+ serviceURL: "https://api.example.test/recover",
62
+ });
63
+
64
+ await expect(api.getDetailed<{ ok: boolean }>("items")).resolves.toBe(
65
+ undefined,
66
+ );
67
+ const payload = await api.getDetailed<{ ok: boolean }>("items");
68
+
69
+ expect(fetchMock).toHaveBeenCalledTimes(2);
70
+ expect(payload?.response?.status).toBe(200);
71
+ expect(payload?.result).toEqual({ ok: true });
72
+ });
73
+ });
@@ -49,6 +49,11 @@ export type ApiResult<T> = {
49
49
  /** @deprecated Utiliser `ApiResult`. */
50
50
  export type ApiGetResult<T> = ApiResult<T>;
51
51
 
52
+ type SharedGetResult = {
53
+ processed: ResultTypeInterface;
54
+ response?: Response;
55
+ };
56
+
52
57
  /** Extrait le corps typé depuis le résultat traité par `handleResult`. */
53
58
  function extractTypedApiResult<T>(
54
59
  processed: ResultTypeInterface | null | undefined,
@@ -67,7 +72,10 @@ class API {
67
72
  /**
68
73
  * Ce tableau static permet de ne pas appeler plusieurs fois le même service lors d'appel concurrents en GET.
69
74
  */
70
- private static loadingGetPromises: Map<string, Promise<unknown>> = new Map();
75
+ private static loadingGetPromises: Map<
76
+ string,
77
+ Promise<SharedGetResult | null>
78
+ > = new Map();
71
79
  /**
72
80
  * L'url de base du service sans endpoint
73
81
  */
@@ -202,10 +210,14 @@ class API {
202
210
  if (httpCode === 498 && !API.failledTokenUpdates.has(this.serviceURL)) {
203
211
  this.handleInvalidToken(this.token);
204
212
  if (lastCall.apiMethod === "get") {
205
- result = await this[lastCall.apiMethod](
213
+ const retryResult = await this.get(
206
214
  lastCall.path,
207
215
  lastCall.additionalHeaders,
208
216
  );
217
+ if (retryResult == null) {
218
+ throw new Error("GET retry failed after token refresh");
219
+ }
220
+ result = retryResult;
209
221
  } else {
210
222
  result = await this[lastCall.apiMethod](
211
223
  lastCall.path,
@@ -384,13 +396,16 @@ class API {
384
396
  });
385
397
  }
386
398
 
387
- async get<T>(path: string, additionalHeaders?: HeadersInit) {
388
- await this.firstCallDone();
389
- if (this.blockUntilDone) {
390
- API.firstCallDoneFlags.set(this.serviceURL, "loading");
391
- }
399
+ async get<T>(
400
+ path: string,
401
+ additionalHeaders?: HeadersInit,
402
+ ): Promise<(T & ResultTypeInterface) | null> {
392
403
  const regExp = /dataProvider\((.*?)\)(.*?)$/;
393
404
  if (regExp.test(path)) {
405
+ if (this.blockUntilDone) {
406
+ await this.firstCallDone();
407
+ API.firstCallDoneFlags.set(this.serviceURL, "loading");
408
+ }
394
409
  const match = path.match(regExp);
395
410
  if (!match) throw new Error("dataProvider path is not valid");
396
411
  return await this.localGet<T>(match[1], match[2]);
@@ -405,9 +420,21 @@ class API {
405
420
  const mapKey = JSON.stringify({
406
421
  url: url,
407
422
  headers: headers,
423
+ credentials: this.credentials,
424
+ cache: this.cache,
425
+ keepAlive: this.keepAlive,
408
426
  });
427
+
428
+ // Les GET non bloquants doivent pouvoir rejoindre une requête déjà en vol.
429
+ // Sinon firstCallDone() sérialise les appels avant que la map de déduplication
430
+ // ne soit consultée, ce qui déclenche deux fetch pour deux consommateurs.
431
+ if (this.blockUntilDone) {
432
+ await this.firstCallDone();
433
+ API.firstCallDoneFlags.set(this.serviceURL, "loading");
434
+ }
435
+
409
436
  if (!API.loadingGetPromises.has(mapKey)) {
410
- const promise = new Promise(async (resolve) => {
437
+ const promise = new Promise<SharedGetResult | null>(async (resolve) => {
411
438
  try {
412
439
  const result = await fetch(url, {
413
440
  headers: headers,
@@ -416,16 +443,22 @@ class API {
416
443
  keepalive: this.keepAlive,
417
444
  });
418
445
  const handledResult = await this.handleResult(result, lastCall);
419
- resolve(handledResult);
446
+ resolve({
447
+ processed: handledResult,
448
+ response: this.lastResult,
449
+ });
420
450
  } catch (e) {
451
+ API.firstCallDoneFlags.set(this.serviceURL, "error");
421
452
  resolve(null);
422
453
  }
423
454
  });
424
455
  API.loadingGetPromises.set(mapKey, promise);
425
456
  }
426
- const result = (await API.loadingGetPromises.get(mapKey)) as T;
457
+ const sharedResult = await API.loadingGetPromises.get(mapKey);
427
458
  API.loadingGetPromises.delete(mapKey);
428
- return result as T & ResultTypeInterface;
459
+ if (!sharedResult) return null;
460
+ this.lastResult = sharedResult.response;
461
+ return sharedResult.processed as T & ResultTypeInterface;
429
462
  }
430
463
 
431
464
  /**
@@ -17,6 +17,19 @@ const usersEndpoint = new Endpoint<User[]>("users");
17
17
  payload?: ApiResult<User[]>;
18
18
  ```
19
19
 
20
+ The same decorator adapts to a method. The method receives the complete
21
+ `ApiResult<T>` payload after each GET:
22
+
23
+ ```typescript
24
+ @get(usersEndpoint)
25
+ handleUsers(payload?: ApiResult<User[]>) {
26
+ if (!payload) return;
27
+
28
+ console.log(payload.response?.status);
29
+ console.log(payload.result);
30
+ }
31
+ ```
32
+
20
33
  The first argument is always an
21
34
  [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint). API configuration is read
22
35
  from the HTML scope (`serviceURL`, token, etc.), so the component is usually
@@ -37,15 +50,36 @@ const usersGet = get(new Endpoint<User[]>("users"));
37
50
  @state()
38
51
  payload?: ApiResult<User[]>;
39
52
 
40
- refresh() {
41
- usersGet.refetch(this);
53
+ async refresh() {
54
+ const payload = await usersGet.refetch(this);
55
+ console.log(payload?.result);
56
+ }
57
+ ```
58
+
59
+ `refetch(component)` returns a `Promise<ApiResult<T> | undefined>` that resolves
60
+ after the GET has completed.
61
+
62
+ The named handle can also decorate a method and be reused for refetching:
63
+
64
+ ```typescript
65
+ const usersGet = get(new Endpoint<User[]>("users"));
66
+
67
+ @usersGet
68
+ handleUsers(payload?: ApiResult<User[]>) {
69
+ // ...
70
+ }
71
+
72
+ @refetch(usersGet)
73
+ reloadUsers() {
74
+ // The GET runs and handleUsers receives its payload.
42
75
  }
43
76
  ```
44
77
 
45
78
  ### From a method decorator
46
79
 
47
80
  The refetch can decorate a local method. The default is `after`; if the
48
- method returns a Promise, the GET waits for its resolution.
81
+ method returns a Promise, the GET waits for its resolution. The decorated method
82
+ call also waits for the refetch to complete.
49
83
 
50
84
  ```typescript
51
85
  import { refetch } from "@supersoniks/concorde/decorators";
@@ -56,12 +90,12 @@ async reload() {
56
90
  }
57
91
  ```
58
92
 
59
- Use `{ when: "before" }` when the GET must be scheduled before the method:
93
+ Use `{ when: "before" }` when the GET must complete before the method runs:
60
94
 
61
95
  ```typescript
62
96
  @refetch(usersGet, { when: "before" })
63
97
  reloadBefore() {
64
- // The refetch is scheduled before this method runs.
98
+ // The refetch has completed before this method runs.
65
99
  }
66
100
  ```
67
101
 
@@ -76,6 +110,7 @@ configuration key is provided.
76
110
  - `skipEmptyPlaceholder`: blocks the GET when `${prop}` is `""`.
77
111
  - `refetchEveryMs`: automatically runs the GET again after this interval
78
112
  (milliseconds). Omit the option, or use `0`, to disable polling.
113
+ - `triggerKey`: a publisher whose `invalidate()` runs the GET again.
79
114
 
80
115
  With a configuration key, use:
81
116
 
@@ -88,6 +123,7 @@ With a configuration key, use:
88
123
  - when the component connects and configuration is available;
89
124
  - when a property used in `${...}` changes;
90
125
  - when a configuration publisher changes;
126
+ - when the `triggerKey` publisher is invalidated;
91
127
  - after each response when `refetchEveryMs` is set.
92
128
 
93
129
  See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path) for
@@ -109,6 +145,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
109
145
  <template>
110
146
  <docs-demo-sources for="demo-api-get"></docs-demo-sources>
111
147
  <demo-api-get></demo-api-get>
148
+ <docs-demo-sources for="demo-api-get-method"></docs-demo-sources>
149
+ <demo-api-get-method></demo-api-get-method>
112
150
  <docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
113
151
  <demo-api-get-configuration-key></demo-api-get-configuration-key>
114
152
  </template>
@@ -16,14 +16,27 @@ const patchUser = patch(new Endpoint<User>("users/42"), bodyKey);
16
16
  payload?: ApiResult<User>;
17
17
  ```
18
18
 
19
+ The same decorator adapts to a method:
20
+
21
+ ```typescript
22
+ @patch(new Endpoint<User>("users/42"), bodyKey)
23
+ handlePatch(payload?: ApiResult<User>) {
24
+ if (payload?.response?.ok) console.log(payload.result);
25
+ }
26
+ ```
27
+
19
28
  To run the operation again from the component:
20
29
 
21
30
  ```typescript
22
- savePatch() {
23
- patchUser.send(this);
31
+ async savePatch() {
32
+ await patchUser.send(this);
24
33
  }
25
34
  ```
26
35
 
36
+ `send(component)` returns a Promise resolved after the PATCH completes. See
37
+ [@post](#docs/_decorators/post.md/post) for the method-decorator form and the
38
+ `when: "before"` option.
39
+
27
40
  The PATCH is also sent automatically when the body mutates, unless
28
41
  `autoPostOnBodyMutation: false` is set. The available options are described in
29
42
  [@post](#docs/_decorators/post.md/post):
@@ -32,6 +45,7 @@ The PATCH is also sent automatically when the body mutates, unless
32
45
  - `skipIfBodyMissing`
33
46
  - `refetchEveryMs`
34
47
  - `skipEmptyPlaceholder`
48
+ - `triggerKey`
35
49
 
36
50
  Pass explicit API configuration as the third argument:
37
51
 
@@ -17,6 +17,15 @@ const createUser = post(new Endpoint<User>("users"), userBodyKey);
17
17
  payload?: ApiResult<User>;
18
18
  ```
19
19
 
20
+ The same decorator adapts to a method and receives the complete payload:
21
+
22
+ ```typescript
23
+ @post(new Endpoint<User>("users"), userBodyKey)
24
+ handleCreate(payload?: ApiResult<User>) {
25
+ if (payload?.response?.ok) console.log(payload.result);
26
+ }
27
+ ```
28
+
20
29
  The current body is read from `userBodyKey`. By default, the POST is sent when
21
30
  the component connects and after each body mutation. Mutations in the same
22
31
  frame are coalesced into one request.
@@ -32,18 +41,21 @@ Keep the decorator to access `send(component)`. The body is read again when the
32
41
  operation runs.
33
42
 
34
43
  ```typescript
35
- saveAgain() {
36
- createUser.send(this);
44
+ async saveAgain() {
45
+ const payload = await createUser.send(this);
46
+ console.log(payload?.result);
37
47
  }
38
48
  ```
39
49
 
40
- `send()` is useful for “Save” or “Retry” buttons. It can repeat an operation
41
- with side effects.
50
+ `send()` returns a `Promise<ApiResult<T> | undefined>` that resolves after the
51
+ operation completes. It is useful for “Save” or “Retry” buttons and can repeat
52
+ an operation with side effects.
42
53
 
43
54
  ### From a method decorator
44
55
 
45
56
  The send action can decorate a local method. It runs after the method by
46
- default and waits for an eventual Promise to resolve:
57
+ default, waits for an eventual Promise to resolve, and the decorated method
58
+ call waits for the send to complete:
47
59
 
48
60
  ```typescript
49
61
  import { send } from "@supersoniks/concorde/decorators";
@@ -54,12 +66,12 @@ async save() {
54
66
  }
55
67
  ```
56
68
 
57
- Use `{ when: "before" }` when the body must be sent before the method runs:
69
+ Use `{ when: "before" }` when the send must complete before the method runs:
58
70
 
59
71
  ```typescript
60
72
  @send(createUser, { when: "before" })
61
73
  saveBefore() {
62
- // The POST is scheduled before this method runs.
74
+ // The POST has completed before this method runs.
63
75
  }
64
76
  ```
65
77
 
@@ -80,6 +92,7 @@ is provided.
80
92
  repeating the operation is intended.
81
93
  - `skipEmptyPlaceholder`: block the request when a `${...}` placeholder is
82
94
  `""`.
95
+ - `triggerKey`: a publisher whose `invalidate()` sends the POST again.
83
96
 
84
97
  With a configuration key:
85
98
 
@@ -96,6 +109,7 @@ dynamic path details.
96
109
  - after a body mutation, unless `autoPostOnBodyMutation: false`;
97
110
  - when a property used in `${...}` changes;
98
111
  - when a configuration publisher changes;
112
+ - when the `triggerKey` publisher is invalidated;
99
113
  - when `send()` is called;
100
114
  - after each response when `refetchEveryMs` is set.
101
115
 
@@ -115,6 +129,8 @@ import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
115
129
  <template>
116
130
  <docs-demo-sources for="demo-api-post"></docs-demo-sources>
117
131
  <demo-api-post></demo-api-post>
132
+ <docs-demo-sources for="demo-api-post-method"></docs-demo-sources>
133
+ <demo-api-post-method></demo-api-post-method>
118
134
  <docs-demo-sources for="demo-api-post-dynamic"></docs-demo-sources>
119
135
  <demo-api-post-dynamic></demo-api-post-dynamic>
120
136
  </template>
@@ -16,14 +16,27 @@ const updateUser = put(new Endpoint<User>("users/42"), bodyKey);
16
16
  payload?: ApiResult<User>;
17
17
  ```
18
18
 
19
+ The same decorator adapts to a method:
20
+
21
+ ```typescript
22
+ @put(new Endpoint<User>("users/42"), bodyKey)
23
+ handleUpdate(payload?: ApiResult<User>) {
24
+ if (payload?.response?.ok) console.log(payload.result);
25
+ }
26
+ ```
27
+
19
28
  To run the operation again from the component:
20
29
 
21
30
  ```typescript
22
- update() {
23
- updateUser.send(this);
31
+ async update() {
32
+ await updateUser.send(this);
24
33
  }
25
34
  ```
26
35
 
36
+ `send(component)` returns a Promise resolved after the PUT completes. See
37
+ [@post](#docs/_decorators/post.md/post) for the method-decorator form and the
38
+ `when: "before"` option.
39
+
27
40
  The PUT is also sent automatically when the body mutates, unless
28
41
  `autoPostOnBodyMutation: false` is set. See
29
42
  [@post](#docs/_decorators/post.md/post) for the option details:
@@ -32,6 +45,7 @@ The PUT is also sent automatically when the body mutates, unless
32
45
  - `skipIfBodyMissing`
33
46
  - `refetchEveryMs`
34
47
  - `skipEmptyPlaceholder`
48
+ - `triggerKey`
35
49
 
36
50
  Pass explicit API configuration as the third argument:
37
51
 
@@ -222,7 +222,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
222
222
  "demo-publish": [
223
223
  {
224
224
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
225
- line: 29,
225
+ line: 32,
226
226
  label: "demo-publish",
227
227
  },
228
228
  { path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
@@ -230,7 +230,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
230
230
  "demo-subscribe-dynamic": [
231
231
  {
232
232
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
233
- line: 313,
233
+ line: 269,
234
234
  label: "demo-subscribe-dynamic",
235
235
  },
236
236
  { path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
@@ -238,7 +238,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
238
238
  "demo-sub-template": [
239
239
  {
240
240
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
241
- line: 282,
241
+ line: 238,
242
242
  label: "demo-sub-template",
243
243
  },
244
244
  { path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
@@ -246,16 +246,23 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
246
246
  "demo-api-get": [
247
247
  {
248
248
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
249
- line: 83,
249
+ line: 86,
250
250
  label: "demo-api-get",
251
251
  },
252
252
  { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
253
253
  { path: "src/docs/mock-api/router.ts", label: "mock router" },
254
254
  ],
255
+ "demo-api-get-method": [
256
+ {
257
+ path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
258
+ label: "demo-api-get-method",
259
+ },
260
+ { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
261
+ ],
255
262
  "demo-api-get-configuration-key": [
256
263
  {
257
264
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
258
- line: 147,
265
+ line: 127,
259
266
  label: "demo-api-get-configuration-key",
260
267
  },
261
268
  { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
@@ -263,7 +270,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
263
270
  "demo-api-get-publish-subscribe": [
264
271
  {
265
272
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
266
- line: 227,
273
+ line: 178,
267
274
  label: "demo-api-get-publish-subscribe",
268
275
  },
269
276
  { path: "src/docs/example/decorators-demo-geo.ts", label: "geo API" },
@@ -276,6 +283,13 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
276
283
  },
277
284
  { path: "src/docs/mock-api/router.ts", label: "mock router" },
278
285
  ],
286
+ "demo-api-post-method": [
287
+ {
288
+ path: "src/docs/example/decorators-demo-post.ts",
289
+ label: "demo-api-post-method",
290
+ },
291
+ { path: "src/docs/mock-api/router.ts", label: "mock router" },
292
+ ],
279
293
  "demo-api-post-dynamic": [
280
294
  {
281
295
  path: "src/docs/example/decorators-demo-post.ts",
@@ -295,7 +309,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
295
309
  "demo-handle": [
296
310
  {
297
311
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
298
- line: 360,
312
+ line: 316,
299
313
  label: "demo-handle",
300
314
  },
301
315
  { path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
@@ -303,7 +317,7 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
303
317
  "demo-handle-dynamic": [
304
318
  {
305
319
  path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
306
- line: 396,
320
+ line: 351,
307
321
  label: "demo-handle-dynamic",
308
322
  },
309
323
  { path: "src/docs/example/decorators-demo-init.ts", label: "demo data" },
@@ -65,6 +65,12 @@ export const docsDemoPostPublishRequestKey = new DataProviderKey<RegisterRequest
65
65
  export const docsDemoPostTriggerKey = new DataProviderKey<number>(
66
66
  "docsDemoPostTrigger",
67
67
  );
68
+ export const docsDemoPostMethodRequestKey = new DataProviderKey<RegisterRequest>(
69
+ "docsDemoPostMethodRequest",
70
+ );
71
+ export const docsDemoPostMethodTriggerKey = new DataProviderKey<number>(
72
+ "docsDemoPostMethodTrigger",
73
+ );
68
74
  export const docsDemoPostDynamicRequestKey = new DataProviderKey<SessionSyncRequest>(
69
75
  "docsDemoPostDynamicRequest",
70
76
  );
@@ -140,6 +146,70 @@ export class DemoApiPost extends LitElement {
140
146
  }
141
147
  }
142
148
 
149
+ @customElement("demo-api-post-method")
150
+ export class DemoApiPostMethod extends LitElement {
151
+ static styles = [tailwind];
152
+
153
+ @state()
154
+ private payload?: ApiResult<RegisterResult>;
155
+
156
+ connectedCallback() {
157
+ super.connectedCallback();
158
+ set(docsDemoPostMethodRequestKey, {
159
+ email: "method@concorde.local",
160
+ password: "method-password",
161
+ });
162
+ set(docsDemoPostMethodTriggerKey, 0);
163
+ }
164
+
165
+ @post(
166
+ registerPostEndpoint,
167
+ docsDemoPostMethodRequestKey,
168
+ docsDemoReqresApiConfigurationKey,
169
+ { triggerKey: docsDemoPostMethodTriggerKey },
170
+ )
171
+ handlePayload(payload?: ApiResult<RegisterResult>) {
172
+ this.payload = payload;
173
+ }
174
+
175
+ private updateBody() {
176
+ set(docsDemoPostMethodRequestKey, {
177
+ email: `method-${Date.now()}@concorde.local`,
178
+ password: "method-password",
179
+ });
180
+ }
181
+
182
+ private resend() {
183
+ dp(docsDemoPostMethodTriggerKey).invalidate();
184
+ }
185
+
186
+ render() {
187
+ const result = this.payload?.result;
188
+ return html`
189
+ <div class="space-y-3">
190
+ <div class="flex flex-wrap items-center gap-2">
191
+ <span class="text-sm font-medium">✉️ POST function mode</span>
192
+ <sonic-button size="sm" @click=${this.updateBody}>
193
+ Update body &amp; POST
194
+ </sonic-button>
195
+ <sonic-button size="sm" variant="secondary" @click=${this.resend}>
196
+ Invalidate trigger
197
+ </sonic-button>
198
+ </div>
199
+ <p class="text-sm text-neutral-600 dark:text-neutral-400">
200
+ The <code>@post</code> decorator calls
201
+ <code>handlePayload</code> with the complete API payload.
202
+ </p>
203
+ <p class="text-xs text-neutral-500">
204
+ HTTP status:
205
+ <strong>${this.payload?.response?.status ?? "loading…"}</strong>
206
+ · email: <strong>${result?.email ?? "—"}</strong>
207
+ </p>
208
+ </div>
209
+ `;
210
+ }
211
+ }
212
+
143
213
  @customElement("demo-api-post-dynamic")
144
214
  export class DemoApiPostDynamic extends LitElement {
145
215
  static styles = [tailwind];