@supersoniks/concorde 4.9.91 → 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 (37) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +156 -156
  3. package/concorde-core.es.js +2348 -2331
  4. package/dist/concorde-core.bundle.js +156 -156
  5. package/dist/concorde-core.es.js +2348 -2331
  6. package/dist/js/core/decorators/api.js +64 -51
  7. package/dist/js/core/decorators/api.js.map +1 -1
  8. package/dist/types/core/decorators/api.d.ts +10 -5
  9. package/dist/types/core/decorators/api.d.ts.map +1 -1
  10. package/docs/assets/{index-BL6WVF7g.css → index-D2Q3JyQ6.css} +1 -1
  11. package/docs/assets/{index-DVn8AeyA.js → index-DURr6O71.js} +466 -568
  12. package/docs/crawl/docs/_decorators/get.html +35 -10
  13. package/docs/crawl/docs/_decorators/patch.html +13 -5
  14. package/docs/crawl/docs/_decorators/post.html +39 -10
  15. package/docs/crawl/docs/_decorators/put.html +13 -5
  16. package/docs/crawl/hubs/decorators.html +43 -40
  17. package/docs/crawl/hubs/remote-data.html +43 -40
  18. package/docs/index.html +2 -2
  19. package/docs/llms-full.txt +98 -23
  20. package/docs/llms.txt +1 -1
  21. package/docs/src/docs/_decorators/get.md +39 -5
  22. package/docs/src/docs/_decorators/patch.md +8 -3
  23. package/docs/src/docs/_decorators/post.md +39 -6
  24. package/docs/src/docs/_decorators/put.md +8 -3
  25. package/docs/src/docs/search/docs-search.json +103 -183
  26. package/docs/src/tsconfig.json +268 -268
  27. package/package.json +1 -1
  28. package/src/core/decorators/api.post.spec.ts +2 -2
  29. package/src/core/decorators/api.spec.ts +47 -6
  30. package/src/core/decorators/api.ts +100 -64
  31. package/src/docs/_decorators/get.md +12 -5
  32. package/src/docs/_decorators/patch.md +7 -2
  33. package/src/docs/_decorators/post.md +12 -7
  34. package/src/docs/_decorators/put.md +7 -2
  35. package/src/docs/components/docs-demo-sources.ts +8 -8
  36. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +2 -2
  37. package/src/docs/search/docs-search.json +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.9.91",
3
+ "version": "4.9.92",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -95,14 +95,14 @@ describe("post", () => {
95
95
  vi.mocked(API.prototype.postDetailed).mockClear();
96
96
 
97
97
  pub.set({ value: "second" });
98
- localPost.send(el);
99
- await flushSendFrames();
98
+ const payload = await localPost.send(el);
100
99
 
101
100
  expect(API.prototype.postDetailed).toHaveBeenCalledTimes(1);
102
101
  expect(API.prototype.postDetailed).toHaveBeenCalledWith(
103
102
  "items/local-send",
104
103
  { value: "second" },
105
104
  );
105
+ expect(payload?.result).toEqual({ ok: true });
106
106
  });
107
107
 
108
108
  it("peut décorer une méthode avec send après sa résolution", async () => {
@@ -209,16 +209,18 @@ describe("get", () => {
209
209
  const el = document.createElement(tag) as InstanceType<typeof C>;
210
210
  document.body.appendChild(el);
211
211
 
212
- localGet.refetch(el);
213
- await flushGetFrames();
212
+ const firstRefetch = localGet.refetch(el);
213
+ expect(firstRefetch).toBeInstanceOf(Promise);
214
+ const firstPayload = await firstRefetch;
214
215
  expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
216
+ expect(firstPayload?.result).toEqual({ id: "loaded" });
215
217
 
216
218
  vi.mocked(API.prototype.getDetailed).mockClear();
217
219
  el.payload = mockPayload({ id: "manual" });
218
- localGet.refetch(el);
219
- await flushGetFrames();
220
+ const secondPayload = await localGet.refetch(el);
220
221
 
221
222
  expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
223
+ expect(secondPayload?.result).toEqual({ id: "loaded" });
222
224
  expect(el.payload?.result).toEqual({ id: "loaded" });
223
225
  });
224
226
 
@@ -246,8 +248,7 @@ describe("get", () => {
246
248
  vi.mocked(API.prototype.getDetailed).mockClear();
247
249
 
248
250
  el.remove();
249
- localGet.refetch(el);
250
- await flushGetFrames();
251
+ await localGet.refetch(el);
251
252
 
252
253
  expect(API.prototype.getDetailed).not.toHaveBeenCalled();
253
254
  });
@@ -289,6 +290,46 @@ describe("get", () => {
289
290
  expect(API.prototype.getDetailed).toHaveBeenCalledWith("rows/84");
290
291
  });
291
292
 
293
+ it("attend le refetch avant la méthode avec when before", async () => {
294
+ const pubId = "getSpecMethodRefetchBefore";
295
+ PublisherManager.get(pubId).set(staticConfig);
296
+ const confKey = new DataProviderKey<APIConfiguration>(pubId);
297
+ const localGet = get(
298
+ new Endpoint<{ id: string }>("items/before"),
299
+ confKey,
300
+ );
301
+ const events: string[] = [];
302
+ const tag = "test-api-get-method-refetch-before";
303
+
304
+ class C extends HTMLElement {
305
+ @localGet
306
+ payload: ApiResult<{ id: string }> | null = null;
307
+
308
+ @refetch(localGet, { when: "before" })
309
+ reload() {
310
+ events.push("method");
311
+ }
312
+ }
313
+ if (!customElements.get(tag)) {
314
+ customElements.define(tag, C);
315
+ }
316
+
317
+ const el = document.createElement(tag) as InstanceType<typeof C>;
318
+ document.body.appendChild(el);
319
+ await flushGetFrames();
320
+ vi.mocked(API.prototype.getDetailed).mockClear();
321
+ events.length = 0;
322
+ vi.mocked(API.prototype.getDetailed).mockImplementation(async () => {
323
+ events.push("request");
324
+ return mockPayload({ id: "loaded" }) as never;
325
+ });
326
+
327
+ await el.reload();
328
+
329
+ expect(events).toEqual(["request", "method"]);
330
+ expect(API.prototype.getDetailed).toHaveBeenCalledWith("items/before");
331
+ });
332
+
292
333
  it("refetch automatique avec refetchEveryMs", async () => {
293
334
  vi.useFakeTimers();
294
335
  const itemEp = new Endpoint<{ id: string }>("items/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
 
@@ -168,8 +186,7 @@ type TriggerPublisherBinding = {
168
186
  };
169
187
 
170
188
  type OperationBinding = {
171
- component: object;
172
- run: () => void;
189
+ run: () => Promise<unknown>;
173
190
  };
174
191
 
175
192
  /**
@@ -182,8 +199,8 @@ type OperationBinding = {
182
199
  class ApiOperationController {
183
200
  private readonly bindings = new WeakMap<object, Set<OperationBinding>>();
184
201
 
185
- bind(component: object, run: () => void): () => void {
186
- const binding: OperationBinding = { component, run };
202
+ bind(component: object, run: () => Promise<unknown>): () => void {
203
+ const binding: OperationBinding = { run };
187
204
  let componentBindings = this.bindings.get(component);
188
205
  if (!componentBindings) {
189
206
  componentBindings = new Set();
@@ -201,8 +218,13 @@ class ApiOperationController {
201
218
  };
202
219
  }
203
220
 
204
- run(component: object): void {
205
- this.bindings.get(component)?.forEach(({ run }) => run());
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);
206
228
  }
207
229
  }
208
230
 
@@ -220,8 +242,10 @@ export type GetDecoratorHandle<T, Host extends object = object> =
220
242
  *
221
243
  * Le handle peut être conservé :
222
244
  * `const usersGet = get(new Endpoint<User[]>("users"))`.
245
+ *
246
+ * La Promise est résolue après la fin du GET.
223
247
  */
224
- refetch(component: Host): void;
248
+ refetch(component: Host): Promise<ApiResult<T> | undefined>;
225
249
  };
226
250
 
227
251
  export type ApiSendDecoratorHandle<T, Host extends object = object> =
@@ -230,19 +254,22 @@ export type ApiSendDecoratorHandle<T, Host extends object = object> =
230
254
  * Relance l'opération d'écriture avec le body courant.
231
255
  *
232
256
  * Disponible avec `post`, `put` et `patch`.
257
+ * La Promise est résolue après la fin de l'opération.
233
258
  */
234
- send(component: Host): void;
259
+ send(component: Host): Promise<ApiResult<T> | undefined>;
235
260
  };
236
261
 
237
262
  export type ApiMethodActionOptions = {
238
263
  /**
239
264
  * Moment du déclenchement par rapport à la méthode.
240
- * `after` est la valeur par défaut ; avec une Promise, l'action attend sa résolution.
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.
241
268
  */
242
269
  when?: "before" | "after";
243
270
  };
244
271
 
245
- type ApiMethod = (this: object, ...args: any[]) => unknown;
272
+ type ApiMethod = (this: object, ...args: unknown[]) => unknown;
246
273
 
247
274
  function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
248
275
  return (
@@ -253,7 +280,7 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
253
280
  }
254
281
 
255
282
  function createApiMethodActionDecorator(
256
- action: (component: object) => void,
283
+ action: (component: object) => Promise<unknown>,
257
284
  options?: ApiMethodActionOptions,
258
285
  ): MethodDecorator {
259
286
  const when = options?.when ?? "after";
@@ -262,22 +289,19 @@ function createApiMethodActionDecorator(
262
289
  const original = descriptor.value as ApiMethod | undefined;
263
290
  if (!original) return;
264
291
 
265
- const wrapped = function (this: object, ...args: any[]) {
292
+ const wrapped = function (this: object, ...args: unknown[]) {
266
293
  if (when === "before") {
267
- action(this);
268
- return original.apply(this, args);
294
+ return action(this).then(() => original.apply(this, args));
269
295
  }
270
296
 
271
297
  const result = original.apply(this, args);
272
298
  if (isPromiseLike(result)) {
273
299
  return result.then((value) => {
274
- action(this);
275
- return value;
300
+ return action(this).then(() => value);
276
301
  });
277
302
  }
278
303
 
279
- action(this);
280
- return result;
304
+ return action(this).then(() => result);
281
305
  };
282
306
  descriptor.value = wrapped as unknown as typeof descriptor.value;
283
307
  };
@@ -362,7 +386,7 @@ function detachConfigPublisher(state: ApiGetState): void {
362
386
  * payload?: ApiResult<User[]>;
363
387
  *
364
388
  * refresh() {
365
- * usersGet.refetch(this);
389
+ * await usersGet.refetch(this);
366
390
  * }
367
391
  *
368
392
  * @example
@@ -467,18 +491,18 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
467
491
  if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
468
492
  state.refetchTimeoutId = setTimeout(() => {
469
493
  state.refetchTimeoutId = null;
470
- runFetch();
494
+ void runFetch().catch(() => {});
471
495
  }, state.refetchEveryMs);
472
496
  };
473
497
 
474
- const runFetch = () => {
498
+ const runFetch = async (): Promise<ApiResult<T> | undefined> => {
475
499
  const resolution = isDynamicPath
476
500
  ? resolveDynamicPath(component, pathTemplate, pathOptions)
477
501
  : { ready: true, path: pathTemplate };
478
502
  if (!resolution.ready || !resolution.path) {
479
503
  comp[propertyKey] = undefined;
480
504
  scheduleRefetch();
481
- return;
505
+ return undefined;
482
506
  }
483
507
  let config: APIConfiguration | null = null;
484
508
  if (usesPublisherConfig && configurationKeyPath) {
@@ -490,7 +514,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
490
514
  if (!configRes.ready || !configRes.path) {
491
515
  comp[propertyKey] = undefined;
492
516
  scheduleRefetch();
493
- return;
517
+ return undefined;
494
518
  }
495
519
  const configPublisher = getPublisherFromPath(configRes.path);
496
520
  config = readApiConfigurationFromPublisher(configPublisher);
@@ -499,7 +523,10 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
499
523
  }
500
524
  if (!isScopedConfigurationReady(config)) {
501
525
  if (!usesPublisherConfig && !state.scopeWatchCleanup) {
502
- const scopeWatch = watchScopedConfiguration(component, runFetch);
526
+ const scopeWatch = watchScopedConfiguration(
527
+ component,
528
+ scheduleFetchAndIgnoreErrors,
529
+ );
503
530
  state.scopeWatchCleanup = scopeWatch;
504
531
  state.cleanupWatchers.push(() => {
505
532
  scopeWatch();
@@ -507,20 +534,21 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
507
534
  });
508
535
  }
509
536
  scheduleRefetch();
510
- return;
537
+ return undefined;
511
538
  }
512
539
  const generation = ++state.requestGeneration;
513
540
  const api = new API(config);
514
- void api
515
- .getDetailed<T>(resolution.path)
516
- .then((payload?: ApiResult<T>) => {
517
- if (generation !== state.requestGeneration) return;
518
- comp[propertyKey] = payload;
519
- scheduleRefetch();
520
- });
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;
521
546
  };
522
547
 
523
548
  const scheduleFetch = createAnimationFrameCoalescedRunner(runFetch);
549
+ const scheduleFetchAndIgnoreErrors = () => {
550
+ void scheduleFetch().catch(() => {});
551
+ };
524
552
  state.cleanupOperation = operationController.bind(
525
553
  component,
526
554
  scheduleFetch,
@@ -539,7 +567,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
539
567
  }
540
568
  const publisher = getPublisherFromPath(triggerResolution.path);
541
569
  if (!publisher) return;
542
- const invalidateHandler = () => scheduleFetch();
570
+ const invalidateHandler = scheduleFetchAndIgnoreErrors;
543
571
  publisher.onInvalidate(invalidateHandler);
544
572
  state.triggerPublisher = publisher;
545
573
  state.triggerInvalidateHandler = invalidateHandler;
@@ -562,7 +590,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
562
590
  comp[propertyKey] = undefined;
563
591
  return;
564
592
  }
565
- const mutationHandler = () => scheduleFetch();
593
+ const mutationHandler = scheduleFetchAndIgnoreErrors;
566
594
  publisher.onInternalMutation(mutationHandler);
567
595
  state.configPublisher = publisher;
568
596
  state.configMutationHandler = mutationHandler;
@@ -572,7 +600,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
572
600
  rebindTriggerPublisher();
573
601
  rebindPublisherConfig();
574
602
  if (!usesPublisherConfig) {
575
- scheduleFetch();
603
+ scheduleFetchAndIgnoreErrors();
576
604
  }
577
605
  };
578
606
 
@@ -597,13 +625,13 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
597
625
  getDynamicWatchKeys.hooked,
598
626
  component,
599
627
  dependency,
600
- () => scheduleFetch(),
628
+ scheduleFetchAndIgnoreErrors,
601
629
  );
602
630
  state.cleanupWatchers.push(unsubscribe);
603
631
  }
604
632
  }
605
633
  rebindTriggerPublisher();
606
- runFetch();
634
+ void runFetch().catch(() => {});
607
635
  };
608
636
  state.cleanupWatchers.push(
609
637
  scheduleAfterHostReady(component, startScopedFetch),
@@ -628,7 +656,9 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
628
656
  } as GetDecoratorHandle<T, object>;
629
657
 
630
658
  decorator.refetch = (component: object) => {
631
- operationController.run(component);
659
+ return operationController.run(component) as Promise<
660
+ ApiResult<T> | undefined
661
+ >;
632
662
  };
633
663
 
634
664
  return decorator;
@@ -823,18 +853,18 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
823
853
  if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
824
854
  state.refetchTimeoutId = setTimeout(() => {
825
855
  state.refetchTimeoutId = null;
826
- runSend();
856
+ void runSend().catch(() => {});
827
857
  }, state.refetchEveryMs);
828
858
  };
829
859
 
830
- const runSend = () => {
860
+ const runSend = async (): Promise<ApiResult<unknown> | undefined> => {
831
861
  const resolution = isDynamicPath
832
862
  ? resolveDynamicPath(component, pathTemplate, pathOptions)
833
863
  : { ready: true, path: pathTemplate };
834
864
  if (!resolution.ready || !resolution.path) {
835
865
  comp[propertyKey] = undefined;
836
866
  scheduleRefetch();
837
- return;
867
+ return undefined;
838
868
  }
839
869
 
840
870
  const bodyResolution = resolveDynamicPath(
@@ -845,7 +875,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
845
875
  if (!bodyResolution.ready || !bodyResolution.path) {
846
876
  comp[propertyKey] = undefined;
847
877
  scheduleRefetch();
848
- return;
878
+ return undefined;
849
879
  }
850
880
 
851
881
  const bodyPublisher = getPublisherFromPath(bodyResolution.path);
@@ -855,7 +885,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
855
885
  (body === null || body === undefined)
856
886
  ) {
857
887
  scheduleRefetch();
858
- return;
888
+ return undefined;
859
889
  }
860
890
 
861
891
  let config: APIConfiguration | null = null;
@@ -868,7 +898,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
868
898
  if (!configRes.ready || !configRes.path) {
869
899
  comp[propertyKey] = undefined;
870
900
  scheduleRefetch();
871
- return;
901
+ return undefined;
872
902
  }
873
903
  const configPublisher = getPublisherFromPath(configRes.path);
874
904
  config = readApiConfigurationFromPublisher(configPublisher);
@@ -878,7 +908,10 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
878
908
 
879
909
  if (!isScopedConfigurationReady(config)) {
880
910
  if (!usesPublisherConfig && !state.scopeWatchCleanup) {
881
- const scopeWatch = watchScopedConfiguration(component, runSend);
911
+ const scopeWatch = watchScopedConfiguration(
912
+ component,
913
+ scheduleSendAndIgnoreErrors,
914
+ );
882
915
  state.scopeWatchCleanup = scopeWatch;
883
916
  state.cleanupWatchers.push(() => {
884
917
  scopeWatch();
@@ -886,21 +919,22 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
886
919
  });
887
920
  }
888
921
  scheduleRefetch();
889
- return;
922
+ return undefined;
890
923
  }
891
924
 
892
925
  const generation = ++state.requestGeneration;
893
926
  const api = new API(config);
894
- void callDetailed(api, resolution.path, body).then(
895
- (payload?: ApiResult<unknown>) => {
896
- if (generation !== state.requestGeneration) return;
897
- comp[propertyKey] = payload;
898
- scheduleRefetch();
899
- },
900
- );
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;
901
932
  };
902
933
 
903
934
  const scheduleSend = createAnimationFrameCoalescedRunner(runSend);
935
+ const scheduleSendAndIgnoreErrors = () => {
936
+ void scheduleSend().catch(() => {});
937
+ };
904
938
  state.cleanupOperation = operationController.bind(
905
939
  component,
906
940
  scheduleSend,
@@ -921,7 +955,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
921
955
  }
922
956
  const publisher = getPublisherFromPath(bodyResolution.path);
923
957
  if (!publisher) return;
924
- const mutationHandler = () => scheduleSend();
958
+ const mutationHandler = scheduleSendAndIgnoreErrors;
925
959
  publisher.onInternalMutation(mutationHandler);
926
960
  state.bodyPublisher = publisher;
927
961
  state.bodyMutationHandler = mutationHandler;
@@ -940,7 +974,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
940
974
  }
941
975
  const publisher = getPublisherFromPath(triggerResolution.path);
942
976
  if (!publisher) return;
943
- const invalidateHandler = () => scheduleSend();
977
+ const invalidateHandler = scheduleSendAndIgnoreErrors;
944
978
  publisher.onInvalidate(invalidateHandler);
945
979
  state.triggerPublisher = publisher;
946
980
  state.triggerInvalidateHandler = invalidateHandler;
@@ -963,7 +997,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
963
997
  comp[propertyKey] = undefined;
964
998
  return;
965
999
  }
966
- const mutationHandler = () => scheduleSend();
1000
+ const mutationHandler = scheduleSendAndIgnoreErrors;
967
1001
  publisher.onInternalMutation(mutationHandler);
968
1002
  state.configPublisher = publisher;
969
1003
  state.configMutationHandler = mutationHandler;
@@ -974,7 +1008,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
974
1008
  rebindTriggerPublisher();
975
1009
  rebindPublisherConfig();
976
1010
  if (state.autoPostOnBodyMutation) {
977
- scheduleSend();
1011
+ scheduleSendAndIgnoreErrors();
978
1012
  }
979
1013
  };
980
1014
 
@@ -1003,7 +1037,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
1003
1037
  getDynamicWatchKeys.hooked,
1004
1038
  component,
1005
1039
  dependency,
1006
- () => scheduleSend(),
1040
+ scheduleSendAndIgnoreErrors,
1007
1041
  );
1008
1042
  state.cleanupWatchers.push(unsubscribe);
1009
1043
  }
@@ -1011,7 +1045,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
1011
1045
  rebindBodyPublisher();
1012
1046
  rebindTriggerPublisher();
1013
1047
  if (state.autoPostOnBodyMutation) {
1014
- scheduleSend();
1048
+ scheduleSendAndIgnoreErrors();
1015
1049
  }
1016
1050
  };
1017
1051
  state.cleanupWatchers.push(
@@ -1038,7 +1072,9 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
1038
1072
  } as ApiSendDecoratorHandle<T, object>;
1039
1073
 
1040
1074
  decorator.send = (component: object) => {
1041
- operationController.run(component);
1075
+ return operationController.run(component) as Promise<
1076
+ ApiResult<T> | undefined
1077
+ >;
1042
1078
  };
1043
1079
 
1044
1080
  return decorator;
@@ -1061,7 +1097,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
1061
1097
  * @save
1062
1098
  * payload?: ApiResult<User>;
1063
1099
  * // Dans un handler du composant :
1064
- * save.send(this);
1100
+ * await save.send(this);
1065
1101
  */
1066
1102
  export function post<T, B, Ue = any>(
1067
1103
  endpoint: Endpoint<T, Ue>,
@@ -37,15 +37,20 @@ const usersGet = get(new Endpoint<User[]>("users"));
37
37
  @state()
38
38
  payload?: ApiResult<User[]>;
39
39
 
40
- refresh() {
41
- usersGet.refetch(this);
40
+ async refresh() {
41
+ const payload = await usersGet.refetch(this);
42
+ console.log(payload?.result);
42
43
  }
43
44
  ```
44
45
 
46
+ `refetch(component)` returns a `Promise<ApiResult<T> | undefined>` that resolves
47
+ after the GET has completed.
48
+
45
49
  ### From a method decorator
46
50
 
47
51
  The refetch can decorate a local method. The default is `after`; if the
48
- method returns a Promise, the GET waits for its resolution.
52
+ method returns a Promise, the GET waits for its resolution. The decorated method
53
+ call also waits for the refetch to complete.
49
54
 
50
55
  ```typescript
51
56
  import { refetch } from "@supersoniks/concorde/decorators";
@@ -56,12 +61,12 @@ async reload() {
56
61
  }
57
62
  ```
58
63
 
59
- Use `{ when: "before" }` when the GET must be scheduled before the method:
64
+ Use `{ when: "before" }` when the GET must complete before the method runs:
60
65
 
61
66
  ```typescript
62
67
  @refetch(usersGet, { when: "before" })
63
68
  reloadBefore() {
64
- // The refetch is scheduled before this method runs.
69
+ // The refetch has completed before this method runs.
65
70
  }
66
71
  ```
67
72
 
@@ -76,6 +81,7 @@ configuration key is provided.
76
81
  - `skipEmptyPlaceholder`: blocks the GET when `${prop}` is `""`.
77
82
  - `refetchEveryMs`: automatically runs the GET again after this interval
78
83
  (milliseconds). Omit the option, or use `0`, to disable polling.
84
+ - `triggerKey`: a publisher whose `invalidate()` runs the GET again.
79
85
 
80
86
  With a configuration key, use:
81
87
 
@@ -88,6 +94,7 @@ With a configuration key, use:
88
94
  - when the component connects and configuration is available;
89
95
  - when a property used in `${...}` changes;
90
96
  - when a configuration publisher changes;
97
+ - when the `triggerKey` publisher is invalidated;
91
98
  - after each response when `refetchEveryMs` is set.
92
99
 
93
100
  See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path) for
@@ -19,11 +19,15 @@ payload?: ApiResult<User>;
19
19
  To run the operation again from the component:
20
20
 
21
21
  ```typescript
22
- savePatch() {
23
- patchUser.send(this);
22
+ async savePatch() {
23
+ await patchUser.send(this);
24
24
  }
25
25
  ```
26
26
 
27
+ `send(component)` returns a Promise resolved after the PATCH completes. See
28
+ [@post](#docs/_decorators/post.md/post) for the method-decorator form and the
29
+ `when: "before"` option.
30
+
27
31
  The PATCH is also sent automatically when the body mutates, unless
28
32
  `autoPostOnBodyMutation: false` is set. The available options are described in
29
33
  [@post](#docs/_decorators/post.md/post):
@@ -32,6 +36,7 @@ The PATCH is also sent automatically when the body mutates, unless
32
36
  - `skipIfBodyMissing`
33
37
  - `refetchEveryMs`
34
38
  - `skipEmptyPlaceholder`
39
+ - `triggerKey`
35
40
 
36
41
  Pass explicit API configuration as the third argument:
37
42
 
@@ -32,18 +32,21 @@ Keep the decorator to access `send(component)`. The body is read again when the
32
32
  operation runs.
33
33
 
34
34
  ```typescript
35
- saveAgain() {
36
- createUser.send(this);
35
+ async saveAgain() {
36
+ const payload = await createUser.send(this);
37
+ console.log(payload?.result);
37
38
  }
38
39
  ```
39
40
 
40
- `send()` is useful for “Save” or “Retry” buttons. It can repeat an operation
41
- with side effects.
41
+ `send()` returns a `Promise<ApiResult<T> | undefined>` that resolves after the
42
+ operation completes. It is useful for “Save” or “Retry” buttons and can repeat
43
+ an operation with side effects.
42
44
 
43
45
  ### From a method decorator
44
46
 
45
47
  The send action can decorate a local method. It runs after the method by
46
- default and waits for an eventual Promise to resolve:
48
+ default, waits for an eventual Promise to resolve, and the decorated method
49
+ call waits for the send to complete:
47
50
 
48
51
  ```typescript
49
52
  import { send } from "@supersoniks/concorde/decorators";
@@ -54,12 +57,12 @@ async save() {
54
57
  }
55
58
  ```
56
59
 
57
- Use `{ when: "before" }` when the body must be sent before the method runs:
60
+ Use `{ when: "before" }` when the send must complete before the method runs:
58
61
 
59
62
  ```typescript
60
63
  @send(createUser, { when: "before" })
61
64
  saveBefore() {
62
- // The POST is scheduled before this method runs.
65
+ // The POST has completed before this method runs.
63
66
  }
64
67
  ```
65
68
 
@@ -80,6 +83,7 @@ is provided.
80
83
  repeating the operation is intended.
81
84
  - `skipEmptyPlaceholder`: block the request when a `${...}` placeholder is
82
85
  `""`.
86
+ - `triggerKey`: a publisher whose `invalidate()` sends the POST again.
83
87
 
84
88
  With a configuration key:
85
89
 
@@ -96,6 +100,7 @@ dynamic path details.
96
100
  - after a body mutation, unless `autoPostOnBodyMutation: false`;
97
101
  - when a property used in `${...}` changes;
98
102
  - when a configuration publisher changes;
103
+ - when the `triggerKey` publisher is invalidated;
99
104
  - when `send()` is called;
100
105
  - after each response when `refetchEveryMs` is set.
101
106