@supersoniks/concorde 4.2.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +163 -0
  2. package/build-infos.json +1 -1
  3. package/concorde-core.bundle.js +175 -171
  4. package/concorde-core.es.js +2490 -2246
  5. package/dist/concorde-core.bundle.js +175 -171
  6. package/dist/concorde-core.es.js +2490 -2246
  7. package/package.json +22 -1
  8. package/php/get-challenge.php +34 -0
  9. package/php/some-service.php +42 -0
  10. package/scripts/pre-build.mjs +4 -0
  11. package/src/core/_types/endpoint.ts +4 -0
  12. package/src/core/_types/key.ts +1 -0
  13. package/src/core/components/functional/example/example.ts +38 -6
  14. package/src/core/decorators/Subscriber.ts +2 -0
  15. package/src/core/decorators/api.spec.ts +150 -0
  16. package/src/core/decorators/api.ts +244 -0
  17. package/src/core/decorators/subscriber/bind.ts +57 -145
  18. package/src/core/decorators/subscriber/dynamicPath.ts +77 -0
  19. package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +105 -0
  20. package/src/core/decorators/subscriber/onAssign.ts +11 -147
  21. package/src/core/decorators/subscriber/publish.spec.ts +21 -0
  22. package/src/core/decorators/subscriber/publish.ts +148 -0
  23. package/src/core/decorators/subscriber/publisherPath.ts +13 -0
  24. package/src/core/decorators/subscriber/subscribe.spec.ts +21 -0
  25. package/src/core/decorators/subscriber/subscribe.ts +32 -0
  26. package/src/core/decorators/subscriber/subscribe.type-test.ts +32 -0
  27. package/src/core/utils/api.ts +83 -15
  28. package/src/core/utils/dataProviderKey.spec.ts +34 -0
  29. package/src/core/utils/dataProviderKey.ts +86 -0
  30. package/src/core/utils/endpoint.spec.ts +41 -0
  31. package/src/core/utils/endpoint.ts +87 -0
  32. package/src/decorators.ts +14 -0
  33. package/src/docs/{_misc → _decorators}/ancestor-attribute.md +15 -31
  34. package/src/docs/_decorators/bind.md +164 -0
  35. package/src/docs/_decorators/get.md +65 -0
  36. package/src/docs/_decorators/publish.md +54 -0
  37. package/src/docs/_decorators/subscribe.md +36 -0
  38. package/src/docs/_misc/dataProviderKey.md +135 -0
  39. package/src/docs/_misc/endpoint.md +42 -0
  40. package/src/docs/example/decorators-demo-bind-demos.ts +210 -0
  41. package/src/docs/example/decorators-demo-geo.ts +45 -0
  42. package/src/docs/example/decorators-demo-init.ts +228 -0
  43. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +324 -0
  44. package/src/docs/example/decorators-demo.ts +12 -459
  45. package/src/docs/navigation/navigation.ts +27 -10
  46. package/src/docs/search/docs-search.json +1059 -609
  47. package/src/tsconfig-model.json +1 -1
  48. package/src/tsconfig.json +65 -1
  49. package/src/tsconfig.tsbuildinfo +1 -1
  50. package/src/utils.ts +8 -1
  51. package/vite.config.mts +11 -0
  52. package/src/docs/_misc/bind.md +0 -362
  53. /package/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
  54. /package/src/docs/{_misc → _decorators}/on-assign.md +0 -0
  55. /package/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -17,9 +17,12 @@
17
17
  "exports": {
18
18
  "./core/components/ui/icon/icons.json": "./src/core/components/ui/icon/icons.json",
19
19
  "./core/components/functional/sdui/default-library.json": "./src/core/components/functional/sdui/default-library.json",
20
+ "./dataProviderKey": "./src/core/utils/dataProviderKey.ts",
20
21
  "./*": "./src/*.ts",
21
22
  "./vite-config": "./vite/config.js",
22
23
  "./rollup-plugin-*": "./rollup/plugin-*.js",
24
+ "./_types/endpoint": "./src/core/_types/endpoint.ts",
25
+ "./_types/key": "./src/core/_types/key.ts",
23
26
  "./_types/types": "./src/core/_types/types.ts",
24
27
  "./date": "./src/core/components/functional/date/date.ts",
25
28
  "./functional/date": "./src/core/components/functional/date/date.ts",
@@ -271,13 +274,23 @@
271
274
  "./ui/ui": "./src/core/components/ui/ui.ts",
272
275
  "./core": "./src/core/core.ts",
273
276
  "./decorators/Subscriber": "./src/core/decorators/Subscriber.ts",
277
+ "./decorators/api.spec": "./src/core/decorators/api.spec.ts",
278
+ "./decorators/api": "./src/core/decorators/api.ts",
274
279
  "./decorators/lifecycle": "./src/core/decorators/lifecycle.ts",
275
280
  "./decorators/subscriber/ancestorAttribute": "./src/core/decorators/subscriber/ancestorAttribute.ts",
276
281
  "./decorators/subscriber/autoFill": "./src/core/decorators/subscriber/autoFill.ts",
277
282
  "./decorators/subscriber/autoSubscribe": "./src/core/decorators/subscriber/autoSubscribe.ts",
278
283
  "./decorators/subscriber/bind": "./src/core/decorators/subscriber/bind.ts",
279
284
  "./decorators/subscriber/common": "./src/core/decorators/subscriber/common.ts",
285
+ "./decorators/subscriber/dynamicPath": "./src/core/decorators/subscriber/dynamicPath.ts",
286
+ "./decorators/subscriber/dynamicPropertyWatch": "./src/core/decorators/subscriber/dynamicPropertyWatch.ts",
280
287
  "./decorators/subscriber/onAssign": "./src/core/decorators/subscriber/onAssign.ts",
288
+ "./decorators/subscriber/publish.spec": "./src/core/decorators/subscriber/publish.spec.ts",
289
+ "./decorators/subscriber/publish": "./src/core/decorators/subscriber/publish.ts",
290
+ "./decorators/subscriber/publisherPath": "./src/core/decorators/subscriber/publisherPath.ts",
291
+ "./decorators/subscriber/subscribe.spec": "./src/core/decorators/subscriber/subscribe.spec.ts",
292
+ "./decorators/subscriber/subscribe": "./src/core/decorators/subscriber/subscribe.ts",
293
+ "./decorators/subscriber/subscribe.type-test": "./src/core/decorators/subscriber/subscribe.type-test.ts",
281
294
  "./directives/DataProvider": "./src/core/directives/DataProvider.ts",
282
295
  "./directives/Wording": "./src/core/directives/Wording.ts",
283
296
  "./mixins/Fetcher": "./src/core/mixins/Fetcher.ts",
@@ -298,11 +311,19 @@
298
311
  "./utils/Utils": "./src/core/utils/Utils.ts",
299
312
  "./utils/aesCrypto": "./src/core/utils/aesCrypto.ts",
300
313
  "./utils/api": "./src/core/utils/api.ts",
314
+ "./utils/dataProviderKey.spec": "./src/core/utils/dataProviderKey.spec.ts",
315
+ "./utils/dataProviderKey": "./src/core/utils/dataProviderKey.ts",
316
+ "./utils/endpoint.spec": "./src/core/utils/endpoint.spec.ts",
317
+ "./utils/endpoint": "./src/core/utils/endpoint.ts",
301
318
  "./utils/route.spec": "./src/core/utils/route.spec.ts",
302
319
  "./utils/route": "./src/core/utils/route.ts",
303
320
  "./utils/url-pattern": "./src/core/utils/url-pattern.ts",
304
321
  "./code": "./src/docs/code.ts",
305
322
  "./docs": "./src/docs/docs.ts",
323
+ "./example/decorators-demo-bind-demos": "./src/docs/example/decorators-demo-bind-demos.ts",
324
+ "./example/decorators-demo-geo": "./src/docs/example/decorators-demo-geo.ts",
325
+ "./example/decorators-demo-init": "./src/docs/example/decorators-demo-init.ts",
326
+ "./example/decorators-demo-subscribe-publish-get-demos": "./src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
306
327
  "./example/decorators-demo": "./src/docs/example/decorators-demo.ts",
307
328
  "./example/users": "./src/docs/example/users.ts",
308
329
  "./header/header": "./src/docs/header/header.ts",
@@ -0,0 +1,34 @@
1
+ <?php
2
+ /* *
3
+ * Call get-challenge on auto-hosted latcha service at https://altcha.supersoniks.org
4
+ * */
5
+
6
+ // Autoriser toutes les origines
7
+ header("Access-Control-Allow-Origin: *");
8
+
9
+ // Autoriser les méthodes HTTP spécifiques
10
+ header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
11
+
12
+ // Autoriser certains en-têtes spécifiques
13
+ header("Access-Control-Allow-Headers: Content-Type, Authorization");
14
+
15
+ // Si la méthode est OPTIONS, terminer la requête ici
16
+ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
17
+ http_response_code(200);
18
+ exit();
19
+ }
20
+
21
+ function getChallenge($key){
22
+ $maxNumber=20000;
23
+ $queryString = $params = [
24
+ 'key' => $key,
25
+ 'maxNumber' => $maxNumber
26
+ ];
27
+ // Générer la chaîne de requête
28
+ $queryString = http_build_query($params);
29
+ $url = "https://altcha.supersoniks.org/get-challenge?key=".$queryString;
30
+ $response = file_get_contents($url);
31
+ return $response;
32
+ }
33
+
34
+ echo getChallenge($_GET['key']);
@@ -0,0 +1,42 @@
1
+ <?php
2
+ /* *
3
+ * Call verify-solution on auto-hosted latcha service at https://altcha.supersoniks.org
4
+ * */
5
+
6
+
7
+ // Autoriser toutes les origines
8
+ header("Access-Control-Allow-Origin: *");
9
+
10
+ // Autoriser les méthodes HTTP spécifiques
11
+ header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
12
+
13
+ // Autoriser certains en-têtes spécifiques
14
+ header("Access-Control-Allow-Headers: Content-Type, Authorization, x-altcha-spam-filter");
15
+
16
+ // Si la méthode est OPTIONS, terminer la requête ici
17
+ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
18
+ http_response_code(200);
19
+ exit();
20
+ }
21
+
22
+ function verifySolution($key, $solution){
23
+ $queryString = $params = [
24
+ 'key' => $key,
25
+ 'altcha' => $solution
26
+ ];
27
+ // Générer la chaîne de requête
28
+ $queryString = http_build_query($params);
29
+ $url = "https://altcha.supersoniks.org/verify-solution?".$queryString;
30
+ $response = file_get_contents($url);
31
+ return $response;
32
+ }
33
+
34
+ /**
35
+ * Get json posted data
36
+ */
37
+
38
+ // Get the posted data
39
+
40
+ $data = json_decode(file_get_contents("php://input"));
41
+
42
+ echo verifySolution($data->captchakey, $data->captchatoken);
@@ -99,6 +99,9 @@ jsonString = jsonString;
99
99
 
100
100
  const tsConfig = JSON.parse(jsonString);
101
101
  tsConfig.compilerOptions.paths = { ...shortPathMapping };
102
+ tsConfig.compilerOptions.paths["@supersoniks/concorde/dataProviderKey"] = [
103
+ path.resolve(__dirname, "../src/core/utils/dataProviderKey.ts"),
104
+ ];
102
105
  tsConfig.compilerOptions.paths["@supersoniks/concorde/*"] = ["./*"];
103
106
  tsConfig.compilerOptions.paths["@concorde/*"] = ["./*"];
104
107
  fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2));
@@ -113,6 +116,7 @@ const pExports = {
113
116
  "./src/core/components/ui/icon/icons.json",
114
117
  "./core/components/functional/sdui/default-library.json":
115
118
  "./src/core/components/functional/sdui/default-library.json",
119
+ "./dataProviderKey": "./src/core/utils/dataProviderKey.ts",
116
120
  "./*": "./src/*.ts",
117
121
  "./vite-config": "./vite/config.js",
118
122
  "./rollup-plugin-*": "./rollup/plugin-*.js",
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Réexport : définition dans [`utils/endpoint`](../utils/endpoint.ts) — `Endpoint<T, U = any>`.
3
+ */
4
+ export { Endpoint } from "../utils/endpoint";
@@ -0,0 +1 @@
1
+ export { DataProviderKey } from "../utils/dataProviderKey";
@@ -1,11 +1,43 @@
1
- import {html, LitElement} from "lit";
2
- import {customElement, property} from "lit/decorators.js";
3
- import Subscriber from "@supersoniks/concorde/core/mixins/Subscriber";
1
+ import { html, LitElement } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { Endpoint } from "@supersoniks/concorde/utils";
4
+ import { DataProviderKey } from "@supersoniks/concorde/core/utils/dataProviderKey";
5
+ import {
6
+ get,
7
+ publish,
8
+ subscribe,
9
+ ApiGetResult,
10
+ } from "@supersoniks/concorde/decorators";
11
+
12
+ /** Same API as the queue demo: https://geo.api.gouv.fr/ */
13
+ type City = { nom: string; code: string };
14
+
15
+ const endpoint = new Endpoint<City[], { limit: number }>(
16
+ "communes?limit=$limit&fields=nom,code",
17
+ );
18
+
19
+ /** Même path que l’endpoint ; type = charge utile `@get` pour `@publish`. */
20
+ const dpKey = new DataProviderKey<ApiGetResult<City[]>>(endpoint.path);
21
+
4
22
  const tagName = "sonic-example"; // For Astro.build
5
23
  @customElement(tagName)
6
- export class SonicComponent extends Subscriber(LitElement) {
7
- @property() text = "Example";
24
+ export class SonicComponent extends LitElement {
25
+ @property({ type: Number })
26
+ limit = 5;
27
+
28
+ @get(endpoint)
29
+ @publish(dpKey)
30
+ geoCommunesPayload?: ApiGetResult<City[]>;
31
+
32
+ @state()
33
+ @subscribe(dpKey.result)
34
+ geoCommunesResult?: City[];
35
+
8
36
  render() {
9
- return html`<div>${this.text}</div>`;
37
+ return html` <span part="api-get-demo">
38
+ · get: ${JSON.stringify(this.geoCommunesPayload?.result ?? null)} · HTTP
39
+ ${this.geoCommunesPayload?.response?.status ?? "—"} · subscribe:
40
+ ${JSON.stringify(this.geoCommunesResult ?? null)}</span
41
+ >`;
10
42
  }
11
43
  }
@@ -1,4 +1,6 @@
1
1
  export { bind } from "./subscriber/bind";
2
+ export { publish } from "./subscriber/publish";
3
+ export { subscribe } from "./subscriber/subscribe";
2
4
  export { onAssign } from "./subscriber/onAssign";
3
5
  export { autoSubscribe } from "./subscriber/autoSubscribe";
4
6
  export { autoFill } from "./subscriber/autoFill";
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { Endpoint } from "../utils/endpoint";
3
+ import { DataProviderKey } from "../utils/dataProviderKey";
4
+ import API, {
5
+ APIConfiguration,
6
+ type ApiGetResult,
7
+ } from "@supersoniks/concorde/core/utils/api";
8
+ import { PublisherManager } from "@supersoniks/concorde/core/utils/PublisherProxy";
9
+ import { get } from "./api";
10
+
11
+ const staticConfig: APIConfiguration = {
12
+ serviceURL: "https://api.example.test",
13
+ token: null,
14
+ userName: null,
15
+ password: null,
16
+ authToken: null,
17
+ tokenProvider: null,
18
+ };
19
+
20
+ async function flushGetMicrotasks() {
21
+ await Promise.resolve();
22
+ await Promise.resolve();
23
+ }
24
+
25
+ function mockPayload<T>(result: T) {
26
+ return {
27
+ request: new Request("https://api.example.test/items/1"),
28
+ response: new Response(JSON.stringify(result), {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json" },
31
+ }),
32
+ result,
33
+ };
34
+ }
35
+
36
+ describe("get", () => {
37
+ beforeEach(() => {
38
+ vi.spyOn(API.prototype, "getDetailed").mockResolvedValue(
39
+ mockPayload({ id: "loaded" }) as never,
40
+ );
41
+ });
42
+
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ document.body.replaceChildren();
46
+ });
47
+
48
+ it("assigne ApiGetResult avec configurationKey (publisher)", async () => {
49
+ const pubId = "getSpecPub1";
50
+ PublisherManager.get(pubId).set(staticConfig);
51
+ const confKey = new DataProviderKey<APIConfiguration>(pubId);
52
+
53
+ const itemEp = new Endpoint<{ id: string }>("items/1");
54
+ const tag = "test-api-get-config-key";
55
+ class C extends HTMLElement {
56
+ @get(itemEp, confKey)
57
+ payload: ApiGetResult<{ id: string }> | null = null;
58
+ }
59
+ if (!customElements.get(tag)) {
60
+ customElements.define(tag, C);
61
+ }
62
+ const el = document.createElement(tag) as InstanceType<typeof C>;
63
+ document.body.appendChild(el);
64
+ await flushGetMicrotasks();
65
+ expect(el.payload?.result).toEqual({ id: "loaded" });
66
+ expect(el.payload?.response?.ok).toBe(true);
67
+ expect(el.payload?.request.url).toContain("api.example.test");
68
+ expect(API.prototype.getDetailed).toHaveBeenCalledWith("items/1");
69
+ });
70
+
71
+ it("endpoint dynamique avec configurationKey", async () => {
72
+ const pubId = "getSpecPub2";
73
+ PublisherManager.get(pubId).set(staticConfig);
74
+ const confKey = new DataProviderKey<APIConfiguration>(pubId);
75
+ const rowEp = new Endpoint<{ id: string }>("rows/${rowId}");
76
+
77
+ class TestRow extends HTMLElement {
78
+ rowId = "42";
79
+
80
+ @get(rowEp, confKey)
81
+ payload: ApiGetResult<{ id: string }> | null = null;
82
+ }
83
+
84
+ const tag = "test-api-get-row";
85
+ if (!customElements.get(tag)) {
86
+ customElements.define(tag, TestRow);
87
+ }
88
+
89
+ const el = document.createElement(tag) as InstanceType<typeof TestRow>;
90
+ document.body.appendChild(el);
91
+ await flushGetMicrotasks();
92
+
93
+ expect(API.prototype.getDetailed).toHaveBeenCalledWith("rows/42");
94
+ expect(el.payload?.result).toEqual({ id: "loaded" });
95
+ });
96
+
97
+ it("mode scoped lit la configuration sur les ancêtres (getApiConfiguration)", async () => {
98
+ const dataEp = new Endpoint<{ id: string }>("scoped-endpoint");
99
+
100
+ class TestScoped extends HTMLElement {
101
+ @get(dataEp)
102
+ payload: ApiGetResult<{ id: string }> | null = null;
103
+ }
104
+
105
+ const tag = "test-api-get-scoped";
106
+ if (!customElements.get(tag)) {
107
+ customElements.define(tag, TestScoped);
108
+ }
109
+
110
+ const wrap = document.createElement("div");
111
+ wrap.setAttribute("serviceURL", "https://from-ancestor.test");
112
+ const el = document.createElement(tag) as InstanceType<typeof TestScoped>;
113
+ wrap.appendChild(el);
114
+ document.body.appendChild(wrap);
115
+
116
+ await flushGetMicrotasks();
117
+
118
+ expect(API.prototype.getDetailed).toHaveBeenCalledWith("scoped-endpoint");
119
+ expect(el.payload?.result).toEqual({ id: "loaded" });
120
+ });
121
+
122
+ it("relance le GET quand le publisher de config mute (onInternalMutation)", async () => {
123
+ const pubId = "getSpecPubMut";
124
+ const pub = PublisherManager.get(pubId);
125
+ pub.set(staticConfig);
126
+ const confKey = new DataProviderKey<APIConfiguration>(pubId);
127
+ const itemEp = new Endpoint<{ id: string }>("items/1");
128
+ const tag = "test-api-get-mutation";
129
+
130
+ class C extends HTMLElement {
131
+ @get(itemEp, confKey)
132
+ payload: ApiGetResult<{ id: string }> | null = null;
133
+ }
134
+ if (!customElements.get(tag)) {
135
+ customElements.define(tag, C);
136
+ }
137
+ const el = document.createElement(tag) as InstanceType<typeof C>;
138
+ document.body.appendChild(el);
139
+ await flushGetMicrotasks();
140
+ expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
141
+
142
+ pub.set({
143
+ ...staticConfig,
144
+ serviceURL: "https://api.example.test/v2",
145
+ });
146
+ await flushGetMicrotasks();
147
+ expect(API.prototype.getDetailed).toHaveBeenCalledTimes(2);
148
+ expect(el.payload?.result).toEqual({ id: "loaded" });
149
+ });
150
+ });
@@ -0,0 +1,244 @@
1
+ import type {
2
+ DataProviderKey,
3
+ DataProviderKeyHost,
4
+ } from "../utils/dataProviderKey";
5
+ import { Endpoint } from "../utils/endpoint";
6
+ import HTML, {
7
+ SearchableDomElement,
8
+ } from "@supersoniks/concorde/core/utils/HTML";
9
+ import API, {
10
+ APIConfiguration,
11
+ type ApiGetResult,
12
+ } from "@supersoniks/concorde/core/utils/api";
13
+ import DataProvider from "../utils/PublisherProxy";
14
+ import { ConnectedComponent, setSubscribable } from "./subscriber/common";
15
+ import {
16
+ extractDynamicDependencies,
17
+ resolveDynamicPath,
18
+ } from "./subscriber/dynamicPath";
19
+ import {
20
+ getDynamicWatchKeys,
21
+ registerDynamicPropertyWatcher,
22
+ } from "./subscriber/dynamicPropertyWatch";
23
+ import { getPublisherFromPath } from "./subscriber/publisherPath";
24
+
25
+ function asSearchableHost(component: unknown): SearchableDomElement | null {
26
+ if (component instanceof HTMLElement || component instanceof ShadowRoot) {
27
+ return component;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function readApiConfigurationFromPublisher(
33
+ publisher: DataProvider | null,
34
+ ): APIConfiguration | null {
35
+ if (!publisher || typeof publisher.get !== "function") return null;
36
+ const raw = publisher.get();
37
+ if (!raw || typeof raw !== "object") return null;
38
+ if (!("serviceURL" in (raw as object))) return null;
39
+ return raw as APIConfiguration;
40
+ }
41
+
42
+ function resolveScopedConfiguration(
43
+ component: unknown,
44
+ ): APIConfiguration | null {
45
+ const host = asSearchableHost(component);
46
+ if (!host) return null;
47
+ return HTML.getApiConfiguration(host);
48
+ }
49
+
50
+ type ApiGetState = {
51
+ cleanupWatchers: Array<() => void>;
52
+ requestGeneration: number;
53
+ configPublisher: DataProvider | null;
54
+ configMutationHandler: (() => void) | null;
55
+ };
56
+
57
+ function detachConfigPublisher(state: ApiGetState): void {
58
+ if (state.configPublisher && state.configMutationHandler) {
59
+ state.configPublisher.offInternalMutation(state.configMutationHandler);
60
+ }
61
+ state.configPublisher = null;
62
+ state.configMutationHandler = null;
63
+ }
64
+
65
+ /**
66
+ * Décorateur **`@get`** : charge des données via `API.getDetailed` et assigne un
67
+ * `ApiGetResult<T>` (`request`, `response`, `result` typé `T`) ou `null`.
68
+ * Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
69
+ *
70
+ * **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
71
+ * (`HTMLElement` / `ShadowRoot`). Sans hôte DOM valide, la propriété reste inchangée jusqu’à connexion.
72
+ *
73
+ * **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
74
+ * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
75
+ * Toute mutation interne du publisher (`onInternalMutation`) relance le GET.
76
+ *
77
+ * @example
78
+ * @get(new Endpoint<User, { userId: string }>("users/${userId}"))
79
+ * payload?: ApiGetResult<User>;
80
+ *
81
+ * @example
82
+ * const apiConf = new DataProviderKey<APIConfiguration>("myApiConf");
83
+ * PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... });
84
+ * @get(new Endpoint<Thing, { id: string }>("things/${id}"), apiConf)
85
+ * payload?: ApiGetResult<Thing>;
86
+ */
87
+ export function get<T, Ue = any>(
88
+ endpoint: Endpoint<T, Ue>,
89
+ ): <K extends string>(
90
+ target: DataProviderKeyHost<Ue> & {
91
+ [P in K]?: ApiGetResult<T> | null | undefined;
92
+ },
93
+ propertyKey: K,
94
+ ) => void;
95
+ export function get<T, Ue = any, Uk = any>(
96
+ endpoint: Endpoint<T, Ue>,
97
+ configurationKey: DataProviderKey<APIConfiguration, Uk>,
98
+ ): <K extends string>(
99
+ target: DataProviderKeyHost<Ue> &
100
+ DataProviderKeyHost<Uk> & {
101
+ [P in K]?: ApiGetResult<T> | null | undefined;
102
+ },
103
+ propertyKey: K,
104
+ ) => void;
105
+ export function get<T, Ue = any, Uk = any>(
106
+ endpoint: Endpoint<T, Ue>,
107
+ configurationKey?: DataProviderKey<APIConfiguration, Uk>,
108
+ ): <K extends string>(
109
+ target: object & { [P in K]?: ApiGetResult<T> | null | undefined },
110
+ propertyKey: K,
111
+ ) => void {
112
+ const pathTemplate = endpoint.path;
113
+ const configurationKeyPath = configurationKey?.path;
114
+ const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
115
+ const configKeyDynamicDependencies = configurationKeyPath
116
+ ? extractDynamicDependencies(configurationKeyPath)
117
+ : [];
118
+ const mergedDynamicDependencies = [
119
+ ...new Set([
120
+ ...endpointDynamicDependencies,
121
+ ...configKeyDynamicDependencies,
122
+ ]),
123
+ ];
124
+ const isDynamicPath = endpointDynamicDependencies.length > 0;
125
+ const usesPublisherConfig = Boolean(configurationKeyPath);
126
+
127
+ return function (target: object, propertyKey: string) {
128
+ if (!target) return;
129
+ setSubscribable(target);
130
+ const stateKey = `__get_state_${propertyKey}`;
131
+
132
+ (target as ConnectedComponent).__onConnected__((component) => {
133
+ const comp = component as Record<string, unknown>;
134
+ let state = comp[stateKey] as ApiGetState | undefined;
135
+ if (!state) {
136
+ state = {
137
+ cleanupWatchers: [],
138
+ requestGeneration: 0,
139
+ configPublisher: null,
140
+ configMutationHandler: null,
141
+ };
142
+ comp[stateKey] = state;
143
+ }
144
+
145
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
146
+ state.cleanupWatchers = [];
147
+ state.requestGeneration++;
148
+
149
+ const runFetch = () => {
150
+ const resolution = isDynamicPath
151
+ ? resolveDynamicPath(component, pathTemplate)
152
+ : { ready: true, path: pathTemplate };
153
+ if (!resolution.ready || !resolution.path) {
154
+ comp[propertyKey] = undefined;
155
+ return;
156
+ }
157
+ let config: APIConfiguration | null = null;
158
+ if (usesPublisherConfig && configurationKeyPath) {
159
+ const configRes = resolveDynamicPath(component, configurationKeyPath);
160
+ if (!configRes.ready || !configRes.path) {
161
+ comp[propertyKey] = undefined;
162
+ return;
163
+ }
164
+ const configPublisher = getPublisherFromPath(configRes.path);
165
+ config = readApiConfigurationFromPublisher(configPublisher);
166
+ } else {
167
+ config = resolveScopedConfiguration(component);
168
+ }
169
+ if (!config) {
170
+ comp[propertyKey] = undefined;
171
+ return;
172
+ }
173
+ const generation = ++state.requestGeneration;
174
+ const api = new API(config);
175
+ void api
176
+ .getDetailed<T>(resolution.path)
177
+ .then((payload?: ApiGetResult<T>) => {
178
+ if (generation !== state.requestGeneration) return;
179
+ comp[propertyKey] = payload;
180
+ });
181
+ };
182
+
183
+ const rebindPublisherConfig = () => {
184
+ if (!usesPublisherConfig || !configurationKeyPath) return;
185
+ detachConfigPublisher(state);
186
+ const configRes = resolveDynamicPath(component, configurationKeyPath);
187
+ if (!configRes.ready || !configRes.path) {
188
+ comp[propertyKey] = undefined;
189
+ return;
190
+ }
191
+ const publisher = getPublisherFromPath(configRes.path);
192
+ if (!publisher) {
193
+ comp[propertyKey] = undefined;
194
+ return;
195
+ }
196
+ const mutationHandler = () => {
197
+ runFetch();
198
+ };
199
+ publisher.onInternalMutation(mutationHandler);
200
+ state.configPublisher = publisher;
201
+ state.configMutationHandler = mutationHandler;
202
+ };
203
+
204
+ if (usesPublisherConfig) {
205
+ for (const dependency of mergedDynamicDependencies) {
206
+ const unsubscribe = registerDynamicPropertyWatcher(
207
+ getDynamicWatchKeys.watcherStore,
208
+ getDynamicWatchKeys.hooked,
209
+ component,
210
+ dependency,
211
+ () => rebindPublisherConfig(),
212
+ );
213
+ state.cleanupWatchers.push(unsubscribe);
214
+ }
215
+ rebindPublisherConfig();
216
+ } else {
217
+ if (isDynamicPath) {
218
+ for (const dependency of endpointDynamicDependencies) {
219
+ const unsubscribe = registerDynamicPropertyWatcher(
220
+ getDynamicWatchKeys.watcherStore,
221
+ getDynamicWatchKeys.hooked,
222
+ component,
223
+ dependency,
224
+ () => runFetch(),
225
+ );
226
+ state.cleanupWatchers.push(unsubscribe);
227
+ }
228
+ }
229
+ runFetch();
230
+ }
231
+ });
232
+
233
+ (target as ConnectedComponent).__onDisconnected__((component) => {
234
+ const comp = component as Record<string, unknown>;
235
+ const state = comp[stateKey] as ApiGetState | undefined;
236
+ if (!state) return;
237
+ detachConfigPublisher(state);
238
+ state.cleanupWatchers.forEach((cleanup) => cleanup());
239
+ state.cleanupWatchers = [];
240
+ state.requestGeneration++;
241
+ comp[propertyKey] = undefined;
242
+ });
243
+ };
244
+ }