@supersoniks/concorde 4.9.92 → 4.9.94

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 (111) hide show
  1. package/ai/cursor/rules/concorde.mdc +7 -0
  2. package/ai/jetbrains/rules/concorde.md +6 -0
  3. package/ai/skills/concorde/SKILL.md +48 -0
  4. package/build-infos.json +1 -1
  5. package/concorde-core.bundle.js +299 -292
  6. package/concorde-core.es.js +10139 -6035
  7. package/dist/concorde-core.bundle.js +299 -292
  8. package/dist/concorde-core.es.js +10139 -6035
  9. package/dist/docs-mock-api-sw.js +110 -0
  10. package/dist/docs-mock-api-sw.js.map +3 -3
  11. package/dist/js/core/components/functional/functional.js +1 -0
  12. package/dist/js/core/components/functional/functional.js.map +1 -1
  13. package/dist/js/core/components/functional/jsonata/jsonata.js +119 -0
  14. package/dist/js/core/components/functional/jsonata/jsonata.js.map +1 -0
  15. package/dist/js/core/components/ui/button/button.js +1 -1
  16. package/dist/js/core/components/ui/button/button.js.map +1 -1
  17. package/dist/js/core/decorators/Subscriber.js +1 -0
  18. package/dist/js/core/decorators/Subscriber.js.map +1 -1
  19. package/dist/js/core/decorators/api.js +108 -16
  20. package/dist/js/core/decorators/api.js.map +1 -1
  21. package/dist/js/core/decorators/subscriber/derive.js +60 -0
  22. package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
  23. package/dist/js/core/utils/api.js +29 -8
  24. package/dist/js/core/utils/api.js.map +1 -1
  25. package/dist/js/core/utils/derive.js +246 -0
  26. package/dist/js/core/utils/derive.js.map +1 -0
  27. package/dist/js/core/utils/jsonataHelpers.js +69 -0
  28. package/dist/js/core/utils/jsonataHelpers.js.map +1 -0
  29. package/dist/js/core/utils/jsonataRuntime.js +75 -0
  30. package/dist/js/core/utils/jsonataRuntime.js.map +1 -0
  31. package/dist/js/utils.js +2 -0
  32. package/dist/js/utils.js.map +1 -1
  33. package/dist/types/core/components/functional/functional.d.ts +1 -0
  34. package/dist/types/core/components/functional/functional.d.ts.map +1 -1
  35. package/dist/types/core/components/functional/jsonata/jsonata.d.ts +29 -0
  36. package/dist/types/core/components/functional/jsonata/jsonata.d.ts.map +1 -0
  37. package/dist/types/core/components/ui/button/button.d.ts +1 -1
  38. package/dist/types/core/components/ui/button/button.d.ts.map +1 -1
  39. package/dist/types/core/components/ui/form/checkbox/checkbox.d.ts +3 -3
  40. package/dist/types/core/components/ui/form/input/input.d.ts +3 -3
  41. package/dist/types/core/components/ui/form/input-autocomplete/input-autocomplete.d.ts +3 -3
  42. package/dist/types/core/components/ui/form/textarea/textarea.d.ts +3 -3
  43. package/dist/types/core/decorators/Subscriber.d.ts +2 -0
  44. package/dist/types/core/decorators/Subscriber.d.ts.map +1 -1
  45. package/dist/types/core/decorators/api.d.ts +21 -12
  46. package/dist/types/core/decorators/api.d.ts.map +1 -1
  47. package/dist/types/core/decorators/subscriber/derive.d.ts +30 -0
  48. package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
  49. package/dist/types/core/utils/api.d.ts +1 -1
  50. package/dist/types/core/utils/api.d.ts.map +1 -1
  51. package/dist/types/core/utils/derive.d.ts +40 -0
  52. package/dist/types/core/utils/derive.d.ts.map +1 -0
  53. package/dist/types/core/utils/jsonataHelpers.d.ts +15 -0
  54. package/dist/types/core/utils/jsonataHelpers.d.ts.map +1 -0
  55. package/dist/types/core/utils/jsonataRuntime.d.ts +13 -0
  56. package/dist/types/core/utils/jsonataRuntime.d.ts.map +1 -0
  57. package/dist/types/mixins.d.ts +3 -3
  58. package/dist/types/utils.d.ts +2 -0
  59. package/dist/types/utils.d.ts.map +1 -1
  60. package/docs/assets/{index-DURr6O71.js → index-DpIkksht.js} +652 -479
  61. package/docs/crawl/docs/_decorators/get.html +27 -6
  62. package/docs/crawl/docs/_decorators/patch.html +6 -0
  63. package/docs/crawl/docs/_decorators/post.html +8 -0
  64. package/docs/crawl/docs/_decorators/put.html +6 -0
  65. package/docs/crawl/hubs/decorators.html +36 -31
  66. package/docs/crawl/hubs/remote-data.html +36 -31
  67. package/docs/index.html +1 -1
  68. package/docs/llms-full.txt +47 -3
  69. package/docs/src/docs/_decorators/get.md +31 -0
  70. package/docs/src/docs/_decorators/patch.md +9 -0
  71. package/docs/src/docs/_decorators/post.md +11 -0
  72. package/docs/src/docs/_decorators/put.md +9 -0
  73. package/docs/src/docs/search/docs-search.json +120 -5
  74. package/docs/src/tsconfig.json +268 -268
  75. package/package.json +28 -3
  76. package/public/docs-mock-api-sw.js +110 -0
  77. package/public/docs-mock-api-sw.js.map +3 -3
  78. package/src/core/components/functional/functional.ts +1 -0
  79. package/src/core/components/functional/jsonata/jsonata.md +56 -0
  80. package/src/core/components/functional/jsonata/jsonata.spec.ts +202 -0
  81. package/src/core/components/functional/jsonata/jsonata.ts +116 -0
  82. package/src/core/components/ui/button/button.ts +2 -2
  83. package/src/core/decorators/Subscriber.ts +2 -0
  84. package/src/core/decorators/api.post.spec.ts +99 -4
  85. package/src/core/decorators/api.spec.ts +90 -0
  86. package/src/core/decorators/api.ts +212 -92
  87. package/src/core/decorators/subscriber/derive.ts +95 -0
  88. package/src/core/utils/api.spec.ts +73 -0
  89. package/src/core/utils/api.ts +44 -11
  90. package/src/core/utils/derive.spec.ts +190 -0
  91. package/src/core/utils/derive.ts +357 -0
  92. package/src/core/utils/jsonataHelpers.ts +100 -0
  93. package/src/core/utils/jsonataRuntime.ts +88 -0
  94. package/src/docs/_core-concept/dataFlow.md +9 -0
  95. package/src/docs/_decorators/get.md +31 -0
  96. package/src/docs/_decorators/patch.md +9 -0
  97. package/src/docs/_decorators/post.md +11 -0
  98. package/src/docs/_decorators/put.md +9 -0
  99. package/src/docs/_misc/docs-mock-api.md +3 -0
  100. package/src/docs/components/docs-demo-sources.ts +40 -0
  101. package/src/docs/docs.ts +1 -0
  102. package/src/docs/example/decorators-demo-post.ts +70 -0
  103. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +59 -0
  104. package/src/docs/example/docs-jsonata-demos.ts +128 -0
  105. package/src/docs/mock-api/fixtures-jsonata.ts +131 -0
  106. package/src/docs/mock-api/router.ts +26 -0
  107. package/src/docs/navigation/navigation.ts +4 -0
  108. package/src/docs/search/docs-search.json +300 -5
  109. package/src/tsconfig.json +18 -0
  110. package/src/tsconfig.tsbuildinfo +1 -1
  111. package/src/utils.ts +13 -0
@@ -0,0 +1,73 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import API, { type APIConfiguration } from "./api";
3
+
4
+ const baseConfig: APIConfiguration = {
5
+ serviceURL: "https://api.example.test",
6
+ token: null,
7
+ userName: null,
8
+ password: null,
9
+ authToken: null,
10
+ tokenProvider: null,
11
+ };
12
+
13
+ afterEach(() => {
14
+ vi.restoreAllMocks();
15
+ vi.unstubAllGlobals();
16
+ });
17
+
18
+ describe("API GET synchronization", () => {
19
+ it("deduplicates fetch while sharing the response metadata", async () => {
20
+ const response = new Response(JSON.stringify({ items: [1, 2] }), {
21
+ status: 200,
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ const fetchMock = vi.fn().mockResolvedValue(response);
25
+ vi.stubGlobal("fetch", fetchMock);
26
+
27
+ const firstApi = new API({
28
+ ...baseConfig,
29
+ serviceURL: "https://api.example.test/concurrent",
30
+ });
31
+ const secondApi = new API({
32
+ ...baseConfig,
33
+ serviceURL: "https://api.example.test/concurrent",
34
+ });
35
+
36
+ const [firstPayload, secondPayload] = await Promise.all([
37
+ firstApi.getDetailed<{ items: number[] }>("items"),
38
+ secondApi.getDetailed<{ items: number[] }>("items"),
39
+ ]);
40
+
41
+ expect(fetchMock).toHaveBeenCalledTimes(1);
42
+ expect(firstPayload?.response?.status).toBe(200);
43
+ expect(secondPayload?.response?.status).toBe(200);
44
+ expect(firstPayload?.result).toEqual({ items: [1, 2] });
45
+ expect(secondPayload?.result).toEqual({ items: [1, 2] });
46
+ });
47
+
48
+ it("permet un nouvel appel après une erreur réseau", async () => {
49
+ const response = new Response(JSON.stringify({ ok: true }), {
50
+ status: 200,
51
+ headers: { "Content-Type": "application/json" },
52
+ });
53
+ const fetchMock = vi
54
+ .fn()
55
+ .mockRejectedValueOnce(new Error("offline"))
56
+ .mockResolvedValueOnce(response);
57
+ vi.stubGlobal("fetch", fetchMock);
58
+
59
+ const api = new API({
60
+ ...baseConfig,
61
+ serviceURL: "https://api.example.test/recover",
62
+ });
63
+
64
+ await expect(api.getDetailed<{ ok: boolean }>("items")).resolves.toBe(
65
+ undefined,
66
+ );
67
+ const payload = await api.getDetailed<{ ok: boolean }>("items");
68
+
69
+ expect(fetchMock).toHaveBeenCalledTimes(2);
70
+ expect(payload?.response?.status).toBe(200);
71
+ expect(payload?.result).toEqual({ ok: true });
72
+ });
73
+ });
@@ -49,6 +49,11 @@ export type ApiResult<T> = {
49
49
  /** @deprecated Utiliser `ApiResult`. */
50
50
  export type ApiGetResult<T> = ApiResult<T>;
51
51
 
52
+ type SharedGetResult = {
53
+ processed: ResultTypeInterface;
54
+ response?: Response;
55
+ };
56
+
52
57
  /** Extrait le corps typé depuis le résultat traité par `handleResult`. */
53
58
  function extractTypedApiResult<T>(
54
59
  processed: ResultTypeInterface | null | undefined,
@@ -67,7 +72,10 @@ class API {
67
72
  /**
68
73
  * Ce tableau static permet de ne pas appeler plusieurs fois le même service lors d'appel concurrents en GET.
69
74
  */
70
- private static loadingGetPromises: Map<string, Promise<unknown>> = new Map();
75
+ private static loadingGetPromises: Map<
76
+ string,
77
+ Promise<SharedGetResult | null>
78
+ > = new Map();
71
79
  /**
72
80
  * L'url de base du service sans endpoint
73
81
  */
@@ -202,10 +210,14 @@ class API {
202
210
  if (httpCode === 498 && !API.failledTokenUpdates.has(this.serviceURL)) {
203
211
  this.handleInvalidToken(this.token);
204
212
  if (lastCall.apiMethod === "get") {
205
- result = await this[lastCall.apiMethod](
213
+ const retryResult = await this.get(
206
214
  lastCall.path,
207
215
  lastCall.additionalHeaders,
208
216
  );
217
+ if (retryResult == null) {
218
+ throw new Error("GET retry failed after token refresh");
219
+ }
220
+ result = retryResult;
209
221
  } else {
210
222
  result = await this[lastCall.apiMethod](
211
223
  lastCall.path,
@@ -384,13 +396,16 @@ class API {
384
396
  });
385
397
  }
386
398
 
387
- async get<T>(path: string, additionalHeaders?: HeadersInit) {
388
- await this.firstCallDone();
389
- if (this.blockUntilDone) {
390
- API.firstCallDoneFlags.set(this.serviceURL, "loading");
391
- }
399
+ async get<T>(
400
+ path: string,
401
+ additionalHeaders?: HeadersInit,
402
+ ): Promise<(T & ResultTypeInterface) | null> {
392
403
  const regExp = /dataProvider\((.*?)\)(.*?)$/;
393
404
  if (regExp.test(path)) {
405
+ if (this.blockUntilDone) {
406
+ await this.firstCallDone();
407
+ API.firstCallDoneFlags.set(this.serviceURL, "loading");
408
+ }
394
409
  const match = path.match(regExp);
395
410
  if (!match) throw new Error("dataProvider path is not valid");
396
411
  return await this.localGet<T>(match[1], match[2]);
@@ -405,9 +420,21 @@ class API {
405
420
  const mapKey = JSON.stringify({
406
421
  url: url,
407
422
  headers: headers,
423
+ credentials: this.credentials,
424
+ cache: this.cache,
425
+ keepAlive: this.keepAlive,
408
426
  });
427
+
428
+ // Les GET non bloquants doivent pouvoir rejoindre une requête déjà en vol.
429
+ // Sinon firstCallDone() sérialise les appels avant que la map de déduplication
430
+ // ne soit consultée, ce qui déclenche deux fetch pour deux consommateurs.
431
+ if (this.blockUntilDone) {
432
+ await this.firstCallDone();
433
+ API.firstCallDoneFlags.set(this.serviceURL, "loading");
434
+ }
435
+
409
436
  if (!API.loadingGetPromises.has(mapKey)) {
410
- const promise = new Promise(async (resolve) => {
437
+ const promise = new Promise<SharedGetResult | null>(async (resolve) => {
411
438
  try {
412
439
  const result = await fetch(url, {
413
440
  headers: headers,
@@ -416,16 +443,22 @@ class API {
416
443
  keepalive: this.keepAlive,
417
444
  });
418
445
  const handledResult = await this.handleResult(result, lastCall);
419
- resolve(handledResult);
446
+ resolve({
447
+ processed: handledResult,
448
+ response: this.lastResult,
449
+ });
420
450
  } catch (e) {
451
+ API.firstCallDoneFlags.set(this.serviceURL, "error");
421
452
  resolve(null);
422
453
  }
423
454
  });
424
455
  API.loadingGetPromises.set(mapKey, promise);
425
456
  }
426
- const result = (await API.loadingGetPromises.get(mapKey)) as T;
457
+ const sharedResult = await API.loadingGetPromises.get(mapKey);
427
458
  API.loadingGetPromises.delete(mapKey);
428
- return result as T & ResultTypeInterface;
459
+ if (!sharedResult) return null;
460
+ this.lastResult = sharedResult.response;
461
+ return sharedResult.processed as T & ResultTypeInterface;
429
462
  }
430
463
 
431
464
  /**
@@ -0,0 +1,190 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
3
+ import {
4
+ derive,
5
+ deriveStateKey,
6
+ } from "@supersoniks/concorde/core/utils/derive";
7
+ import { dp, set } from "@supersoniks/concorde/utils";
8
+
9
+ const sessionsKey = new DataProviderKey<
10
+ Array<{ title: string; revenue: number }>
11
+ >("derive-sessions");
12
+ const formKey = new DataProviderKey<{ top: number }>("derive-form");
13
+ const outKey = new DataProviderKey<Array<{ title: string; ca: number }>>(
14
+ "derive-out",
15
+ );
16
+ const outBKey = new DataProviderKey<unknown>("derive-out-b");
17
+
18
+ function wait(ms: number) {
19
+ return new Promise((r) => setTimeout(r, ms));
20
+ }
21
+
22
+ afterEach(() => {
23
+ // Nettoyage des DPs de test
24
+ set(sessionsKey, []);
25
+ set(formKey, { top: 5 });
26
+ set(outKey, []);
27
+ });
28
+
29
+ describe("derive", () => {
30
+ it("publie un résultat calculé à partir des entrées", async () => {
31
+ set(sessionsKey, [
32
+ { title: "A", revenue: 10 },
33
+ { title: "B", revenue: 30 },
34
+ ]);
35
+ set(formKey, { top: 1 });
36
+
37
+ const handle = derive({
38
+ inputs: { s: sessionsKey },
39
+ variables: formKey,
40
+ output: outKey,
41
+ debounceMs: 10,
42
+ compute: (i, v) =>
43
+ [...i.s]
44
+ .sort((a, b) => b.revenue - a.revenue)
45
+ .slice(0, v.top)
46
+ .map((x) => ({ title: x.title, ca: x.revenue })),
47
+ });
48
+
49
+ await wait(40);
50
+ expect(dp(outKey).get()).toEqual([{ title: "B", ca: 30 }]);
51
+ expect(dp(deriveStateKey(outKey)).get().status).toBe("ready");
52
+ handle.stop();
53
+ });
54
+
55
+ it("recalcule quand les variables changent (sans toucher aux entrées)", async () => {
56
+ set(sessionsKey, [
57
+ { title: "A", revenue: 10 },
58
+ { title: "B", revenue: 30 },
59
+ { title: "C", revenue: 20 },
60
+ ]);
61
+ set(formKey, { top: 2 });
62
+
63
+ const handle = derive({
64
+ inputs: { s: sessionsKey },
65
+ variables: formKey,
66
+ output: outKey,
67
+ debounceMs: 10,
68
+ compute: (i, v) =>
69
+ [...i.s]
70
+ .sort((a, b) => b.revenue - a.revenue)
71
+ .slice(0, Number(v.top))
72
+ .map((x) => ({ title: x.title, ca: x.revenue })),
73
+ });
74
+
75
+ await wait(40);
76
+ expect(dp(outKey).get()).toHaveLength(2);
77
+
78
+ // Comme sonic-input : écriture sur la sous-clé, pas set() racine
79
+ dp(formKey).top.set(1);
80
+ await wait(40);
81
+ expect(dp(outKey).get()).toEqual([{ title: "B", ca: 30 }]);
82
+ handle.stop();
83
+ });
84
+
85
+ it("debounce + génération : seul le dernier calcul compte", async () => {
86
+ set(sessionsKey, [{ title: "A", revenue: 1 }]);
87
+ let calls = 0;
88
+
89
+ const handle = derive({
90
+ inputs: { s: sessionsKey },
91
+ output: outKey,
92
+ debounceMs: 30,
93
+ compute: async (i) => {
94
+ calls += 1;
95
+ const n = calls;
96
+ await wait(40);
97
+ return i.s.map((x) => ({ title: x.title + n, ca: x.revenue }));
98
+ },
99
+ });
100
+
101
+ await wait(5);
102
+ set(sessionsKey, [{ title: "B", revenue: 2 }]);
103
+ await wait(5);
104
+ set(sessionsKey, [{ title: "C", revenue: 3 }]);
105
+ await wait(120);
106
+
107
+ const result = dp(outKey).get();
108
+ expect(result?.[0]?.title).toMatch(/^C/);
109
+ handle.stop();
110
+ });
111
+
112
+ it("refuse un cycle direct sortie ∈ entrées", () => {
113
+ expect(() =>
114
+ derive({
115
+ inputs: { o: outKey },
116
+ output: outKey,
117
+ compute: (i) => i.o,
118
+ }),
119
+ ).toThrow(/Cycle/);
120
+ });
121
+
122
+ it("refuse un cycle en chaîne", () => {
123
+ const a = new DataProviderKey<number>("derive-cycle-a");
124
+ const b = new DataProviderKey<number>("derive-cycle-b");
125
+ const h1 = derive({
126
+ inputs: { a },
127
+ output: b,
128
+ debounceMs: 5,
129
+ compute: (i) => i.a + 1,
130
+ });
131
+ expect(() =>
132
+ derive({
133
+ inputs: { b },
134
+ output: a,
135
+ compute: (i) => (i.b as number) + 1,
136
+ }),
137
+ ).toThrow(/Cycle/);
138
+ h1.stop();
139
+ });
140
+
141
+ it("sortie readonly : écriture externe ignorée + warning", async () => {
142
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
143
+ set(sessionsKey, [{ title: "A", revenue: 5 }]);
144
+ const handle = derive({
145
+ inputs: { s: sessionsKey },
146
+ output: outKey,
147
+ debounceMs: 10,
148
+ compute: (i) => i.s.map((x) => ({ title: x.title, ca: x.revenue })),
149
+ });
150
+ await wait(40);
151
+ expect(dp(outKey).get()).toEqual([{ title: "A", ca: 5 }]);
152
+
153
+ set(outKey, [{ title: "HACK", ca: 0 }]);
154
+ await wait(10);
155
+ expect(dp(outKey).get()).toEqual([{ title: "A", ca: 5 }]);
156
+ expect(warn).toHaveBeenCalled();
157
+ warn.mockRestore();
158
+ handle.stop();
159
+ });
160
+
161
+ it("publie status error puis empty", async () => {
162
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
163
+ set(sessionsKey, [{ title: "A", revenue: 1 }]);
164
+ const handle = derive({
165
+ inputs: { s: sessionsKey },
166
+ output: outBKey,
167
+ debounceMs: 10,
168
+ compute: () => {
169
+ throw new Error("boom-jsonata");
170
+ },
171
+ });
172
+ await wait(40);
173
+ expect(dp(deriveStateKey(outBKey)).get()).toMatchObject({
174
+ status: "error",
175
+ error: expect.stringContaining("boom-jsonata"),
176
+ });
177
+ handle.stop();
178
+ errSpy.mockRestore();
179
+
180
+ const handle2 = derive({
181
+ inputs: { s: sessionsKey },
182
+ output: outKey,
183
+ debounceMs: 10,
184
+ compute: () => [],
185
+ });
186
+ await wait(40);
187
+ expect(dp(deriveStateKey(outKey)).get().status).toBe("empty");
188
+ handle2.stop();
189
+ });
190
+ });