@supersoniks/concorde 4.2.0 → 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 (65) hide show
  1. package/README.md +0 -0
  2. package/build-infos.json +1 -1
  3. package/concorde-core.bundle.js +175 -171
  4. package/concorde-core.es.js +2493 -2247
  5. package/dist/concorde-core.bundle.js +175 -171
  6. package/dist/concorde-core.es.js +2493 -2247
  7. package/docs/assets/{index-BbnRiebQ.js → index-B0IJ9I_B.js} +268 -242
  8. package/docs/assets/{index-BBv9CZqo.css → index-B3QHEJTV.css} +1 -1
  9. package/docs/index.html +2 -2
  10. package/docs/src/docs/_misc/bind.md +74 -0
  11. package/docs/src/docs/_misc/key.md +135 -0
  12. package/docs/src/docs/search/docs-search.json +310 -0
  13. package/docs/src/tsconfig-model.json +1 -1
  14. package/docs/src/tsconfig.json +322 -306
  15. package/package.json +22 -4
  16. package/php/get-challenge.php +0 -0
  17. package/php/some-service.php +0 -0
  18. package/scripts/pre-build.mjs +4 -0
  19. package/src/core/_types/endpoint.ts +4 -0
  20. package/src/core/_types/key.ts +1 -0
  21. package/src/core/components/functional/example/example.ts +38 -6
  22. package/src/core/components/ui/captcha/captcha.ts +12 -6
  23. package/src/core/decorators/Subscriber.ts +2 -0
  24. package/src/core/decorators/api.spec.ts +150 -0
  25. package/src/core/decorators/api.ts +244 -0
  26. package/src/core/decorators/subscriber/bind.ts +57 -145
  27. package/src/core/decorators/subscriber/dynamicPath.ts +77 -0
  28. package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +105 -0
  29. package/src/core/decorators/subscriber/onAssign.ts +11 -147
  30. package/src/core/decorators/subscriber/publish.spec.ts +21 -0
  31. package/src/core/decorators/subscriber/publish.ts +148 -0
  32. package/src/core/decorators/subscriber/publisherPath.ts +13 -0
  33. package/src/core/decorators/subscriber/subscribe.spec.ts +21 -0
  34. package/src/core/decorators/subscriber/subscribe.ts +32 -0
  35. package/src/core/decorators/subscriber/subscribe.type-test.ts +32 -0
  36. package/src/core/utils/api.ts +83 -15
  37. package/src/core/utils/dataProviderKey.spec.ts +34 -0
  38. package/src/core/utils/dataProviderKey.ts +86 -0
  39. package/src/core/utils/endpoint.spec.ts +41 -0
  40. package/src/core/utils/endpoint.ts +87 -0
  41. package/src/decorators.ts +14 -0
  42. package/src/docs/{_misc → _decorators}/ancestor-attribute.md +15 -31
  43. package/src/docs/_decorators/bind.md +164 -0
  44. package/src/docs/_decorators/get.md +65 -0
  45. package/src/docs/_decorators/publish.md +54 -0
  46. package/src/docs/_decorators/subscribe.md +36 -0
  47. package/src/docs/_misc/dataProviderKey.md +135 -0
  48. package/src/docs/_misc/endpoint.md +42 -0
  49. package/src/docs/example/decorators-demo-bind-demos.ts +210 -0
  50. package/src/docs/example/decorators-demo-geo.ts +45 -0
  51. package/src/docs/example/decorators-demo-init.ts +228 -0
  52. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +324 -0
  53. package/src/docs/example/decorators-demo.ts +12 -459
  54. package/src/docs/navigation/navigation.ts +27 -10
  55. package/src/docs/search/docs-search.json +1059 -609
  56. package/src/tsconfig-model.json +1 -1
  57. package/src/tsconfig.json +65 -10
  58. package/src/tsconfig.tsbuildinfo +1 -1
  59. package/src/utils.ts +8 -1
  60. package/vite.config.mts +11 -0
  61. package/src/core/components/ui/modal/modal.stories.ts +0 -140
  62. package/src/docs/_misc/bind.md +0 -362
  63. /package/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
  64. /package/src/docs/{_misc → _decorators}/on-assign.md +0 -0
  65. /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.0",
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",
@@ -217,9 +220,6 @@
217
220
  "./modal-title": "./src/core/components/ui/modal/modal-title.ts",
218
221
  "./ui/modal-title": "./src/core/components/ui/modal/modal-title.ts",
219
222
  "./ui/modal/modal-title": "./src/core/components/ui/modal/modal-title.ts",
220
- "./modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
221
- "./ui/modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
222
- "./ui/modal/modal.stories": "./src/core/components/ui/modal/modal.stories.ts",
223
223
  "./modal": "./src/core/components/ui/modal/modal.ts",
224
224
  "./ui/modal": "./src/core/components/ui/modal/modal.ts",
225
225
  "./pop": "./src/core/components/ui/pop/pop.ts",
@@ -274,13 +274,23 @@
274
274
  "./ui/ui": "./src/core/components/ui/ui.ts",
275
275
  "./core": "./src/core/core.ts",
276
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",
277
279
  "./decorators/lifecycle": "./src/core/decorators/lifecycle.ts",
278
280
  "./decorators/subscriber/ancestorAttribute": "./src/core/decorators/subscriber/ancestorAttribute.ts",
279
281
  "./decorators/subscriber/autoFill": "./src/core/decorators/subscriber/autoFill.ts",
280
282
  "./decorators/subscriber/autoSubscribe": "./src/core/decorators/subscriber/autoSubscribe.ts",
281
283
  "./decorators/subscriber/bind": "./src/core/decorators/subscriber/bind.ts",
282
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",
283
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",
284
294
  "./directives/DataProvider": "./src/core/directives/DataProvider.ts",
285
295
  "./directives/Wording": "./src/core/directives/Wording.ts",
286
296
  "./mixins/Fetcher": "./src/core/mixins/Fetcher.ts",
@@ -301,11 +311,19 @@
301
311
  "./utils/Utils": "./src/core/utils/Utils.ts",
302
312
  "./utils/aesCrypto": "./src/core/utils/aesCrypto.ts",
303
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",
304
318
  "./utils/route.spec": "./src/core/utils/route.spec.ts",
305
319
  "./utils/route": "./src/core/utils/route.ts",
306
320
  "./utils/url-pattern": "./src/core/utils/url-pattern.ts",
307
321
  "./code": "./src/docs/code.ts",
308
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",
309
327
  "./example/decorators-demo": "./src/docs/example/decorators-demo.ts",
310
328
  "./example/users": "./src/docs/example/users.ts",
311
329
  "./header/header": "./src/docs/header/header.ts",
File without changes
File without changes
@@ -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
  }
@@ -3,7 +3,10 @@ import { dp, HTML } from "@supersoniks/concorde/utils";
3
3
  import { css, html, LitElement, nothing } from "lit";
4
4
  import { customElement, property } from "lit/decorators.js";
5
5
  import { ConcordeWindow } from "@supersoniks/concorde/core/_types/types";
6
- import type { DataProvider, Publisher } from "@supersoniks/concorde/core/utils/PublisherProxy";
6
+ import type {
7
+ DataProvider,
8
+ Publisher,
9
+ } from "@supersoniks/concorde/core/utils/PublisherProxy";
7
10
  import {
8
11
  generateKey,
9
12
  encryptToBase64,
@@ -72,7 +75,8 @@ export class Captcha extends Subscriber(LitElement) {
72
75
  script.type = "module";
73
76
  this.setAttribute("async", "");
74
77
  this.setAttribute("defer", "");
75
- script.src = "https://eu.altcha.org/js/latest/altcha.min.js";
78
+ script.src =
79
+ "https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js";
76
80
  scriptAdded = true;
77
81
  document.head.appendChild(script);
78
82
  }
@@ -84,7 +88,7 @@ export class Captcha extends Subscriber(LitElement) {
84
88
  captchaToken: string;
85
89
  }>(
86
90
  this.getAncestorAttributeValue("headersDataProvider") ??
87
- this.getAncestorAttributeValue("formDataProvider")
91
+ this.getAncestorAttributeValue("formDataProvider"),
88
92
  );
89
93
 
90
94
  if (
@@ -93,14 +97,14 @@ export class Captcha extends Subscriber(LitElement) {
93
97
  ) {
94
98
  this.formPublisher.needsCaptchaValidation.set(true);
95
99
  (this.formPublisher.captchaToken as Publisher<string>).onAssign(
96
- this.onCaptchaTokenChanged
100
+ this.onCaptchaTokenChanged,
97
101
  );
98
102
  }
99
103
  }
100
104
  disconnectedCallback(): void {
101
105
  if (this.formPublisher) {
102
106
  (this.formPublisher.captchaToken as Publisher<string>).offAssign(
103
- this.onCaptchaTokenChanged
107
+ this.onCaptchaTokenChanged,
104
108
  );
105
109
  this.formPublisher.captchaToken.set("");
106
110
  this.formPublisher.needsCaptchaValidation.set(false);
@@ -115,7 +119,9 @@ export class Captcha extends Subscriber(LitElement) {
115
119
  if (!form) return;
116
120
  const formData = new FormData(form);
117
121
  this.formPublisher.captchaKey.set(this.key);
118
- this.formPublisher.captchaToken.set(formData.get("altcha")?.toString() || "");
122
+ this.formPublisher.captchaToken.set(
123
+ formData.get("altcha")?.toString() || "",
124
+ );
119
125
  }
120
126
 
121
127
  async generateEncryptedKey() {
@@ -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
+ }