@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.
- package/ai/cursor/rules/concorde.mdc +7 -0
- package/ai/jetbrains/rules/concorde.md +6 -0
- package/ai/skills/concorde/SKILL.md +48 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +299 -292
- package/concorde-core.es.js +10139 -6035
- package/dist/concorde-core.bundle.js +299 -292
- package/dist/concorde-core.es.js +10139 -6035
- package/dist/docs-mock-api-sw.js +110 -0
- package/dist/docs-mock-api-sw.js.map +3 -3
- package/dist/js/core/components/functional/functional.js +1 -0
- package/dist/js/core/components/functional/functional.js.map +1 -1
- package/dist/js/core/components/functional/jsonata/jsonata.js +119 -0
- package/dist/js/core/components/functional/jsonata/jsonata.js.map +1 -0
- package/dist/js/core/components/ui/button/button.js +1 -1
- package/dist/js/core/components/ui/button/button.js.map +1 -1
- package/dist/js/core/decorators/Subscriber.js +1 -0
- package/dist/js/core/decorators/Subscriber.js.map +1 -1
- package/dist/js/core/decorators/api.js +108 -16
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/decorators/subscriber/derive.js +60 -0
- package/dist/js/core/decorators/subscriber/derive.js.map +1 -0
- package/dist/js/core/utils/api.js +29 -8
- package/dist/js/core/utils/api.js.map +1 -1
- package/dist/js/core/utils/derive.js +246 -0
- package/dist/js/core/utils/derive.js.map +1 -0
- package/dist/js/core/utils/jsonataHelpers.js +69 -0
- package/dist/js/core/utils/jsonataHelpers.js.map +1 -0
- package/dist/js/core/utils/jsonataRuntime.js +75 -0
- package/dist/js/core/utils/jsonataRuntime.js.map +1 -0
- package/dist/js/utils.js +2 -0
- package/dist/js/utils.js.map +1 -1
- package/dist/types/core/components/functional/functional.d.ts +1 -0
- package/dist/types/core/components/functional/functional.d.ts.map +1 -1
- package/dist/types/core/components/functional/jsonata/jsonata.d.ts +29 -0
- package/dist/types/core/components/functional/jsonata/jsonata.d.ts.map +1 -0
- package/dist/types/core/components/ui/button/button.d.ts +1 -1
- package/dist/types/core/components/ui/button/button.d.ts.map +1 -1
- package/dist/types/core/components/ui/form/checkbox/checkbox.d.ts +3 -3
- package/dist/types/core/components/ui/form/input/input.d.ts +3 -3
- package/dist/types/core/components/ui/form/input-autocomplete/input-autocomplete.d.ts +3 -3
- package/dist/types/core/components/ui/form/textarea/textarea.d.ts +3 -3
- package/dist/types/core/decorators/Subscriber.d.ts +2 -0
- package/dist/types/core/decorators/Subscriber.d.ts.map +1 -1
- package/dist/types/core/decorators/api.d.ts +21 -12
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/decorators/subscriber/derive.d.ts +30 -0
- package/dist/types/core/decorators/subscriber/derive.d.ts.map +1 -0
- package/dist/types/core/utils/api.d.ts +1 -1
- package/dist/types/core/utils/api.d.ts.map +1 -1
- package/dist/types/core/utils/derive.d.ts +40 -0
- package/dist/types/core/utils/derive.d.ts.map +1 -0
- package/dist/types/core/utils/jsonataHelpers.d.ts +15 -0
- package/dist/types/core/utils/jsonataHelpers.d.ts.map +1 -0
- package/dist/types/core/utils/jsonataRuntime.d.ts +13 -0
- package/dist/types/core/utils/jsonataRuntime.d.ts.map +1 -0
- package/dist/types/mixins.d.ts +3 -3
- package/dist/types/utils.d.ts +2 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/docs/assets/{index-DURr6O71.js → index-DpIkksht.js} +652 -479
- package/docs/crawl/docs/_decorators/get.html +27 -6
- package/docs/crawl/docs/_decorators/patch.html +6 -0
- package/docs/crawl/docs/_decorators/post.html +8 -0
- package/docs/crawl/docs/_decorators/put.html +6 -0
- package/docs/crawl/hubs/decorators.html +36 -31
- package/docs/crawl/hubs/remote-data.html +36 -31
- package/docs/index.html +1 -1
- package/docs/llms-full.txt +47 -3
- package/docs/src/docs/_decorators/get.md +31 -0
- package/docs/src/docs/_decorators/patch.md +9 -0
- package/docs/src/docs/_decorators/post.md +11 -0
- package/docs/src/docs/_decorators/put.md +9 -0
- package/docs/src/docs/search/docs-search.json +120 -5
- package/docs/src/tsconfig.json +268 -268
- package/package.json +28 -3
- package/public/docs-mock-api-sw.js +110 -0
- package/public/docs-mock-api-sw.js.map +3 -3
- package/src/core/components/functional/functional.ts +1 -0
- package/src/core/components/functional/jsonata/jsonata.md +56 -0
- package/src/core/components/functional/jsonata/jsonata.spec.ts +202 -0
- package/src/core/components/functional/jsonata/jsonata.ts +116 -0
- package/src/core/components/ui/button/button.ts +2 -2
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/api.post.spec.ts +99 -4
- package/src/core/decorators/api.spec.ts +90 -0
- package/src/core/decorators/api.ts +212 -92
- package/src/core/decorators/subscriber/derive.ts +95 -0
- package/src/core/utils/api.spec.ts +73 -0
- package/src/core/utils/api.ts +44 -11
- package/src/core/utils/derive.spec.ts +190 -0
- package/src/core/utils/derive.ts +357 -0
- package/src/core/utils/jsonataHelpers.ts +100 -0
- package/src/core/utils/jsonataRuntime.ts +88 -0
- package/src/docs/_core-concept/dataFlow.md +9 -0
- package/src/docs/_decorators/get.md +31 -0
- package/src/docs/_decorators/patch.md +9 -0
- package/src/docs/_decorators/post.md +11 -0
- package/src/docs/_decorators/put.md +9 -0
- package/src/docs/_misc/docs-mock-api.md +3 -0
- package/src/docs/components/docs-demo-sources.ts +40 -0
- package/src/docs/docs.ts +1 -0
- package/src/docs/example/decorators-demo-post.ts +70 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +59 -0
- package/src/docs/example/docs-jsonata-demos.ts +128 -0
- package/src/docs/mock-api/fixtures-jsonata.ts +131 -0
- package/src/docs/mock-api/router.ts +26 -0
- package/src/docs/navigation/navigation.ts +4 -0
- package/src/docs/search/docs-search.json +300 -5
- package/src/tsconfig.json +18 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- 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
|
+
});
|
package/src/core/utils/api.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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>(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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(
|
|
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
|
|
457
|
+
const sharedResult = await API.loadingGetPromises.get(mapKey);
|
|
427
458
|
API.loadingGetPromises.delete(mapKey);
|
|
428
|
-
|
|
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
|
+
});
|