@supersoniks/concorde 4.9.9 → 4.9.92

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/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +241 -241
  3. package/concorde-core.es.js +2931 -2851
  4. package/dist/concorde-core.bundle.js +241 -241
  5. package/dist/concorde-core.es.js +2931 -2851
  6. package/dist/js/core/decorators/api.js +146 -43
  7. package/dist/js/core/decorators/api.js.map +1 -1
  8. package/dist/js/decorators.js +4 -0
  9. package/dist/js/decorators.js.map +1 -1
  10. package/dist/types/core/decorators/api.d.ts +87 -60
  11. package/dist/types/core/decorators/api.d.ts.map +1 -1
  12. package/dist/types/decorators.d.ts +3 -1
  13. package/dist/types/decorators.d.ts.map +1 -1
  14. package/docs/assets/index-D2Q3JyQ6.css +1 -0
  15. package/docs/assets/{index-hElUgV9_.js → index-DURr6O71.js} +651 -736
  16. package/docs/crawl/docs/_decorators/get.html +89 -81
  17. package/docs/crawl/docs/_decorators/patch.html +43 -23
  18. package/docs/crawl/docs/_decorators/post.html +101 -72
  19. package/docs/crawl/docs/_decorators/put.html +44 -21
  20. package/docs/crawl/docs/_getting-started/ai-agents.html +45 -6
  21. package/docs/crawl/hubs/decorators.html +145 -70
  22. package/docs/crawl/hubs/remote-data.html +145 -70
  23. package/docs/index.html +2 -2
  24. package/docs/llms-full.txt +276 -147
  25. package/docs/llms.txt +4 -4
  26. package/docs/src/core/components/functional/queue/queue.md +0 -0
  27. package/docs/src/core/components/ui/button/button.md +0 -0
  28. package/docs/src/docs/_decorators/get.md +88 -80
  29. package/docs/src/docs/_decorators/patch.md +41 -25
  30. package/docs/src/docs/_decorators/post.md +104 -62
  31. package/docs/src/docs/_decorators/put.md +42 -24
  32. package/docs/src/docs/_getting-started/ai-agents.md +24 -4
  33. package/docs/src/docs/search/docs-search.json +182 -112
  34. package/docs/src/tsconfig.json +270 -267
  35. package/package.json +1 -1
  36. package/src/core/decorators/api.post.spec.ts +79 -1
  37. package/src/core/decorators/api.spec.ts +141 -1
  38. package/src/core/decorators/api.ts +330 -180
  39. package/src/decorators.ts +7 -0
  40. package/src/docs/_decorators/get.md +88 -80
  41. package/src/docs/_decorators/patch.md +41 -25
  42. package/src/docs/_decorators/post.md +104 -62
  43. package/src/docs/_decorators/put.md +42 -24
  44. package/src/docs/components/docs-demo-sources.ts +9 -14
  45. package/src/docs/example/decorators-demo-geo.ts +0 -21
  46. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +42 -133
  47. package/src/docs/search/docs-search.json +128 -133
  48. package/docs/assets/index-Bnp7gVaA.css +0 -1
@@ -83,14 +83,32 @@ function isLitHost(component: unknown): component is LitHost {
83
83
  * `rebindAll`, etc.) en un seul appel par frame — même principe que
84
84
  * `dynamicPropertyWatch.ts` (`scheduleObservedPropertyChanges`).
85
85
  */
86
- function createAnimationFrameCoalescedRunner(run: () => void): () => void {
86
+ function createAnimationFrameCoalescedRunner<T>(
87
+ run: () => Promise<T>,
88
+ ): () => Promise<T> {
87
89
  let frameId: number | null = null;
90
+ let pending: Array<{
91
+ resolve: (value: T) => void;
92
+ reject: (reason: unknown) => void;
93
+ }> = [];
94
+
88
95
  return () => {
89
- if (frameId !== null) return;
96
+ const promise = new Promise<T>((resolve, reject) => {
97
+ pending.push({ resolve, reject });
98
+ });
99
+
100
+ if (frameId !== null) return promise;
90
101
  frameId = requestAnimationFrame(() => {
91
102
  frameId = null;
92
- run();
103
+ const current = pending;
104
+ pending = [];
105
+ void run().then(
106
+ (value) => current.forEach(({ resolve }) => resolve(value)),
107
+ (reason) => current.forEach(({ reject }) => reject(reason)),
108
+ );
93
109
  });
110
+
111
+ return promise;
94
112
  };
95
113
  }
96
114
 
@@ -167,6 +185,148 @@ type TriggerPublisherBinding = {
167
185
  triggerInvalidateHandler: (() => void) | null;
168
186
  };
169
187
 
188
+ type OperationBinding = {
189
+ run: () => Promise<unknown>;
190
+ };
191
+
192
+ /**
193
+ * Handle interne des décorateurs API.
194
+ *
195
+ * Le WeakMap cible l'instance connectée, pas la valeur de payload : une
196
+ * opération peut donc être relancée avant son premier résultat et reste
197
+ * indépendante d'une affectation manuelle de la propriété décorée.
198
+ */
199
+ class ApiOperationController {
200
+ private readonly bindings = new WeakMap<object, Set<OperationBinding>>();
201
+
202
+ bind(component: object, run: () => Promise<unknown>): () => void {
203
+ const binding: OperationBinding = { run };
204
+ let componentBindings = this.bindings.get(component);
205
+ if (!componentBindings) {
206
+ componentBindings = new Set();
207
+ this.bindings.set(component, componentBindings);
208
+ }
209
+ componentBindings.add(binding);
210
+
211
+ return () => {
212
+ const currentBindings = this.bindings.get(component);
213
+ if (!currentBindings) return;
214
+ currentBindings.delete(binding);
215
+ if (currentBindings.size === 0) {
216
+ this.bindings.delete(component);
217
+ }
218
+ };
219
+ }
220
+
221
+ run(component: object): Promise<unknown> {
222
+ const runs = [...(this.bindings.get(component) ?? [])].map(({ run }) =>
223
+ run(),
224
+ );
225
+ if (runs.length === 0) return Promise.resolve(undefined);
226
+ if (runs.length === 1) return runs[0];
227
+ return Promise.all(runs).then(() => undefined);
228
+ }
229
+ }
230
+
231
+ type ApiPropertyDecorator<T, Host extends object> = <K extends string>(
232
+ target: Host & {
233
+ [P in K]?: ApiResult<T> | null | undefined;
234
+ },
235
+ propertyKey: K,
236
+ ) => void;
237
+
238
+ export type GetDecoratorHandle<T, Host extends object = object> =
239
+ ApiPropertyDecorator<T, Host> & {
240
+ /**
241
+ * Relance le GET pour cette instance connectée.
242
+ *
243
+ * Le handle peut être conservé :
244
+ * `const usersGet = get(new Endpoint<User[]>("users"))`.
245
+ *
246
+ * La Promise est résolue après la fin du GET.
247
+ */
248
+ refetch(component: Host): Promise<ApiResult<T> | undefined>;
249
+ };
250
+
251
+ export type ApiSendDecoratorHandle<T, Host extends object = object> =
252
+ ApiPropertyDecorator<T, Host> & {
253
+ /**
254
+ * Relance l'opération d'écriture avec le body courant.
255
+ *
256
+ * Disponible avec `post`, `put` et `patch`.
257
+ * La Promise est résolue après la fin de l'opération.
258
+ */
259
+ send(component: Host): Promise<ApiResult<T> | undefined>;
260
+ };
261
+
262
+ export type ApiMethodActionOptions = {
263
+ /**
264
+ * Moment du déclenchement par rapport à la méthode.
265
+ * `after` est la valeur par défaut ; l'action attend la résolution d'une
266
+ * Promise retournée par la méthode.
267
+ * Avec `before`, l'action est terminée avant l'appel de la méthode.
268
+ */
269
+ when?: "before" | "after";
270
+ };
271
+
272
+ type ApiMethod = (this: object, ...args: unknown[]) => unknown;
273
+
274
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
275
+ return (
276
+ value !== null &&
277
+ (typeof value === "object" || typeof value === "function") &&
278
+ typeof (value as { then?: unknown }).then === "function"
279
+ );
280
+ }
281
+
282
+ function createApiMethodActionDecorator(
283
+ action: (component: object) => Promise<unknown>,
284
+ options?: ApiMethodActionOptions,
285
+ ): MethodDecorator {
286
+ const when = options?.when ?? "after";
287
+
288
+ return (_target, _propertyKey, descriptor) => {
289
+ const original = descriptor.value as ApiMethod | undefined;
290
+ if (!original) return;
291
+
292
+ const wrapped = function (this: object, ...args: unknown[]) {
293
+ if (when === "before") {
294
+ return action(this).then(() => original.apply(this, args));
295
+ }
296
+
297
+ const result = original.apply(this, args);
298
+ if (isPromiseLike(result)) {
299
+ return result.then((value) => {
300
+ return action(this).then(() => value);
301
+ });
302
+ }
303
+
304
+ return action(this).then(() => result);
305
+ };
306
+ descriptor.value = wrapped as unknown as typeof descriptor.value;
307
+ };
308
+ }
309
+
310
+ export function refetch<T, Host extends object = object>(
311
+ handle: GetDecoratorHandle<T, Host>,
312
+ options?: ApiMethodActionOptions,
313
+ ): MethodDecorator {
314
+ return createApiMethodActionDecorator(
315
+ (component) => handle.refetch(component as Host),
316
+ options,
317
+ );
318
+ }
319
+
320
+ export function send<T, Host extends object = object>(
321
+ handle: ApiSendDecoratorHandle<T, Host>,
322
+ options?: ApiMethodActionOptions,
323
+ ): MethodDecorator {
324
+ return createApiMethodActionDecorator(
325
+ (component) => handle.send(component as Host),
326
+ options,
327
+ );
328
+ }
329
+
170
330
  type RefetchScheduleState = {
171
331
  refetchTimeoutId: ReturnType<typeof setTimeout> | null;
172
332
  refetchEveryMs: number;
@@ -175,6 +335,7 @@ type RefetchScheduleState = {
175
335
  type ApiGetState = TriggerPublisherBinding &
176
336
  RefetchScheduleState & {
177
337
  cleanupWatchers: Array<() => void>;
338
+ cleanupOperation: (() => void) | null;
178
339
  requestGeneration: number;
179
340
  configPublisher: DataProvider | null;
180
341
  configMutationHandler: (() => void) | null;
@@ -202,8 +363,12 @@ function detachConfigPublisher(state: ApiGetState): void {
202
363
  * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
203
364
  * Toute mutation interne du publisher (`onInternalMutation`) relance le GET.
204
365
  *
205
- * **Options** (`GetOptions`) : `triggerKey` — `PublisherManager.get(...).invalidate()` relance le GET ;
206
- * `refetchEveryMs` — polling automatique.
366
+ * Le décorateur retourné expose `refetch(component)` pour relancer le GET d'une
367
+ * instance connectée. Pour déclencher le refetch depuis un autre composant,
368
+ * utiliser `triggerKey` puis `PublisherManager.get(...).invalidate()`.
369
+ *
370
+ * **Options** (`GetOptions`) : `triggerKey` — signal de refetch externe ;
371
+ * `refetchEveryMs` — polling automatique (0 ou absent = désactivé).
207
372
  *
208
373
  * @example
209
374
  * @get(new Endpoint<User, { userId: string }>("users/${userId}"))
@@ -216,58 +381,48 @@ function detachConfigPublisher(state: ApiGetState): void {
216
381
  * payload?: ApiResult<Thing>;
217
382
  *
218
383
  * @example
384
+ * const usersGet = get(new Endpoint<User[]>("users"));
385
+ * @usersGet
386
+ * payload?: ApiResult<User[]>;
387
+ *
388
+ * refresh() {
389
+ * await usersGet.refetch(this);
390
+ * }
391
+ *
392
+ * @example
219
393
  * @get(usersEndpoint, { triggerKey: refreshUsersKey })
220
394
  * payload?: ApiResult<User[]>;
221
395
  * // PublisherManager.get("users.refresh").invalidate() → refetch
222
396
  */
223
397
  export function get<T, Ue = any>(
224
398
  endpoint: Endpoint<T, Ue>,
225
- ): <K extends string>(
226
- target: DataProviderKeyHost<Ue> & {
227
- [P in K]?: ApiResult<T> | null | undefined;
228
- },
229
- propertyKey: K,
230
- ) => void;
399
+ ): GetDecoratorHandle<T, DataProviderKeyHost<Ue>>;
231
400
  export function get<T, Ue = any, Uk = any>(
232
401
  endpoint: Endpoint<T, Ue>,
233
402
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
234
- ): <K extends string>(
235
- target: DataProviderKeyHost<Ue> &
236
- DataProviderKeyHost<Uk> & {
237
- [P in K]?: ApiResult<T> | null | undefined;
238
- },
239
- propertyKey: K,
240
- ) => void;
403
+ ): GetDecoratorHandle<
404
+ T,
405
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
406
+ >;
241
407
  export function get<T, Ue = any, Uk = any>(
242
408
  endpoint: Endpoint<T, Ue>,
243
409
  options: GetOptions,
244
- ): <K extends string>(
245
- target: DataProviderKeyHost<Ue> & {
246
- [P in K]?: ApiResult<T> | null | undefined;
247
- },
248
- propertyKey: K,
249
- ) => void;
410
+ ): GetDecoratorHandle<T, DataProviderKeyHost<Ue>>;
250
411
  export function get<T, Ue = any, Uk = any, Ut = any>(
251
412
  endpoint: Endpoint<T, Ue>,
252
413
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
253
414
  options: GetOptions,
254
- ): <K extends string>(
255
- target: DataProviderKeyHost<Ue> &
256
- DataProviderKeyHost<Uk> & {
257
- [P in K]?: ApiResult<T> | null | undefined;
258
- },
259
- propertyKey: K,
260
- ) => void;
415
+ ): GetDecoratorHandle<
416
+ T,
417
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
418
+ >;
261
419
  export function get<T, Ue = any, Uk = any, _Ut = any>(
262
420
  endpoint: Endpoint<T, Ue>,
263
421
  configurationKeyOrOptions?:
264
422
  | DataProviderKey<APIConfiguration, Uk>
265
423
  | GetOptions,
266
424
  maybeOptions?: GetOptions,
267
- ): <K extends string>(
268
- target: object & { [P in K]?: ApiResult<T> | null | undefined },
269
- propertyKey: K,
270
- ) => void {
425
+ ): GetDecoratorHandle<T, object> {
271
426
  const pathTemplate = endpoint.path;
272
427
  const configurationKey = isGetOptions(configurationKeyOrOptions)
273
428
  ? undefined
@@ -297,8 +452,9 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
297
452
  const isDynamicPath = endpointDynamicDependencies.length > 0;
298
453
  const usesPublisherConfig = Boolean(configurationKeyPath);
299
454
  const refetchEveryMs = options?.refetchEveryMs ?? 0;
455
+ const operationController = new ApiOperationController();
300
456
 
301
- return function (target: object, propertyKey: string) {
457
+ const decorator = function (target: object, propertyKey: string) {
302
458
  if (!target) return;
303
459
  setSubscribable(target);
304
460
  const stateKey = `__get_state_${propertyKey}`;
@@ -309,6 +465,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
309
465
  if (!state) {
310
466
  state = {
311
467
  cleanupWatchers: [],
468
+ cleanupOperation: null,
312
469
  requestGeneration: 0,
313
470
  configPublisher: null,
314
471
  configMutationHandler: null,
@@ -321,6 +478,8 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
321
478
  comp[stateKey] = state;
322
479
  }
323
480
 
481
+ state.cleanupOperation?.();
482
+ state.cleanupOperation = null;
324
483
  state.cleanupWatchers.forEach((cleanup) => cleanup());
325
484
  state.cleanupWatchers = [];
326
485
  state.requestGeneration++;
@@ -332,18 +491,18 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
332
491
  if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
333
492
  state.refetchTimeoutId = setTimeout(() => {
334
493
  state.refetchTimeoutId = null;
335
- runFetch();
494
+ void runFetch().catch(() => {});
336
495
  }, state.refetchEveryMs);
337
496
  };
338
497
 
339
- const runFetch = () => {
498
+ const runFetch = async (): Promise<ApiResult<T> | undefined> => {
340
499
  const resolution = isDynamicPath
341
500
  ? resolveDynamicPath(component, pathTemplate, pathOptions)
342
501
  : { ready: true, path: pathTemplate };
343
502
  if (!resolution.ready || !resolution.path) {
344
503
  comp[propertyKey] = undefined;
345
504
  scheduleRefetch();
346
- return;
505
+ return undefined;
347
506
  }
348
507
  let config: APIConfiguration | null = null;
349
508
  if (usesPublisherConfig && configurationKeyPath) {
@@ -355,7 +514,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
355
514
  if (!configRes.ready || !configRes.path) {
356
515
  comp[propertyKey] = undefined;
357
516
  scheduleRefetch();
358
- return;
517
+ return undefined;
359
518
  }
360
519
  const configPublisher = getPublisherFromPath(configRes.path);
361
520
  config = readApiConfigurationFromPublisher(configPublisher);
@@ -364,7 +523,10 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
364
523
  }
365
524
  if (!isScopedConfigurationReady(config)) {
366
525
  if (!usesPublisherConfig && !state.scopeWatchCleanup) {
367
- const scopeWatch = watchScopedConfiguration(component, runFetch);
526
+ const scopeWatch = watchScopedConfiguration(
527
+ component,
528
+ scheduleFetchAndIgnoreErrors,
529
+ );
368
530
  state.scopeWatchCleanup = scopeWatch;
369
531
  state.cleanupWatchers.push(() => {
370
532
  scopeWatch();
@@ -372,20 +534,25 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
372
534
  });
373
535
  }
374
536
  scheduleRefetch();
375
- return;
537
+ return undefined;
376
538
  }
377
539
  const generation = ++state.requestGeneration;
378
540
  const api = new API(config);
379
- void api
380
- .getDetailed<T>(resolution.path)
381
- .then((payload?: ApiResult<T>) => {
382
- if (generation !== state.requestGeneration) return;
383
- comp[propertyKey] = payload;
384
- scheduleRefetch();
385
- });
541
+ const payload = await api.getDetailed<T>(resolution.path);
542
+ if (generation !== state.requestGeneration) return payload;
543
+ comp[propertyKey] = payload;
544
+ scheduleRefetch();
545
+ return payload;
386
546
  };
387
547
 
388
548
  const scheduleFetch = createAnimationFrameCoalescedRunner(runFetch);
549
+ const scheduleFetchAndIgnoreErrors = () => {
550
+ void scheduleFetch().catch(() => {});
551
+ };
552
+ state.cleanupOperation = operationController.bind(
553
+ component,
554
+ scheduleFetch,
555
+ );
389
556
 
390
557
  const rebindTriggerPublisher = () => {
391
558
  detachTriggerPublisher(state);
@@ -400,7 +567,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
400
567
  }
401
568
  const publisher = getPublisherFromPath(triggerResolution.path);
402
569
  if (!publisher) return;
403
- const invalidateHandler = () => scheduleFetch();
570
+ const invalidateHandler = scheduleFetchAndIgnoreErrors;
404
571
  publisher.onInvalidate(invalidateHandler);
405
572
  state.triggerPublisher = publisher;
406
573
  state.triggerInvalidateHandler = invalidateHandler;
@@ -423,7 +590,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
423
590
  comp[propertyKey] = undefined;
424
591
  return;
425
592
  }
426
- const mutationHandler = () => scheduleFetch();
593
+ const mutationHandler = scheduleFetchAndIgnoreErrors;
427
594
  publisher.onInternalMutation(mutationHandler);
428
595
  state.configPublisher = publisher;
429
596
  state.configMutationHandler = mutationHandler;
@@ -433,7 +600,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
433
600
  rebindTriggerPublisher();
434
601
  rebindPublisherConfig();
435
602
  if (!usesPublisherConfig) {
436
- scheduleFetch();
603
+ scheduleFetchAndIgnoreErrors();
437
604
  }
438
605
  };
439
606
 
@@ -458,13 +625,13 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
458
625
  getDynamicWatchKeys.hooked,
459
626
  component,
460
627
  dependency,
461
- () => scheduleFetch(),
628
+ scheduleFetchAndIgnoreErrors,
462
629
  );
463
630
  state.cleanupWatchers.push(unsubscribe);
464
631
  }
465
632
  }
466
633
  rebindTriggerPublisher();
467
- runFetch();
634
+ void runFetch().catch(() => {});
468
635
  };
469
636
  state.cleanupWatchers.push(
470
637
  scheduleAfterHostReady(component, startScopedFetch),
@@ -476,6 +643,8 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
476
643
  const comp = component as Record<string, unknown>;
477
644
  const state = comp[stateKey] as ApiGetState | undefined;
478
645
  if (!state) return;
646
+ state.cleanupOperation?.();
647
+ state.cleanupOperation = null;
479
648
  detachConfigPublisher(state);
480
649
  detachTriggerPublisher(state);
481
650
  clearRefetch(state);
@@ -484,27 +653,38 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
484
653
  state.requestGeneration++;
485
654
  comp[propertyKey] = undefined;
486
655
  });
656
+ } as GetDecoratorHandle<T, object>;
657
+
658
+ decorator.refetch = (component: object) => {
659
+ return operationController.run(component) as Promise<
660
+ ApiResult<T> | undefined
661
+ >;
487
662
  };
663
+
664
+ return decorator;
488
665
  }
489
666
 
490
667
  export type GetOptions<Uk = any> = DynamicPathOptions & {
491
- /** Intervalle de refetch automatique (ms). 0 ou absent = pas de polling. */
668
+ /** Intervalle de refetch automatique (ms). 0 ou absent = désactivé. */
492
669
  refetchEveryMs?: number;
493
- /** Publisher dont `invalidate()` relance le GET. */
670
+ /** Publisher-signal dont `invalidate()` relance le GET. */
494
671
  triggerKey?: DataProviderKey<unknown, Uk>;
495
672
  };
496
673
 
497
674
  export type ApiSendOptions<Uk = any> = DynamicPathOptions & {
498
- /** Intervalle de renvoi automatique (ms). 0 ou absent = pas de polling. */
675
+ /**
676
+ * Intervalle de renvoi automatique (ms).
677
+ * 0 ou absent = désactivé ; attention aux effets de bord de l'opération.
678
+ */
499
679
  refetchEveryMs?: number;
500
- /** Ne pas envoyer si le body publisher est null/undefined (défaut: true). */
680
+ /** Ignore l'envoi si le body vaut null/undefined (défaut : true). */
501
681
  skipIfBodyMissing?: boolean;
502
682
  /**
503
- * Relancer l'envoi quand le body publisher mute (défaut: true).
504
- * `false` : le body est lu au moment de l'envoi ; déclencher via `triggerKey.invalidate()`.
683
+ * Relance l'envoi quand le body publisher mute (défaut : true).
684
+ * `false` : le body est lu au moment de `send()` ou `triggerKey.invalidate()`.
505
685
  */
506
686
  autoPostOnBodyMutation?: boolean;
507
- /** Publisher dont `invalidate()` relance l'envoi (relit le body courant). */
687
+ /** Publisher-signal dont `invalidate()` relance l'envoi avec le body courant. */
508
688
  triggerKey?: DataProviderKey<unknown, Uk>;
509
689
  };
510
690
 
@@ -591,10 +771,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
591
771
  | DataProviderKey<APIConfiguration, Uk>
592
772
  | ApiSendOptions<Ut>,
593
773
  maybeOptions?: ApiSendOptions<Ut>,
594
- ): <K extends string>(
595
- target: object & { [P in K]?: ApiResult<T> | null | undefined },
596
- propertyKey: K,
597
- ) => void {
774
+ ): ApiSendDecoratorHandle<T, object> {
598
775
  const pathTemplate = endpoint.path;
599
776
  const bodyKeyPath = bodyKey.path;
600
777
  const configurationKey = isApiSendOptions(configurationKeyOrOptions)
@@ -631,8 +808,9 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
631
808
  skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
632
809
  };
633
810
  const stateKeyPrefix = `__${method.toLowerCase()}_state_`;
811
+ const operationController = new ApiOperationController();
634
812
 
635
- return function (target: object, propertyKey: string) {
813
+ const decorator = function (target: object, propertyKey: string) {
636
814
  if (!target) return;
637
815
  setSubscribable(target);
638
816
  const stateKey = `${stateKeyPrefix}${propertyKey}`;
@@ -643,6 +821,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
643
821
  if (!state) {
644
822
  state = {
645
823
  cleanupWatchers: [],
824
+ cleanupOperation: null,
646
825
  requestGeneration: 0,
647
826
  configPublisher: null,
648
827
  configMutationHandler: null,
@@ -659,6 +838,8 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
659
838
  comp[stateKey] = state;
660
839
  }
661
840
 
841
+ state.cleanupOperation?.();
842
+ state.cleanupOperation = null;
662
843
  state.cleanupWatchers.forEach((cleanup) => cleanup());
663
844
  state.cleanupWatchers = [];
664
845
  state.requestGeneration++;
@@ -672,18 +853,18 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
672
853
  if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
673
854
  state.refetchTimeoutId = setTimeout(() => {
674
855
  state.refetchTimeoutId = null;
675
- runSend();
856
+ void runSend().catch(() => {});
676
857
  }, state.refetchEveryMs);
677
858
  };
678
859
 
679
- const runSend = () => {
860
+ const runSend = async (): Promise<ApiResult<unknown> | undefined> => {
680
861
  const resolution = isDynamicPath
681
862
  ? resolveDynamicPath(component, pathTemplate, pathOptions)
682
863
  : { ready: true, path: pathTemplate };
683
864
  if (!resolution.ready || !resolution.path) {
684
865
  comp[propertyKey] = undefined;
685
866
  scheduleRefetch();
686
- return;
867
+ return undefined;
687
868
  }
688
869
 
689
870
  const bodyResolution = resolveDynamicPath(
@@ -694,7 +875,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
694
875
  if (!bodyResolution.ready || !bodyResolution.path) {
695
876
  comp[propertyKey] = undefined;
696
877
  scheduleRefetch();
697
- return;
878
+ return undefined;
698
879
  }
699
880
 
700
881
  const bodyPublisher = getPublisherFromPath(bodyResolution.path);
@@ -704,7 +885,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
704
885
  (body === null || body === undefined)
705
886
  ) {
706
887
  scheduleRefetch();
707
- return;
888
+ return undefined;
708
889
  }
709
890
 
710
891
  let config: APIConfiguration | null = null;
@@ -717,7 +898,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
717
898
  if (!configRes.ready || !configRes.path) {
718
899
  comp[propertyKey] = undefined;
719
900
  scheduleRefetch();
720
- return;
901
+ return undefined;
721
902
  }
722
903
  const configPublisher = getPublisherFromPath(configRes.path);
723
904
  config = readApiConfigurationFromPublisher(configPublisher);
@@ -727,7 +908,10 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
727
908
 
728
909
  if (!isScopedConfigurationReady(config)) {
729
910
  if (!usesPublisherConfig && !state.scopeWatchCleanup) {
730
- const scopeWatch = watchScopedConfiguration(component, runSend);
911
+ const scopeWatch = watchScopedConfiguration(
912
+ component,
913
+ scheduleSendAndIgnoreErrors,
914
+ );
731
915
  state.scopeWatchCleanup = scopeWatch;
732
916
  state.cleanupWatchers.push(() => {
733
917
  scopeWatch();
@@ -735,21 +919,26 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
735
919
  });
736
920
  }
737
921
  scheduleRefetch();
738
- return;
922
+ return undefined;
739
923
  }
740
924
 
741
925
  const generation = ++state.requestGeneration;
742
926
  const api = new API(config);
743
- void callDetailed(api, resolution.path, body).then(
744
- (payload?: ApiResult<unknown>) => {
745
- if (generation !== state.requestGeneration) return;
746
- comp[propertyKey] = payload;
747
- scheduleRefetch();
748
- },
749
- );
927
+ const payload = await callDetailed(api, resolution.path, body);
928
+ if (generation !== state.requestGeneration) return payload;
929
+ comp[propertyKey] = payload;
930
+ scheduleRefetch();
931
+ return payload;
750
932
  };
751
933
 
752
934
  const scheduleSend = createAnimationFrameCoalescedRunner(runSend);
935
+ const scheduleSendAndIgnoreErrors = () => {
936
+ void scheduleSend().catch(() => {});
937
+ };
938
+ state.cleanupOperation = operationController.bind(
939
+ component,
940
+ scheduleSend,
941
+ );
753
942
 
754
943
  const rebindBodyPublisher = () => {
755
944
  detachBodyPublisher(state);
@@ -766,7 +955,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
766
955
  }
767
956
  const publisher = getPublisherFromPath(bodyResolution.path);
768
957
  if (!publisher) return;
769
- const mutationHandler = () => scheduleSend();
958
+ const mutationHandler = scheduleSendAndIgnoreErrors;
770
959
  publisher.onInternalMutation(mutationHandler);
771
960
  state.bodyPublisher = publisher;
772
961
  state.bodyMutationHandler = mutationHandler;
@@ -785,7 +974,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
785
974
  }
786
975
  const publisher = getPublisherFromPath(triggerResolution.path);
787
976
  if (!publisher) return;
788
- const invalidateHandler = () => scheduleSend();
977
+ const invalidateHandler = scheduleSendAndIgnoreErrors;
789
978
  publisher.onInvalidate(invalidateHandler);
790
979
  state.triggerPublisher = publisher;
791
980
  state.triggerInvalidateHandler = invalidateHandler;
@@ -808,7 +997,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
808
997
  comp[propertyKey] = undefined;
809
998
  return;
810
999
  }
811
- const mutationHandler = () => scheduleSend();
1000
+ const mutationHandler = scheduleSendAndIgnoreErrors;
812
1001
  publisher.onInternalMutation(mutationHandler);
813
1002
  state.configPublisher = publisher;
814
1003
  state.configMutationHandler = mutationHandler;
@@ -819,7 +1008,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
819
1008
  rebindTriggerPublisher();
820
1009
  rebindPublisherConfig();
821
1010
  if (state.autoPostOnBodyMutation) {
822
- scheduleSend();
1011
+ scheduleSendAndIgnoreErrors();
823
1012
  }
824
1013
  };
825
1014
 
@@ -848,7 +1037,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
848
1037
  getDynamicWatchKeys.hooked,
849
1038
  component,
850
1039
  dependency,
851
- () => scheduleSend(),
1040
+ scheduleSendAndIgnoreErrors,
852
1041
  );
853
1042
  state.cleanupWatchers.push(unsubscribe);
854
1043
  }
@@ -856,7 +1045,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
856
1045
  rebindBodyPublisher();
857
1046
  rebindTriggerPublisher();
858
1047
  if (state.autoPostOnBodyMutation) {
859
- scheduleSend();
1048
+ scheduleSendAndIgnoreErrors();
860
1049
  }
861
1050
  };
862
1051
  state.cleanupWatchers.push(
@@ -869,6 +1058,8 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
869
1058
  const comp = component as Record<string, unknown>;
870
1059
  const state = comp[stateKey] as ApiSendState | undefined;
871
1060
  if (!state) return;
1061
+ state.cleanupOperation?.();
1062
+ state.cleanupOperation = null;
872
1063
  detachConfigPublisher(state);
873
1064
  detachBodyPublisher(state);
874
1065
  detachTriggerPublisher(state);
@@ -878,7 +1069,15 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
878
1069
  state.requestGeneration++;
879
1070
  comp[propertyKey] = undefined;
880
1071
  });
1072
+ } as ApiSendDecoratorHandle<T, object>;
1073
+
1074
+ decorator.send = (component: object) => {
1075
+ return operationController.run(component) as Promise<
1076
+ ApiResult<T> | undefined
1077
+ >;
881
1078
  };
1079
+
1080
+ return decorator;
882
1081
  };
883
1082
  }
884
1083
 
@@ -886,54 +1085,46 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
886
1085
  * Décorateur **`@post`** : envoie des données via `API.postDetailed` et assigne un
887
1086
  * `ApiResult<T>` (`request`, `response`, `result` typé `T`).
888
1087
  *
889
- * Le body est lu depuis un `DataProviderKey` (mutations et assignations relancent le POST).
1088
+ * Le body est lu depuis un `DataProviderKey`. Par défaut, une mutation du body
1089
+ * relance le POST ; `send(component)` permet de le relancer explicitement.
890
1090
  *
891
1091
  * @example
892
1092
  * @post(new Endpoint<SyncResult, { sessionId: string }>("sessions/${sessionId}/sync"), syncRequestKey)
893
1093
  * payload?: ApiResult<SyncResult>;
1094
+ *
1095
+ * @example
1096
+ * const save = post(new Endpoint<User>("users"), userRequestKey);
1097
+ * @save
1098
+ * payload?: ApiResult<User>;
1099
+ * // Dans un handler du composant :
1100
+ * await save.send(this);
894
1101
  */
895
1102
  export function post<T, B, Ue = any>(
896
1103
  endpoint: Endpoint<T, Ue>,
897
1104
  bodyKey: DataProviderKey<B>,
898
- ): <K extends string>(
899
- target: DataProviderKeyHost<Ue> & {
900
- [P in K]?: ApiResult<T> | null | undefined;
901
- },
902
- propertyKey: K,
903
- ) => void;
1105
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
904
1106
  export function post<T, B, Ue = any, Uk = any>(
905
1107
  endpoint: Endpoint<T, Ue>,
906
1108
  bodyKey: DataProviderKey<B>,
907
1109
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
908
- ): <K extends string>(
909
- target: DataProviderKeyHost<Ue> &
910
- DataProviderKeyHost<Uk> & {
911
- [P in K]?: ApiResult<T> | null | undefined;
912
- },
913
- propertyKey: K,
914
- ) => void;
1110
+ ): ApiSendDecoratorHandle<
1111
+ T,
1112
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1113
+ >;
915
1114
  export function post<T, B, Ue = any, Uk = any>(
916
1115
  endpoint: Endpoint<T, Ue>,
917
1116
  bodyKey: DataProviderKey<B>,
918
1117
  options: PostOptions<Uk>,
919
- ): <K extends string>(
920
- target: DataProviderKeyHost<Ue> & {
921
- [P in K]?: ApiResult<T> | null | undefined;
922
- },
923
- propertyKey: K,
924
- ) => void;
1118
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
925
1119
  export function post<T, B, Ue = any, Uk = any, Ut = any>(
926
1120
  endpoint: Endpoint<T, Ue>,
927
1121
  bodyKey: DataProviderKey<B>,
928
1122
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
929
1123
  options: PostOptions<Ut>,
930
- ): <K extends string>(
931
- target: DataProviderKeyHost<Ue> &
932
- DataProviderKeyHost<Uk> & {
933
- [P in K]?: ApiResult<T> | null | undefined;
934
- },
935
- propertyKey: K,
936
- ) => void;
1124
+ ): ApiSendDecoratorHandle<
1125
+ T,
1126
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1127
+ >;
937
1128
  export function post<T, B, Ue = any, Uk = any, Ut = any>(
938
1129
  endpoint: Endpoint<T, Ue>,
939
1130
  bodyKey: DataProviderKey<B>,
@@ -941,58 +1132,39 @@ export function post<T, B, Ue = any, Uk = any, Ut = any>(
941
1132
  | DataProviderKey<APIConfiguration, Uk>
942
1133
  | PostOptions<Ut>,
943
1134
  maybeOptions?: PostOptions<Ut>,
944
- ): <K extends string>(
945
- target: object & { [P in K]?: ApiResult<T> | null | undefined },
946
- propertyKey: K,
947
- ) => void {
1135
+ ): ApiSendDecoratorHandle<T, object> {
948
1136
  return createApiSendDecorator<T, B, Ue, Uk, Ut>("POST", (api, path, body) =>
949
1137
  api.postDetailed<T, B>(path, body as B),
950
1138
  )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);
951
1139
  }
952
1140
 
953
- /** `@put` — même modèle que `@post` via `API.putDetailed`. */
1141
+ /** `@put` — même modèle que `@post`, via `API.putDetailed` et `send()`. */
954
1142
  export function put<T, B, Ue = any>(
955
1143
  endpoint: Endpoint<T, Ue>,
956
1144
  bodyKey: DataProviderKey<B>,
957
- ): <K extends string>(
958
- target: DataProviderKeyHost<Ue> & {
959
- [P in K]?: ApiResult<T> | null | undefined;
960
- },
961
- propertyKey: K,
962
- ) => void;
1145
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
963
1146
  export function put<T, B, Ue = any, Uk = any>(
964
1147
  endpoint: Endpoint<T, Ue>,
965
1148
  bodyKey: DataProviderKey<B>,
966
1149
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
967
- ): <K extends string>(
968
- target: DataProviderKeyHost<Ue> &
969
- DataProviderKeyHost<Uk> & {
970
- [P in K]?: ApiResult<T> | null | undefined;
971
- },
972
- propertyKey: K,
973
- ) => void;
1150
+ ): ApiSendDecoratorHandle<
1151
+ T,
1152
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1153
+ >;
974
1154
  export function put<T, B, Ue = any, Uk = any>(
975
1155
  endpoint: Endpoint<T, Ue>,
976
1156
  bodyKey: DataProviderKey<B>,
977
1157
  options: PutOptions<Uk>,
978
- ): <K extends string>(
979
- target: DataProviderKeyHost<Ue> & {
980
- [P in K]?: ApiResult<T> | null | undefined;
981
- },
982
- propertyKey: K,
983
- ) => void;
1158
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
984
1159
  export function put<T, B, Ue = any, Uk = any, Ut = any>(
985
1160
  endpoint: Endpoint<T, Ue>,
986
1161
  bodyKey: DataProviderKey<B>,
987
1162
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
988
1163
  options: PutOptions<Ut>,
989
- ): <K extends string>(
990
- target: DataProviderKeyHost<Ue> &
991
- DataProviderKeyHost<Uk> & {
992
- [P in K]?: ApiResult<T> | null | undefined;
993
- },
994
- propertyKey: K,
995
- ) => void;
1164
+ ): ApiSendDecoratorHandle<
1165
+ T,
1166
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1167
+ >;
996
1168
  export function put<T, B, Ue = any, Uk = any, Ut = any>(
997
1169
  endpoint: Endpoint<T, Ue>,
998
1170
  bodyKey: DataProviderKey<B>,
@@ -1000,58 +1172,39 @@ export function put<T, B, Ue = any, Uk = any, Ut = any>(
1000
1172
  | DataProviderKey<APIConfiguration, Uk>
1001
1173
  | PutOptions<Ut>,
1002
1174
  maybeOptions?: PutOptions<Ut>,
1003
- ): <K extends string>(
1004
- target: object & { [P in K]?: ApiResult<T> | null | undefined },
1005
- propertyKey: K,
1006
- ) => void {
1175
+ ): ApiSendDecoratorHandle<T, object> {
1007
1176
  return createApiSendDecorator<T, B, Ue, Uk, Ut>("PUT", (api, path, body) =>
1008
1177
  api.putDetailed<T, B>(path, body as B),
1009
1178
  )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);
1010
1179
  }
1011
1180
 
1012
- /** `@patch` — même modèle que `@post` via `API.patchDetailed`. */
1181
+ /** `@patch` — même modèle que `@post`, via `API.patchDetailed` et `send()`. */
1013
1182
  export function patch<T, B, Ue = any>(
1014
1183
  endpoint: Endpoint<T, Ue>,
1015
1184
  bodyKey: DataProviderKey<B>,
1016
- ): <K extends string>(
1017
- target: DataProviderKeyHost<Ue> & {
1018
- [P in K]?: ApiResult<T> | null | undefined;
1019
- },
1020
- propertyKey: K,
1021
- ) => void;
1185
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
1022
1186
  export function patch<T, B, Ue = any, Uk = any>(
1023
1187
  endpoint: Endpoint<T, Ue>,
1024
1188
  bodyKey: DataProviderKey<B>,
1025
1189
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
1026
- ): <K extends string>(
1027
- target: DataProviderKeyHost<Ue> &
1028
- DataProviderKeyHost<Uk> & {
1029
- [P in K]?: ApiResult<T> | null | undefined;
1030
- },
1031
- propertyKey: K,
1032
- ) => void;
1190
+ ): ApiSendDecoratorHandle<
1191
+ T,
1192
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1193
+ >;
1033
1194
  export function patch<T, B, Ue = any, Uk = any>(
1034
1195
  endpoint: Endpoint<T, Ue>,
1035
1196
  bodyKey: DataProviderKey<B>,
1036
1197
  options: PatchOptions<Uk>,
1037
- ): <K extends string>(
1038
- target: DataProviderKeyHost<Ue> & {
1039
- [P in K]?: ApiResult<T> | null | undefined;
1040
- },
1041
- propertyKey: K,
1042
- ) => void;
1198
+ ): ApiSendDecoratorHandle<T, DataProviderKeyHost<Ue>>;
1043
1199
  export function patch<T, B, Ue = any, Uk = any, Ut = any>(
1044
1200
  endpoint: Endpoint<T, Ue>,
1045
1201
  bodyKey: DataProviderKey<B>,
1046
1202
  configurationKey: DataProviderKey<APIConfiguration, Uk>,
1047
1203
  options: PatchOptions<Ut>,
1048
- ): <K extends string>(
1049
- target: DataProviderKeyHost<Ue> &
1050
- DataProviderKeyHost<Uk> & {
1051
- [P in K]?: ApiResult<T> | null | undefined;
1052
- },
1053
- propertyKey: K,
1054
- ) => void;
1204
+ ): ApiSendDecoratorHandle<
1205
+ T,
1206
+ DataProviderKeyHost<Ue> & DataProviderKeyHost<Uk>
1207
+ >;
1055
1208
  export function patch<T, B, Ue = any, Uk = any, Ut = any>(
1056
1209
  endpoint: Endpoint<T, Ue>,
1057
1210
  bodyKey: DataProviderKey<B>,
@@ -1059,10 +1212,7 @@ export function patch<T, B, Ue = any, Uk = any, Ut = any>(
1059
1212
  | DataProviderKey<APIConfiguration, Uk>
1060
1213
  | PatchOptions<Ut>,
1061
1214
  maybeOptions?: PatchOptions<Ut>,
1062
- ): <K extends string>(
1063
- target: object & { [P in K]?: ApiResult<T> | null | undefined },
1064
- propertyKey: K,
1065
- ) => void {
1215
+ ): ApiSendDecoratorHandle<T, object> {
1066
1216
  return createApiSendDecorator<T, B, Ue, Uk, Ut>("PATCH", (api, path, body) =>
1067
1217
  api.patchDetailed<T, B>(path, body as B),
1068
1218
  )(endpoint, bodyKey, configurationKeyOrOptions, maybeOptions);