@supersoniks/concorde 4.7.0 → 4.7.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.7.0",
3
+ "version": "4.7.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -0,0 +1,2 @@
1
+ User-agent: *
2
+ Disallow: /
@@ -10,15 +10,3 @@
10
10
  </template>
11
11
  </sonic-code>
12
12
 
13
-
14
-
15
- <sonic-code>
16
- <template>
17
- <sonic-captcha formDataProvider="captchaTestDataProvider">
18
- <sonic-submit serviceURL="https://atelier.julien.supersoniks.pro" endPoint="php/some-service.php" onclick>
19
- <sonic-button class="mt-4">Submit with captcha</sonic-button>
20
- </sonic-submit>
21
- </sonic-captcha>
22
- </template>
23
- </sonic-code>
24
-
@@ -47,11 +47,110 @@ function resolveScopedConfiguration(
47
47
  return HTML.getApiConfiguration(host);
48
48
  }
49
49
 
50
+ function isScopedConfigurationReady(
51
+ config: APIConfiguration | null,
52
+ ): config is APIConfiguration {
53
+ return typeof config?.serviceURL === "string" && config.serviceURL.length > 0;
54
+ }
55
+
56
+ /** Attributs scope dont la présence peut déclencher un GET différé (Lit → minuscules). */
57
+ const SCOPE_WATCH_ATTRIBUTES = [
58
+ "serviceURL",
59
+ "serviceurl",
60
+ "token",
61
+ "credentials",
62
+ "tokenProvider",
63
+ "tokenprovider",
64
+ "userName",
65
+ "username",
66
+ "password",
67
+ "eventsApiToken",
68
+ "eventsapitoken",
69
+ ] as const;
70
+
71
+ type LitHost = HTMLElement & { updateComplete?: Promise<unknown> };
72
+
73
+ function isLitHost(component: unknown): component is LitHost {
74
+ return (
75
+ component instanceof HTMLElement &&
76
+ typeof (component as LitHost).updateComplete !== "undefined"
77
+ );
78
+ }
79
+
80
+ function scheduleAfterHostReady(
81
+ component: unknown,
82
+ callback: () => void,
83
+ ): () => void {
84
+ if (isLitHost(component)) {
85
+ let cancelled = false;
86
+ void component.updateComplete!.then(() => {
87
+ if (!cancelled) callback();
88
+ });
89
+ return () => {
90
+ cancelled = true;
91
+ };
92
+ }
93
+ // HTMLElement classique : config ancêtre déjà lisible ; sinon watchScopedConfiguration.
94
+ callback();
95
+ return () => {};
96
+ }
97
+
98
+ /**
99
+ * Attend que la config scope soit lisible (attributs DOM ou propriétés Lit reflect).
100
+ */
101
+ function watchScopedConfiguration(
102
+ component: unknown,
103
+ onReady: () => void,
104
+ ): () => void {
105
+ const host = asSearchableHost(component);
106
+ if (!host) return () => {};
107
+
108
+ let settled = false;
109
+ const tryNotify = () => {
110
+ if (settled) return;
111
+ if (!isScopedConfigurationReady(resolveScopedConfiguration(component))) {
112
+ return;
113
+ }
114
+ settled = true;
115
+ onReady();
116
+ };
117
+
118
+ tryNotify();
119
+ if (settled) return () => {};
120
+
121
+ const cleanups: Array<() => void> = [];
122
+
123
+ const rafId = requestAnimationFrame(() => tryNotify());
124
+ cleanups.push(() => cancelAnimationFrame(rafId));
125
+
126
+ queueMicrotask(() => tryNotify());
127
+
128
+ const observer = new MutationObserver(() => tryNotify());
129
+ let node: SearchableDomElement | null = host;
130
+ while (node) {
131
+ if (node instanceof Element) {
132
+ observer.observe(node, {
133
+ attributes: true,
134
+ attributeFilter: [...SCOPE_WATCH_ATTRIBUTES],
135
+ });
136
+ }
137
+ node = (node.parentNode ||
138
+ (node as ShadowRoot).host) as SearchableDomElement;
139
+ }
140
+ cleanups.push(() => observer.disconnect());
141
+
142
+ return () => {
143
+ settled = true;
144
+ cleanups.forEach((cleanup) => cleanup());
145
+ };
146
+ }
147
+
50
148
  type ApiGetState = {
51
149
  cleanupWatchers: Array<() => void>;
52
150
  requestGeneration: number;
53
151
  configPublisher: DataProvider | null;
54
152
  configMutationHandler: (() => void) | null;
153
+ scopeWatchCleanup: (() => void) | null;
55
154
  };
56
155
 
57
156
  function detachConfigPublisher(state: ApiGetState): void {
@@ -68,7 +167,8 @@ function detachConfigPublisher(state: ApiGetState): void {
68
167
  * Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
69
168
  *
70
169
  * **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.
170
+ * (`HTMLElement` / `ShadowRoot`). Si `serviceURL` n’est pas encore disponible (reflect Lit, scope
171
+ * ancêtre), le GET est différé jusqu’à ce que la config soit prête.
72
172
  *
73
173
  * **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
74
174
  * `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
@@ -138,6 +238,7 @@ export function get<T, Ue = any, Uk = any>(
138
238
  requestGeneration: 0,
139
239
  configPublisher: null,
140
240
  configMutationHandler: null,
241
+ scopeWatchCleanup: null,
141
242
  };
142
243
  comp[stateKey] = state;
143
244
  }
@@ -166,8 +267,15 @@ export function get<T, Ue = any, Uk = any>(
166
267
  } else {
167
268
  config = resolveScopedConfiguration(component);
168
269
  }
169
- if (!config) {
170
- comp[propertyKey] = undefined;
270
+ if (!isScopedConfigurationReady(config)) {
271
+ if (!usesPublisherConfig && !state.scopeWatchCleanup) {
272
+ const scopeWatch = watchScopedConfiguration(component, runFetch);
273
+ state.scopeWatchCleanup = scopeWatch;
274
+ state.cleanupWatchers.push(() => {
275
+ scopeWatch();
276
+ state.scopeWatchCleanup = null;
277
+ });
278
+ }
171
279
  return;
172
280
  }
173
281
  const generation = ++state.requestGeneration;
@@ -214,19 +322,24 @@ export function get<T, Ue = any, Uk = any>(
214
322
  }
215
323
  rebindPublisherConfig();
216
324
  } else {
217
- if (isDynamicPath) {
218
- for (const dependency of endpointDynamicDependencies) {
219
- const unsubscribe = observeDynamicProperty(
220
- getDynamicWatchKeys.watcherStore,
221
- getDynamicWatchKeys.hooked,
222
- component,
223
- dependency,
224
- () => runFetch(),
225
- );
226
- state.cleanupWatchers.push(unsubscribe);
325
+ const startScopedFetch = () => {
326
+ if (isDynamicPath) {
327
+ for (const dependency of endpointDynamicDependencies) {
328
+ const unsubscribe = observeDynamicProperty(
329
+ getDynamicWatchKeys.watcherStore,
330
+ getDynamicWatchKeys.hooked,
331
+ component,
332
+ dependency,
333
+ () => runFetch(),
334
+ );
335
+ state.cleanupWatchers.push(unsubscribe);
336
+ }
227
337
  }
228
- }
229
- runFetch();
338
+ runFetch();
339
+ };
340
+ state.cleanupWatchers.push(
341
+ scheduleAfterHostReady(component, startScopedFetch),
342
+ );
230
343
  }
231
344
  });
232
345
 
@@ -59,8 +59,37 @@ class HTML {
59
59
  return null;
60
60
  }
61
61
 
62
+ /** Noms d'attribut DOM à tester (Lit reflect → minuscules, ex. serviceURL → serviceurl). */
63
+ private static scopeAttributeNames(attributeName: string): string[] {
64
+ const lower = attributeName.toLowerCase();
65
+ return lower === attributeName ? [attributeName] : [attributeName, lower];
66
+ }
67
+
68
+ private static readScopeValueOnElement(
69
+ element: HTMLElement,
70
+ attributeName: string,
71
+ ): string | null {
72
+ // Lit : la propriété est fiable avant le reflect ; un attribut vide (serviceurl="")
73
+ // ne doit pas masquer la propriété.
74
+ const prop = (element as unknown as Record<string, unknown>)[attributeName];
75
+ if (typeof prop === "string" && prop.length > 0) {
76
+ return prop;
77
+ }
78
+ for (const attrName of HTML.scopeAttributeNames(attributeName)) {
79
+ if (element.hasAttribute(attrName)) {
80
+ const attr = element.getAttribute(attrName);
81
+ if (attr != null && attr.length > 0) {
82
+ return attr;
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
62
89
  /**
63
- * Va de parent en parent en partant de node pour trouver un attribut
90
+ * Va de parent en parent en partant de node pour trouver un attribut.
91
+ * Si l'attribut n'est pas encore reflété (ex. Lit `@property({ reflect: true })`
92
+ * au premier `connectedCallback`), lit la propriété homonyme sur l'élément.
64
93
  * @param attributeName nom de l'attribut
65
94
  * @returns valeur de l'attribut ou null si l'attribut n'est pas trouvé
66
95
  */
@@ -69,16 +98,19 @@ class HTML {
69
98
  attributeName: string
70
99
  ): string | null {
71
100
  if (!node) return null;
72
- while (!("hasAttribute" in node && node.hasAttribute(attributeName))) {
73
- const newNode = node.parentNode || (node as ShadowRoot).host;
74
- if (!newNode) break;
75
- node = (node.parentNode ||
76
- (node as ShadowRoot).host) as SearchableDomElement;
77
- }
78
- if (!("hasAttribute" in node)) {
79
- return null;
101
+ let current: SearchableDomElement | null = node;
102
+ while (current) {
103
+ if (current instanceof HTMLElement) {
104
+ const value = HTML.readScopeValueOnElement(current, attributeName);
105
+ if (value != null && value.length > 0) {
106
+ return value;
107
+ }
108
+ }
109
+ const parent = current.parentNode || (current as ShadowRoot).host;
110
+ if (!parent) break;
111
+ current = parent as SearchableDomElement;
80
112
  }
81
- return node.getAttribute(attributeName);
113
+ return null;
82
114
  }
83
115
 
84
116
  /**
@@ -1,34 +0,0 @@
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']);
@@ -1,42 +0,0 @@
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);