@supersoniks/concorde 4.7.3 → 4.8.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.
Files changed (152) hide show
  1. package/README.md +1 -1
  2. package/ai/AGENTS.md +4 -0
  3. package/ai/cursor/rules/concorde.mdc +11 -1
  4. package/ai/jetbrains/rules/concorde.md +8 -0
  5. package/ai/skills/concorde/SKILL.md +29 -2
  6. package/ai/skills/concorde-scope/SKILL.md +2 -2
  7. package/build-infos.json +1 -1
  8. package/concorde-core.bundle.js +289 -289
  9. package/concorde-core.es.js +4839 -4546
  10. package/dist/concorde-core.bundle.js +289 -289
  11. package/dist/concorde-core.es.js +4839 -4546
  12. package/dist/docs-mock-api-sw.js +19 -0
  13. package/dist/docs-mock-api-sw.js.map +2 -2
  14. package/dist/robots.txt +2 -0
  15. package/docs/assets/index-wyNMyWT9.js +11196 -0
  16. package/docs/docs-mock-api-sw.js +19 -0
  17. package/docs/docs-mock-api-sw.js.map +2 -2
  18. package/docs/index.html +1 -1
  19. package/docs/robots.txt +2 -0
  20. package/package.json +9 -1
  21. package/public/docs-mock-api-sw.js +19 -0
  22. package/public/docs-mock-api-sw.js.map +2 -2
  23. package/public/robots.txt +2 -0
  24. package/src/core/components/functional/example/example.ts +3 -3
  25. package/src/core/components/ui/captcha/captcha.md +0 -12
  26. package/src/core/components/ui/icon/icon.ts +17 -2
  27. package/src/core/components/ui/menu/menu.ts +12 -3
  28. package/src/core/decorators/api.post.spec.ts +293 -0
  29. package/src/core/decorators/api.spec.ts +7 -14
  30. package/src/core/decorators/api.ts +648 -15
  31. package/src/core/decorators/subscriber/bind.ts +13 -5
  32. package/src/core/decorators/subscriber/dynamicPath.spec.ts +53 -0
  33. package/src/core/decorators/subscriber/dynamicPath.ts +23 -1
  34. package/src/core/decorators/subscriber/handle.ts +3 -1
  35. package/src/core/decorators/subscriber/onAssign.ts +10 -2
  36. package/src/core/decorators/subscriber/publish.ts +12 -2
  37. package/src/core/utils/PublisherProxy.ts +95 -11
  38. package/src/core/utils/api.ts +72 -3
  39. package/src/core/utils/dpOptions.spec.ts +56 -0
  40. package/src/core/utils/endpoint.ts +3 -3
  41. package/src/decorators.ts +17 -1
  42. package/src/docs/_core-concept/dataFlow.md +9 -3
  43. package/src/docs/_decorators/bind.md +2 -2
  44. package/src/docs/_decorators/get.md +13 -4
  45. package/src/docs/_decorators/handle.md +5 -1
  46. package/src/docs/_decorators/on-assign.md +2 -0
  47. package/src/docs/_decorators/patch.md +45 -0
  48. package/src/docs/_decorators/post.md +93 -0
  49. package/src/docs/_decorators/publish.md +1 -1
  50. package/src/docs/_decorators/put.md +43 -0
  51. package/src/docs/_decorators/subscribe.md +4 -1
  52. package/src/docs/_directives/sub.md +1 -1
  53. package/src/docs/_getting-started/my-first-component.md +1 -1
  54. package/src/docs/_misc/api-configuration.md +3 -1
  55. package/src/docs/_misc/dataProviderKey.md +2 -2
  56. package/src/docs/_misc/dynamic-path.md +71 -0
  57. package/src/docs/_misc/endpoint.md +5 -3
  58. package/src/docs/components/docs-demo-sources.ts +102 -3
  59. package/src/docs/components/docs-lit-demo-raw.ts +2 -26
  60. package/src/docs/components/docs-lit-demo.ts +9 -42
  61. package/src/docs/components/docs-source-excerpt.ts +53 -0
  62. package/src/docs/components/docs-source-link.ts +24 -8
  63. package/src/docs/components/docs-source-raw.ts +34 -0
  64. package/src/docs/example/decorators-demo-geo.ts +2 -2
  65. package/src/docs/example/decorators-demo-post.ts +249 -0
  66. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +5 -5
  67. package/src/docs/example/decorators-demo.ts +1 -0
  68. package/src/docs/example/docs-api-config-demos.ts +5 -5
  69. package/src/docs/mock-api/router.ts +20 -0
  70. package/src/docs/navigation/navigation.ts +16 -0
  71. package/src/docs/search/docs-search.json +540 -15
  72. package/src/tsconfig.json +24 -0
  73. package/src/tsconfig.tsbuildinfo +1 -1
  74. package/vite.config.mts +1 -1
  75. package/docs/assets/index-D9pxaQYK.js +0 -7508
  76. package/docs/src/core/components/functional/date/date.md +0 -290
  77. package/docs/src/core/components/functional/fetch/fetch.md +0 -125
  78. package/docs/src/core/components/functional/if/if.md +0 -9
  79. package/docs/src/core/components/functional/list/list.md +0 -65
  80. package/docs/src/core/components/functional/mix/mix.md +0 -41
  81. package/docs/src/core/components/functional/queue/queue.md +0 -72
  82. package/docs/src/core/components/functional/router/router.md +0 -94
  83. package/docs/src/core/components/functional/sdui/default-library.json +0 -108
  84. package/docs/src/core/components/functional/sdui/example.json +0 -99
  85. package/docs/src/core/components/functional/sdui/sdui.md +0 -356
  86. package/docs/src/core/components/functional/states/states.md +0 -87
  87. package/docs/src/core/components/functional/submit/submit.md +0 -114
  88. package/docs/src/core/components/functional/subscriber/subscriber.md +0 -91
  89. package/docs/src/core/components/functional/value/value.md +0 -35
  90. package/docs/src/core/components/ui/alert/alert.md +0 -121
  91. package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -0
  92. package/docs/src/core/components/ui/badge/badge.md +0 -127
  93. package/docs/src/core/components/ui/button/button.md +0 -182
  94. package/docs/src/core/components/ui/captcha/captcha.md +0 -24
  95. package/docs/src/core/components/ui/card/card.md +0 -97
  96. package/docs/src/core/components/ui/divider/divider.md +0 -35
  97. package/docs/src/core/components/ui/form/checkbox/checkbox.md +0 -77
  98. package/docs/src/core/components/ui/form/fieldset/fieldset.md +0 -129
  99. package/docs/src/core/components/ui/form/form-actions/form-actions.md +0 -77
  100. package/docs/src/core/components/ui/form/form-layout/form-layout.md +0 -44
  101. package/docs/src/core/components/ui/form/input/input.md +0 -142
  102. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +0 -133
  103. package/docs/src/core/components/ui/form/radio/radio.md +0 -57
  104. package/docs/src/core/components/ui/form/select/select.md +0 -71
  105. package/docs/src/core/components/ui/form/switch/switch.md +0 -57
  106. package/docs/src/core/components/ui/form/textarea/textarea.md +0 -65
  107. package/docs/src/core/components/ui/group/group.md +0 -75
  108. package/docs/src/core/components/ui/icon/icon.md +0 -125
  109. package/docs/src/core/components/ui/icon/icons.json +0 -1
  110. package/docs/src/core/components/ui/image/image.md +0 -107
  111. package/docs/src/core/components/ui/link/link.md +0 -43
  112. package/docs/src/core/components/ui/loader/loader.md +0 -55
  113. package/docs/src/core/components/ui/menu/menu.md +0 -329
  114. package/docs/src/core/components/ui/modal/modal.md +0 -119
  115. package/docs/src/core/components/ui/pop/pop.md +0 -96
  116. package/docs/src/core/components/ui/progress/progress.md +0 -63
  117. package/docs/src/core/components/ui/table/table.md +0 -455
  118. package/docs/src/core/components/ui/toast/toast.md +0 -166
  119. package/docs/src/core/components/ui/tooltip/tooltip.md +0 -82
  120. package/docs/src/docs/_core-concept/dataFlow.md +0 -73
  121. package/docs/src/docs/_core-concept/overview.md +0 -57
  122. package/docs/src/docs/_core-concept/subscriber.md +0 -75
  123. package/docs/src/docs/_decorators/ancestor-attribute.md +0 -79
  124. package/docs/src/docs/_decorators/auto-subscribe.md +0 -202
  125. package/docs/src/docs/_decorators/bind.md +0 -167
  126. package/docs/src/docs/_decorators/get.md +0 -68
  127. package/docs/src/docs/_decorators/handle.md +0 -171
  128. package/docs/src/docs/_decorators/on-assign.md +0 -388
  129. package/docs/src/docs/_decorators/publish.md +0 -55
  130. package/docs/src/docs/_decorators/subscribe.md +0 -97
  131. package/docs/src/docs/_decorators/wait-for-ancestors.md +0 -163
  132. package/docs/src/docs/_directives/sub.md +0 -91
  133. package/docs/src/docs/_getting-started/ai-agents.md +0 -56
  134. package/docs/src/docs/_getting-started/concorde-manual-install.md +0 -133
  135. package/docs/src/docs/_getting-started/concorde-outside.md +0 -33
  136. package/docs/src/docs/_getting-started/create-a-component.md +0 -139
  137. package/docs/src/docs/_getting-started/my-first-component.md +0 -236
  138. package/docs/src/docs/_getting-started/my-first-subscriber.md +0 -120
  139. package/docs/src/docs/_getting-started/pubsub.md +0 -37
  140. package/docs/src/docs/_getting-started/start.md +0 -47
  141. package/docs/src/docs/_getting-started/theming.md +0 -91
  142. package/docs/src/docs/_misc/api-configuration.md +0 -79
  143. package/docs/src/docs/_misc/dataProviderKey.md +0 -168
  144. package/docs/src/docs/_misc/docs-mock-api.md +0 -60
  145. package/docs/src/docs/_misc/endpoint.md +0 -43
  146. package/docs/src/docs/_misc/html-integration.md +0 -13
  147. package/docs/src/docs/search/docs-search.json +0 -8532
  148. package/docs/src/tag-list.json +0 -1
  149. package/docs/src/tsconfig-model.json +0 -23
  150. package/docs/src/tsconfig.json +0 -1050
  151. package/php/get-challenge.php +0 -34
  152. package/php/some-service.php +0 -42
@@ -8,11 +8,12 @@ import HTML, {
8
8
  } from "@supersoniks/concorde/core/utils/HTML";
9
9
  import API, {
10
10
  APIConfiguration,
11
- type ApiGetResult,
11
+ type ApiResult,
12
12
  } from "@supersoniks/concorde/core/utils/api";
13
13
  import DataProvider from "../utils/PublisherProxy";
14
14
  import { ConnectedComponent, setSubscribable } from "./subscriber/common";
15
15
  import {
16
+ type DynamicPathOptions,
16
17
  extractDynamicDependencies,
17
18
  resolveDynamicPath,
18
19
  } from "./subscriber/dynamicPath";
@@ -77,6 +78,22 @@ function isLitHost(component: unknown): component is LitHost {
77
78
  );
78
79
  }
79
80
 
81
+ /**
82
+ * Regroupe plusieurs déclencheurs synchrones (mutations `onInternalMutation`,
83
+ * `rebindAll`, etc.) en un seul appel par frame — même principe que
84
+ * `dynamicPropertyWatch.ts` (`scheduleObservedPropertyChanges`).
85
+ */
86
+ function createAnimationFrameCoalescedRunner(run: () => void): () => void {
87
+ let frameId: number | null = null;
88
+ return () => {
89
+ if (frameId !== null) return;
90
+ frameId = requestAnimationFrame(() => {
91
+ frameId = null;
92
+ run();
93
+ });
94
+ };
95
+ }
96
+
80
97
  function scheduleAfterHostReady(
81
98
  component: unknown,
82
99
  callback: () => void,
@@ -90,8 +107,9 @@ function scheduleAfterHostReady(
90
107
  cancelled = true;
91
108
  };
92
109
  }
93
- const rafId = requestAnimationFrame(() => callback());
94
- return () => cancelAnimationFrame(rafId);
110
+ // HTMLElement classique : config ancêtre déjà lisible ; sinon watchScopedConfiguration.
111
+ callback();
112
+ return () => {};
95
113
  }
96
114
 
97
115
  /**
@@ -133,7 +151,8 @@ function watchScopedConfiguration(
133
151
  attributeFilter: [...SCOPE_WATCH_ATTRIBUTES],
134
152
  });
135
153
  }
136
- node = (node.parentNode || (node as ShadowRoot).host) as SearchableDomElement;
154
+ node = (node.parentNode ||
155
+ (node as ShadowRoot).host) as SearchableDomElement;
137
156
  }
138
157
  cleanups.push(() => observer.disconnect());
139
158
 
@@ -161,7 +180,7 @@ function detachConfigPublisher(state: ApiGetState): void {
161
180
 
162
181
  /**
163
182
  * Décorateur **`@get`** : charge des données via `API.getDetailed` et assigne un
164
- * `ApiGetResult<T>` (`request`, `response`, `result` typé `T`) ou `null`.
183
+ * `ApiResult<T>` (`request`, `response`, `result` typé `T`) ou `null`.
165
184
  * Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
166
185
  *
167
186
  * **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
@@ -174,19 +193,19 @@ function detachConfigPublisher(state: ApiGetState): void {
174
193
  *
175
194
  * @example
176
195
  * @get(new Endpoint<User, { userId: string }>("users/${userId}"))
177
- * payload?: ApiGetResult<User>;
196
+ * payload?: ApiResult<User>;
178
197
  *
179
198
  * @example
180
199
  * const apiConf = new DataProviderKey<APIConfiguration>("myApiConf");
181
200
  * PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... });
182
201
  * @get(new Endpoint<Thing, { id: string }>("things/${id}"), apiConf)
183
- * payload?: ApiGetResult<Thing>;
202
+ * payload?: ApiResult<Thing>;
184
203
  */
185
204
  export function get<T, Ue = any>(
186
205
  endpoint: Endpoint<T, Ue>,
187
206
  ): <K extends string>(
188
207
  target: DataProviderKeyHost<Ue> & {
189
- [P in K]?: ApiGetResult<T> | null | undefined;
208
+ [P in K]?: ApiResult<T> | null | undefined;
190
209
  },
191
210
  propertyKey: K,
192
211
  ) => void;
@@ -196,18 +215,50 @@ export function get<T, Ue = any, Uk = any>(
196
215
  ): <K extends string>(
197
216
  target: DataProviderKeyHost<Ue> &
198
217
  DataProviderKeyHost<Uk> & {
199
- [P in K]?: ApiGetResult<T> | null | undefined;
218
+ [P in K]?: ApiResult<T> | null | undefined;
200
219
  },
201
220
  propertyKey: K,
202
221
  ) => void;
203
222
  export function get<T, Ue = any, Uk = any>(
204
223
  endpoint: Endpoint<T, Ue>,
205
- configurationKey?: DataProviderKey<APIConfiguration, Uk>,
224
+ options: GetOptions,
206
225
  ): <K extends string>(
207
- target: object & { [P in K]?: ApiGetResult<T> | null | undefined },
226
+ target: DataProviderKeyHost<Ue> & {
227
+ [P in K]?: ApiResult<T> | null | undefined;
228
+ },
229
+ propertyKey: K,
230
+ ) => void;
231
+ export function get<T, Ue = any, Uk = any, Ut = any>(
232
+ endpoint: Endpoint<T, Ue>,
233
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
234
+ options: GetOptions,
235
+ ): <K extends string>(
236
+ target: DataProviderKeyHost<Ue> &
237
+ DataProviderKeyHost<Uk> & {
238
+ [P in K]?: ApiResult<T> | null | undefined;
239
+ },
240
+ propertyKey: K,
241
+ ) => void;
242
+ export function get<T, Ue = any, Uk = any, _Ut = any>(
243
+ endpoint: Endpoint<T, Ue>,
244
+ configurationKeyOrOptions?:
245
+ | DataProviderKey<APIConfiguration, Uk>
246
+ | GetOptions,
247
+ maybeOptions?: GetOptions,
248
+ ): <K extends string>(
249
+ target: object & { [P in K]?: ApiResult<T> | null | undefined },
208
250
  propertyKey: K,
209
251
  ) => void {
210
252
  const pathTemplate = endpoint.path;
253
+ const configurationKey = isGetOptions(configurationKeyOrOptions)
254
+ ? undefined
255
+ : configurationKeyOrOptions;
256
+ const options = isGetOptions(configurationKeyOrOptions)
257
+ ? configurationKeyOrOptions
258
+ : maybeOptions;
259
+ const pathOptions: DynamicPathOptions = {
260
+ skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
261
+ };
211
262
  const configurationKeyPath = configurationKey?.path;
212
263
  const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
213
264
  const configKeyDynamicDependencies = configurationKeyPath
@@ -247,7 +298,7 @@ export function get<T, Ue = any, Uk = any>(
247
298
 
248
299
  const runFetch = () => {
249
300
  const resolution = isDynamicPath
250
- ? resolveDynamicPath(component, pathTemplate)
301
+ ? resolveDynamicPath(component, pathTemplate, pathOptions)
251
302
  : { ready: true, path: pathTemplate };
252
303
  if (!resolution.ready || !resolution.path) {
253
304
  comp[propertyKey] = undefined;
@@ -255,7 +306,11 @@ export function get<T, Ue = any, Uk = any>(
255
306
  }
256
307
  let config: APIConfiguration | null = null;
257
308
  if (usesPublisherConfig && configurationKeyPath) {
258
- const configRes = resolveDynamicPath(component, configurationKeyPath);
309
+ const configRes = resolveDynamicPath(
310
+ component,
311
+ configurationKeyPath,
312
+ pathOptions,
313
+ );
259
314
  if (!configRes.ready || !configRes.path) {
260
315
  comp[propertyKey] = undefined;
261
316
  return;
@@ -280,7 +335,7 @@ export function get<T, Ue = any, Uk = any>(
280
335
  const api = new API(config);
281
336
  void api
282
337
  .getDetailed<T>(resolution.path)
283
- .then((payload?: ApiGetResult<T>) => {
338
+ .then((payload?: ApiResult<T>) => {
284
339
  if (generation !== state.requestGeneration) return;
285
340
  comp[propertyKey] = payload;
286
341
  });
@@ -289,7 +344,11 @@ export function get<T, Ue = any, Uk = any>(
289
344
  const rebindPublisherConfig = () => {
290
345
  if (!usesPublisherConfig || !configurationKeyPath) return;
291
346
  detachConfigPublisher(state);
292
- const configRes = resolveDynamicPath(component, configurationKeyPath);
347
+ const configRes = resolveDynamicPath(
348
+ component,
349
+ configurationKeyPath,
350
+ pathOptions,
351
+ );
293
352
  if (!configRes.ready || !configRes.path) {
294
353
  comp[propertyKey] = undefined;
295
354
  return;
@@ -353,3 +412,577 @@ export function get<T, Ue = any, Uk = any>(
353
412
  });
354
413
  };
355
414
  }
415
+
416
+ export type GetOptions = DynamicPathOptions;
417
+
418
+ export type ApiSendOptions<Uk = any> = DynamicPathOptions & {
419
+ /** Intervalle de renvoi automatique (ms). 0 ou absent = pas de polling. */
420
+ refetchEveryMs?: number;
421
+ /** Ne pas envoyer si le body publisher est null/undefined (défaut: true). */
422
+ skipIfBodyMissing?: boolean;
423
+ /**
424
+ * Relancer l'envoi quand le body publisher mute (défaut: true).
425
+ * `false` : le body est lu au moment de l'envoi ; déclencher via `triggerKey.invalidate()`.
426
+ */
427
+ autoPostOnBodyMutation?: boolean;
428
+ /** Publisher dont `invalidate()` relance l'envoi (relit le body courant). */
429
+ triggerKey?: DataProviderKey<unknown, Uk>;
430
+ };
431
+
432
+ /** @deprecated Alias — préférer `ApiSendOptions`. */
433
+ export type PostOptions<Uk = any> = ApiSendOptions<Uk>;
434
+ export type PutOptions<Uk = any> = ApiSendOptions<Uk>;
435
+ export type PatchOptions<Uk = any> = ApiSendOptions<Uk>;
436
+
437
+ function isGetOptions(value: unknown): value is GetOptions {
438
+ if (!value || typeof value !== "object") return false;
439
+ return "skipEmptyPlaceholder" in value;
440
+ }
441
+
442
+ function isApiSendOptions(value: unknown): value is ApiSendOptions {
443
+ if (!value || typeof value !== "object") return false;
444
+ return (
445
+ "refetchEveryMs" in value ||
446
+ "skipIfBodyMissing" in value ||
447
+ "autoPostOnBodyMutation" in value ||
448
+ "skipEmptyPlaceholder" in value ||
449
+ "triggerKey" in value
450
+ );
451
+ }
452
+
453
+ type ApiSendState = ApiGetState & {
454
+ bodyPublisher: DataProvider | null;
455
+ bodyMutationHandler: (() => void) | null;
456
+ triggerPublisher: DataProvider | null;
457
+ triggerInvalidateHandler: (() => void) | null;
458
+ refetchTimeoutId: ReturnType<typeof setTimeout> | null;
459
+ refetchEveryMs: number;
460
+ skipIfBodyMissing: boolean;
461
+ autoPostOnBodyMutation: boolean;
462
+ };
463
+
464
+ function detachBodyPublisher(state: ApiSendState): void {
465
+ if (state.bodyPublisher && state.bodyMutationHandler) {
466
+ state.bodyPublisher.offInternalMutation(state.bodyMutationHandler);
467
+ }
468
+ state.bodyPublisher = null;
469
+ state.bodyMutationHandler = null;
470
+ }
471
+
472
+ function detachTriggerPublisher(state: ApiSendState): void {
473
+ if (state.triggerPublisher && state.triggerInvalidateHandler) {
474
+ state.triggerPublisher.offInvalidate(state.triggerInvalidateHandler);
475
+ }
476
+ state.triggerPublisher = null;
477
+ state.triggerInvalidateHandler = null;
478
+ }
479
+
480
+ function clearSendRefetch(state: ApiSendState): void {
481
+ if (state.refetchTimeoutId) {
482
+ clearTimeout(state.refetchTimeoutId);
483
+ state.refetchTimeoutId = null;
484
+ }
485
+ }
486
+
487
+ function readBodyFromPublisher(publisher: DataProvider | null): unknown {
488
+ if (!publisher || typeof publisher.get !== "function") return undefined;
489
+ return publisher.get();
490
+ }
491
+
492
+ type ApiSendMethod = "POST" | "PUT" | "PATCH";
493
+
494
+ type ApiSendDetailedCaller = (
495
+ api: API,
496
+ path: string,
497
+ body: unknown,
498
+ ) => Promise<ApiResult<unknown> | undefined>;
499
+
500
+ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
501
+ method: ApiSendMethod,
502
+ callDetailed: ApiSendDetailedCaller,
503
+ ) {
504
+ return function (
505
+ endpoint: Endpoint<T, Ue>,
506
+ bodyKey: DataProviderKey<B>,
507
+ configurationKeyOrOptions?:
508
+ | DataProviderKey<APIConfiguration, Uk>
509
+ | ApiSendOptions<Ut>,
510
+ maybeOptions?: ApiSendOptions<Ut>,
511
+ ): <K extends string>(
512
+ target: object & { [P in K]?: ApiResult<T> | null | undefined },
513
+ propertyKey: K,
514
+ ) => void {
515
+ const pathTemplate = endpoint.path;
516
+ const bodyKeyPath = bodyKey.path;
517
+ const configurationKey = isApiSendOptions(configurationKeyOrOptions)
518
+ ? undefined
519
+ : configurationKeyOrOptions;
520
+ const options = isApiSendOptions(configurationKeyOrOptions)
521
+ ? configurationKeyOrOptions
522
+ : maybeOptions;
523
+ const configurationKeyPath = configurationKey?.path;
524
+ const triggerKeyPath = options?.triggerKey?.path;
525
+ const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
526
+ const bodyKeyDynamicDependencies = extractDynamicDependencies(bodyKeyPath);
527
+ const configKeyDynamicDependencies = configurationKeyPath
528
+ ? extractDynamicDependencies(configurationKeyPath)
529
+ : [];
530
+ const triggerKeyDynamicDependencies = triggerKeyPath
531
+ ? extractDynamicDependencies(triggerKeyPath)
532
+ : [];
533
+ const mergedDynamicDependencies = [
534
+ ...new Set([
535
+ ...endpointDynamicDependencies,
536
+ ...bodyKeyDynamicDependencies,
537
+ ...configKeyDynamicDependencies,
538
+ ...triggerKeyDynamicDependencies,
539
+ ]),
540
+ ];
541
+ const isDynamicPath = endpointDynamicDependencies.length > 0;
542
+ const usesPublisherConfig = Boolean(configurationKeyPath);
543
+ const refetchEveryMs = options?.refetchEveryMs ?? 0;
544
+ const skipIfBodyMissing = options?.skipIfBodyMissing ?? true;
545
+ const autoPostOnBodyMutation = options?.autoPostOnBodyMutation ?? true;
546
+ const pathOptions: DynamicPathOptions = {
547
+ skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
548
+ };
549
+ const stateKeyPrefix = `__${method.toLowerCase()}_state_`;
550
+
551
+ return function (target: object, propertyKey: string) {
552
+ if (!target) return;
553
+ setSubscribable(target);
554
+ const stateKey = `${stateKeyPrefix}${propertyKey}`;
555
+
556
+ (target as ConnectedComponent).__onConnected__((component) => {
557
+ const comp = component as Record<string, unknown>;
558
+ let state = comp[stateKey] as ApiSendState | undefined;
559
+ if (!state) {
560
+ state = {
561
+ cleanupWatchers: [],
562
+ requestGeneration: 0,
563
+ configPublisher: null,
564
+ configMutationHandler: null,
565
+ scopeWatchCleanup: null,
566
+ bodyPublisher: null,
567
+ bodyMutationHandler: null,
568
+ triggerPublisher: null,
569
+ triggerInvalidateHandler: null,
570
+ refetchTimeoutId: null,
571
+ refetchEveryMs,
572
+ skipIfBodyMissing,
573
+ autoPostOnBodyMutation,
574
+ };
575
+ comp[stateKey] = state;
576
+ }
577
+
578
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
579
+ state.cleanupWatchers = [];
580
+ state.requestGeneration++;
581
+ state.refetchEveryMs = refetchEveryMs;
582
+ state.skipIfBodyMissing = skipIfBodyMissing;
583
+ state.autoPostOnBodyMutation = autoPostOnBodyMutation;
584
+ clearSendRefetch(state);
585
+
586
+ const scheduleRefetch = () => {
587
+ clearSendRefetch(state);
588
+ if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
589
+ state.refetchTimeoutId = setTimeout(() => {
590
+ state.refetchTimeoutId = null;
591
+ runSend();
592
+ }, state.refetchEveryMs);
593
+ };
594
+
595
+ const runSend = () => {
596
+ const resolution = isDynamicPath
597
+ ? resolveDynamicPath(component, pathTemplate, pathOptions)
598
+ : { ready: true, path: pathTemplate };
599
+ if (!resolution.ready || !resolution.path) {
600
+ comp[propertyKey] = undefined;
601
+ scheduleRefetch();
602
+ return;
603
+ }
604
+
605
+ const bodyResolution = resolveDynamicPath(
606
+ component,
607
+ bodyKeyPath,
608
+ pathOptions,
609
+ );
610
+ if (!bodyResolution.ready || !bodyResolution.path) {
611
+ comp[propertyKey] = undefined;
612
+ scheduleRefetch();
613
+ return;
614
+ }
615
+
616
+ const bodyPublisher = getPublisherFromPath(bodyResolution.path);
617
+ const body = readBodyFromPublisher(bodyPublisher);
618
+ if (
619
+ state.skipIfBodyMissing &&
620
+ (body === null || body === undefined)
621
+ ) {
622
+ scheduleRefetch();
623
+ return;
624
+ }
625
+
626
+ let config: APIConfiguration | null = null;
627
+ if (usesPublisherConfig && configurationKeyPath) {
628
+ const configRes = resolveDynamicPath(
629
+ component,
630
+ configurationKeyPath,
631
+ pathOptions,
632
+ );
633
+ if (!configRes.ready || !configRes.path) {
634
+ comp[propertyKey] = undefined;
635
+ scheduleRefetch();
636
+ return;
637
+ }
638
+ const configPublisher = getPublisherFromPath(configRes.path);
639
+ config = readApiConfigurationFromPublisher(configPublisher);
640
+ } else {
641
+ config = resolveScopedConfiguration(component);
642
+ }
643
+
644
+ if (!isScopedConfigurationReady(config)) {
645
+ if (!usesPublisherConfig && !state.scopeWatchCleanup) {
646
+ const scopeWatch = watchScopedConfiguration(component, runSend);
647
+ state.scopeWatchCleanup = scopeWatch;
648
+ state.cleanupWatchers.push(() => {
649
+ scopeWatch();
650
+ state.scopeWatchCleanup = null;
651
+ });
652
+ }
653
+ scheduleRefetch();
654
+ return;
655
+ }
656
+
657
+ const generation = ++state.requestGeneration;
658
+ const api = new API(config);
659
+ void callDetailed(api, resolution.path, body).then(
660
+ (payload?: ApiResult<unknown>) => {
661
+ if (generation !== state.requestGeneration) return;
662
+ comp[propertyKey] = payload;
663
+ scheduleRefetch();
664
+ },
665
+ );
666
+ };
667
+
668
+ const scheduleSend = createAnimationFrameCoalescedRunner(runSend);
669
+
670
+ const rebindBodyPublisher = () => {
671
+ detachBodyPublisher(state);
672
+ if (!state.autoPostOnBodyMutation) {
673
+ return;
674
+ }
675
+ const bodyResolution = resolveDynamicPath(
676
+ component,
677
+ bodyKeyPath,
678
+ pathOptions,
679
+ );
680
+ if (!bodyResolution.ready || !bodyResolution.path) {
681
+ return;
682
+ }
683
+ const publisher = getPublisherFromPath(bodyResolution.path);
684
+ if (!publisher) return;
685
+ const mutationHandler = () => scheduleSend();
686
+ publisher.onInternalMutation(mutationHandler);
687
+ state.bodyPublisher = publisher;
688
+ state.bodyMutationHandler = mutationHandler;
689
+ };
690
+
691
+ const rebindTriggerPublisher = () => {
692
+ detachTriggerPublisher(state);
693
+ if (!triggerKeyPath) return;
694
+ const triggerResolution = resolveDynamicPath(
695
+ component,
696
+ triggerKeyPath,
697
+ pathOptions,
698
+ );
699
+ if (!triggerResolution.ready || !triggerResolution.path) {
700
+ return;
701
+ }
702
+ const publisher = getPublisherFromPath(triggerResolution.path);
703
+ if (!publisher) return;
704
+ const invalidateHandler = () => scheduleSend();
705
+ publisher.onInvalidate(invalidateHandler);
706
+ state.triggerPublisher = publisher;
707
+ state.triggerInvalidateHandler = invalidateHandler;
708
+ };
709
+
710
+ const rebindPublisherConfig = () => {
711
+ if (!usesPublisherConfig || !configurationKeyPath) return;
712
+ detachConfigPublisher(state);
713
+ const configRes = resolveDynamicPath(
714
+ component,
715
+ configurationKeyPath,
716
+ pathOptions,
717
+ );
718
+ if (!configRes.ready || !configRes.path) {
719
+ comp[propertyKey] = undefined;
720
+ return;
721
+ }
722
+ const publisher = getPublisherFromPath(configRes.path);
723
+ if (!publisher) {
724
+ comp[propertyKey] = undefined;
725
+ return;
726
+ }
727
+ const mutationHandler = () => scheduleSend();
728
+ publisher.onInternalMutation(mutationHandler);
729
+ state.configPublisher = publisher;
730
+ state.configMutationHandler = mutationHandler;
731
+ };
732
+
733
+ const rebindAll = () => {
734
+ rebindBodyPublisher();
735
+ rebindTriggerPublisher();
736
+ rebindPublisherConfig();
737
+ if (state.autoPostOnBodyMutation) {
738
+ scheduleSend();
739
+ }
740
+ };
741
+
742
+ if (
743
+ usesPublisherConfig ||
744
+ bodyKeyDynamicDependencies.length ||
745
+ triggerKeyDynamicDependencies.length
746
+ ) {
747
+ for (const dependency of mergedDynamicDependencies) {
748
+ const unsubscribe = observeDynamicProperty(
749
+ getDynamicWatchKeys.watcherStore,
750
+ getDynamicWatchKeys.hooked,
751
+ component,
752
+ dependency,
753
+ () => rebindAll(),
754
+ );
755
+ state.cleanupWatchers.push(unsubscribe);
756
+ }
757
+ rebindAll();
758
+ } else {
759
+ const startScopedSend = () => {
760
+ if (isDynamicPath) {
761
+ for (const dependency of endpointDynamicDependencies) {
762
+ const unsubscribe = observeDynamicProperty(
763
+ getDynamicWatchKeys.watcherStore,
764
+ getDynamicWatchKeys.hooked,
765
+ component,
766
+ dependency,
767
+ () => scheduleSend(),
768
+ );
769
+ state.cleanupWatchers.push(unsubscribe);
770
+ }
771
+ }
772
+ rebindBodyPublisher();
773
+ rebindTriggerPublisher();
774
+ if (state.autoPostOnBodyMutation) {
775
+ scheduleSend();
776
+ }
777
+ };
778
+ state.cleanupWatchers.push(
779
+ scheduleAfterHostReady(component, startScopedSend),
780
+ );
781
+ }
782
+ });
783
+
784
+ (target as ConnectedComponent).__onDisconnected__((component) => {
785
+ const comp = component as Record<string, unknown>;
786
+ const state = comp[stateKey] as ApiSendState | undefined;
787
+ if (!state) return;
788
+ detachConfigPublisher(state);
789
+ detachBodyPublisher(state);
790
+ detachTriggerPublisher(state);
791
+ clearSendRefetch(state);
792
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
793
+ state.cleanupWatchers = [];
794
+ state.requestGeneration++;
795
+ comp[propertyKey] = undefined;
796
+ });
797
+ };
798
+ };
799
+ }
800
+
801
+ /**
802
+ * Décorateur **`@post`** : envoie des données via `API.postDetailed` et assigne un
803
+ * `ApiResult<T>` (`request`, `response`, `result` typé `T`).
804
+ *
805
+ * Le body est lu depuis un `DataProviderKey` (mutations et assignations relancent le POST).
806
+ *
807
+ * @example
808
+ * @post(new Endpoint<SyncResult, { sessionId: string }>("sessions/${sessionId}/sync"), syncRequestKey)
809
+ * payload?: ApiResult<SyncResult>;
810
+ */
811
+ export function post<T, B, Ue = any>(
812
+ endpoint: Endpoint<T, Ue>,
813
+ bodyKey: DataProviderKey<B>,
814
+ ): <K extends string>(
815
+ target: DataProviderKeyHost<Ue> & {
816
+ [P in K]?: ApiResult<T> | null | undefined;
817
+ },
818
+ propertyKey: K,
819
+ ) => void;
820
+ export function post<T, B, Ue = any, Uk = any>(
821
+ endpoint: Endpoint<T, Ue>,
822
+ bodyKey: DataProviderKey<B>,
823
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
824
+ ): <K extends string>(
825
+ target: DataProviderKeyHost<Ue> &
826
+ DataProviderKeyHost<Uk> & {
827
+ [P in K]?: ApiResult<T> | null | undefined;
828
+ },
829
+ propertyKey: K,
830
+ ) => void;
831
+ export function post<T, B, Ue = any, Uk = any>(
832
+ endpoint: Endpoint<T, Ue>,
833
+ bodyKey: DataProviderKey<B>,
834
+ options: PostOptions<Uk>,
835
+ ): <K extends string>(
836
+ target: DataProviderKeyHost<Ue> & {
837
+ [P in K]?: ApiResult<T> | null | undefined;
838
+ },
839
+ propertyKey: K,
840
+ ) => void;
841
+ export function post<T, B, Ue = any, Uk = any, Ut = any>(
842
+ endpoint: Endpoint<T, Ue>,
843
+ bodyKey: DataProviderKey<B>,
844
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
845
+ options: PostOptions<Ut>,
846
+ ): <K extends string>(
847
+ target: DataProviderKeyHost<Ue> &
848
+ DataProviderKeyHost<Uk> & {
849
+ [P in K]?: ApiResult<T> | null | undefined;
850
+ },
851
+ propertyKey: K,
852
+ ) => void;
853
+ export function post<T, B, Ue = any, Uk = any, Ut = any>(
854
+ endpoint: Endpoint<T, Ue>,
855
+ bodyKey: DataProviderKey<B>,
856
+ configurationKeyOrOptions?:
857
+ | DataProviderKey<APIConfiguration, Uk>
858
+ | PostOptions<Ut>,
859
+ maybeOptions?: PostOptions<Ut>,
860
+ ): <K extends string>(
861
+ target: object & { [P in K]?: ApiResult<T> | null | undefined },
862
+ propertyKey: K,
863
+ ) => void {
864
+ return createApiSendDecorator<T, B, Ue, Uk, Ut>(
865
+ "POST",
866
+ (api, path, body) => api.postDetailed<T, B>(path, body as B),
867
+ )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);
868
+ }
869
+
870
+ /** `@put` — même modèle que `@post` via `API.putDetailed`. */
871
+ export function put<T, B, Ue = any>(
872
+ endpoint: Endpoint<T, Ue>,
873
+ bodyKey: DataProviderKey<B>,
874
+ ): <K extends string>(
875
+ target: DataProviderKeyHost<Ue> & {
876
+ [P in K]?: ApiResult<T> | null | undefined;
877
+ },
878
+ propertyKey: K,
879
+ ) => void;
880
+ export function put<T, B, Ue = any, Uk = any>(
881
+ endpoint: Endpoint<T, Ue>,
882
+ bodyKey: DataProviderKey<B>,
883
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
884
+ ): <K extends string>(
885
+ target: DataProviderKeyHost<Ue> &
886
+ DataProviderKeyHost<Uk> & {
887
+ [P in K]?: ApiResult<T> | null | undefined;
888
+ },
889
+ propertyKey: K,
890
+ ) => void;
891
+ export function put<T, B, Ue = any, Uk = any>(
892
+ endpoint: Endpoint<T, Ue>,
893
+ bodyKey: DataProviderKey<B>,
894
+ options: PutOptions<Uk>,
895
+ ): <K extends string>(
896
+ target: DataProviderKeyHost<Ue> & {
897
+ [P in K]?: ApiResult<T> | null | undefined;
898
+ },
899
+ propertyKey: K,
900
+ ) => void;
901
+ export function put<T, B, Ue = any, Uk = any, Ut = any>(
902
+ endpoint: Endpoint<T, Ue>,
903
+ bodyKey: DataProviderKey<B>,
904
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
905
+ options: PutOptions<Ut>,
906
+ ): <K extends string>(
907
+ target: DataProviderKeyHost<Ue> &
908
+ DataProviderKeyHost<Uk> & {
909
+ [P in K]?: ApiResult<T> | null | undefined;
910
+ },
911
+ propertyKey: K,
912
+ ) => void;
913
+ export function put<T, B, Ue = any, Uk = any, Ut = any>(
914
+ endpoint: Endpoint<T, Ue>,
915
+ bodyKey: DataProviderKey<B>,
916
+ configurationKeyOrOptions?:
917
+ | DataProviderKey<APIConfiguration, Uk>
918
+ | PutOptions<Ut>,
919
+ maybeOptions?: PutOptions<Ut>,
920
+ ): <K extends string>(
921
+ target: object & { [P in K]?: ApiResult<T> | null | undefined },
922
+ propertyKey: K,
923
+ ) => void {
924
+ return createApiSendDecorator<T, B, Ue, Uk, Ut>(
925
+ "PUT",
926
+ (api, path, body) => api.putDetailed<T, B>(path, body as B),
927
+ )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);
928
+ }
929
+
930
+ /** `@patch` — même modèle que `@post` via `API.patchDetailed`. */
931
+ export function patch<T, B, Ue = any>(
932
+ endpoint: Endpoint<T, Ue>,
933
+ bodyKey: DataProviderKey<B>,
934
+ ): <K extends string>(
935
+ target: DataProviderKeyHost<Ue> & {
936
+ [P in K]?: ApiResult<T> | null | undefined;
937
+ },
938
+ propertyKey: K,
939
+ ) => void;
940
+ export function patch<T, B, Ue = any, Uk = any>(
941
+ endpoint: Endpoint<T, Ue>,
942
+ bodyKey: DataProviderKey<B>,
943
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
944
+ ): <K extends string>(
945
+ target: DataProviderKeyHost<Ue> &
946
+ DataProviderKeyHost<Uk> & {
947
+ [P in K]?: ApiResult<T> | null | undefined;
948
+ },
949
+ propertyKey: K,
950
+ ) => void;
951
+ export function patch<T, B, Ue = any, Uk = any>(
952
+ endpoint: Endpoint<T, Ue>,
953
+ bodyKey: DataProviderKey<B>,
954
+ options: PatchOptions<Uk>,
955
+ ): <K extends string>(
956
+ target: DataProviderKeyHost<Ue> & {
957
+ [P in K]?: ApiResult<T> | null | undefined;
958
+ },
959
+ propertyKey: K,
960
+ ) => void;
961
+ export function patch<T, B, Ue = any, Uk = any, Ut = any>(
962
+ endpoint: Endpoint<T, Ue>,
963
+ bodyKey: DataProviderKey<B>,
964
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
965
+ options: PatchOptions<Ut>,
966
+ ): <K extends string>(
967
+ target: DataProviderKeyHost<Ue> &
968
+ DataProviderKeyHost<Uk> & {
969
+ [P in K]?: ApiResult<T> | null | undefined;
970
+ },
971
+ propertyKey: K,
972
+ ) => void;
973
+ export function patch<T, B, Ue = any, Uk = any, Ut = any>(
974
+ endpoint: Endpoint<T, Ue>,
975
+ bodyKey: DataProviderKey<B>,
976
+ configurationKeyOrOptions?:
977
+ | DataProviderKey<APIConfiguration, Uk>
978
+ | PatchOptions<Ut>,
979
+ maybeOptions?: PatchOptions<Ut>,
980
+ ): <K extends string>(
981
+ target: object & { [P in K]?: ApiResult<T> | null | undefined },
982
+ propertyKey: K,
983
+ ) => void {
984
+ return createApiSendDecorator<T, B, Ue, Uk, Ut>(
985
+ "PATCH",
986
+ (api, path, body) => api.patchDetailed<T, B>(path, body as B),
987
+ )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);
988
+ }