@supersoniks/concorde 4.5.1 → 4.5.2

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 (143) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-core.bundle.js +228 -224
  3. package/concorde-core.es.js +1631 -1621
  4. package/dist/concorde-core.bundle.js +228 -224
  5. package/dist/concorde-core.es.js +1631 -1621
  6. package/docs/assets/index-CaysOMFz.js +5046 -0
  7. package/docs/assets/index-D8mGoXzF.css +1 -0
  8. package/docs/css/docs.css +0 -0
  9. package/docs/fonts/ClashGrotesk-Bold.eot +0 -0
  10. package/docs/fonts/ClashGrotesk-Bold.ttf +0 -0
  11. package/docs/fonts/ClashGrotesk-Bold.woff +0 -0
  12. package/docs/fonts/ClashGrotesk-Bold.woff2 +0 -0
  13. package/docs/fonts/ClashGrotesk-Extralight.eot +0 -0
  14. package/docs/fonts/ClashGrotesk-Extralight.ttf +0 -0
  15. package/docs/fonts/ClashGrotesk-Extralight.woff +0 -0
  16. package/docs/fonts/ClashGrotesk-Extralight.woff2 +0 -0
  17. package/docs/fonts/ClashGrotesk-Light.eot +0 -0
  18. package/docs/fonts/ClashGrotesk-Light.ttf +0 -0
  19. package/docs/fonts/ClashGrotesk-Light.woff +0 -0
  20. package/docs/fonts/ClashGrotesk-Light.woff2 +0 -0
  21. package/docs/fonts/ClashGrotesk-Medium.eot +0 -0
  22. package/docs/fonts/ClashGrotesk-Medium.ttf +0 -0
  23. package/docs/fonts/ClashGrotesk-Medium.woff +0 -0
  24. package/docs/fonts/ClashGrotesk-Medium.woff2 +0 -0
  25. package/docs/fonts/ClashGrotesk-Regular.eot +0 -0
  26. package/docs/fonts/ClashGrotesk-Regular.ttf +0 -0
  27. package/docs/fonts/ClashGrotesk-Regular.woff +0 -0
  28. package/docs/fonts/ClashGrotesk-Regular.woff2 +0 -0
  29. package/docs/fonts/ClashGrotesk-Semibold.eot +0 -0
  30. package/docs/fonts/ClashGrotesk-Semibold.ttf +0 -0
  31. package/docs/fonts/ClashGrotesk-Semibold.woff +0 -0
  32. package/docs/fonts/ClashGrotesk-Semibold.woff2 +0 -0
  33. package/docs/fonts/ClashGrotesk-Variable.eot +0 -0
  34. package/docs/fonts/ClashGrotesk-Variable.ttf +0 -0
  35. package/docs/fonts/ClashGrotesk-Variable.woff +0 -0
  36. package/docs/fonts/ClashGrotesk-Variable.woff2 +0 -0
  37. package/docs/img/concorde-icon.svg +5 -0
  38. package/docs/img/concorde-logo.svg +1 -0
  39. package/docs/img/concorde.png +0 -0
  40. package/docs/img/concorde_def.png +0 -0
  41. package/docs/img/concorde_seuil.png.webp +0 -0
  42. package/docs/img/concorde_seuil_invert.png +0 -0
  43. package/docs/img/paul_metrand.jpg +0 -0
  44. package/docs/img/paul_metrand_xs.jpg +0 -0
  45. package/docs/index.html +93 -0
  46. package/docs/src/core/components/functional/date/date.md +290 -0
  47. package/docs/src/core/components/functional/fetch/fetch.md +123 -0
  48. package/docs/src/core/components/functional/if/if.md +16 -0
  49. package/docs/src/core/components/functional/list/list.md +199 -0
  50. package/docs/src/core/components/functional/mix/mix.md +41 -0
  51. package/docs/src/core/components/functional/queue/queue.md +87 -0
  52. package/docs/src/core/components/functional/router/router.md +129 -0
  53. package/docs/src/core/components/functional/sdui/default-library.json +108 -0
  54. package/docs/src/core/components/functional/sdui/example.json +99 -0
  55. package/docs/src/core/components/functional/sdui/sdui.md +356 -0
  56. package/docs/src/core/components/functional/states/states.md +87 -0
  57. package/docs/src/core/components/functional/submit/submit.md +83 -0
  58. package/docs/src/core/components/functional/subscriber/subscriber.md +91 -0
  59. package/docs/src/core/components/functional/value/value.md +35 -0
  60. package/docs/src/core/components/ui/alert/alert.md +121 -0
  61. package/docs/src/core/components/ui/alert-messages/alert-messages.md +0 -0
  62. package/docs/src/core/components/ui/badge/badge.md +127 -0
  63. package/docs/src/core/components/ui/button/button.md +182 -0
  64. package/docs/src/core/components/ui/captcha/captcha.md +24 -0
  65. package/docs/src/core/components/ui/card/card.md +97 -0
  66. package/docs/src/core/components/ui/divider/divider.md +35 -0
  67. package/docs/src/core/components/ui/form/checkbox/checkbox.md +104 -0
  68. package/docs/src/core/components/ui/form/fieldset/fieldset.md +129 -0
  69. package/docs/src/core/components/ui/form/form-actions/form-actions.md +77 -0
  70. package/docs/src/core/components/ui/form/form-layout/form-layout.md +44 -0
  71. package/docs/src/core/components/ui/form/input/input.md +167 -0
  72. package/docs/src/core/components/ui/form/input-autocomplete/input-autocomplete.md +131 -0
  73. package/docs/src/core/components/ui/form/radio/radio.md +84 -0
  74. package/docs/src/core/components/ui/form/select/select.md +97 -0
  75. package/docs/src/core/components/ui/form/switch/switch.md +84 -0
  76. package/docs/src/core/components/ui/form/textarea/textarea.md +65 -0
  77. package/docs/src/core/components/ui/group/group.md +75 -0
  78. package/docs/src/core/components/ui/icon/icon.md +125 -0
  79. package/docs/src/core/components/ui/icon/icons.json +1 -0
  80. package/docs/src/core/components/ui/image/image.md +107 -0
  81. package/docs/src/core/components/ui/link/link.md +43 -0
  82. package/docs/src/core/components/ui/loader/loader.md +67 -0
  83. package/docs/src/core/components/ui/menu/menu.md +329 -0
  84. package/docs/src/core/components/ui/modal/modal.md +119 -0
  85. package/docs/src/core/components/ui/pop/pop.md +96 -0
  86. package/docs/src/core/components/ui/progress/progress.md +63 -0
  87. package/docs/src/core/components/ui/table/table.md +455 -0
  88. package/docs/src/core/components/ui/toast/toast.md +166 -0
  89. package/docs/src/core/components/ui/tooltip/tooltip.md +82 -0
  90. package/docs/src/docs/_core-concept/overview.md +57 -0
  91. package/docs/src/docs/_core-concept/subscriber.md +76 -0
  92. package/docs/src/docs/_decorators/ancestor-attribute.md +78 -0
  93. package/docs/src/docs/_decorators/auto-subscribe.md +199 -0
  94. package/docs/src/docs/_decorators/bind.md +164 -0
  95. package/docs/src/docs/_decorators/get.md +65 -0
  96. package/docs/src/docs/_decorators/on-assign.md +362 -0
  97. package/docs/src/docs/_decorators/publish.md +54 -0
  98. package/docs/src/docs/_decorators/subscribe.md +36 -0
  99. package/docs/src/docs/_decorators/wait-for-ancestors.md +160 -0
  100. package/docs/src/docs/_getting-started/concorde-outside.md +143 -0
  101. package/docs/src/docs/_getting-started/create-a-component.md +137 -0
  102. package/docs/src/docs/_getting-started/my-first-subscriber.md +174 -0
  103. package/docs/src/docs/_getting-started/pubsub.md +150 -0
  104. package/docs/src/docs/_getting-started/start.md +39 -0
  105. package/docs/src/docs/_getting-started/theming.md +91 -0
  106. package/docs/src/docs/_misc/dataProviderKey.md +135 -0
  107. package/docs/src/docs/_misc/endpoint.md +42 -0
  108. package/docs/src/docs/_misc/templates-demo.md +19 -0
  109. package/docs/src/docs/search/docs-search.json +5242 -0
  110. package/docs/src/tag-list.json +1 -0
  111. package/docs/src/tsconfig-model.json +23 -0
  112. package/docs/src/tsconfig.json +987 -0
  113. package/docs/svg/regular/plane.svg +1 -0
  114. package/docs/svg/solid/plane.svg +1 -0
  115. package/package.json +2 -1
  116. package/src/core/components/functional/fetch/fetch.md +0 -0
  117. package/src/core/components/ui/_css/scroll.ts +0 -0
  118. package/src/core/components/ui/_css/size.ts +0 -0
  119. package/src/core/components/ui/alert/alert.ts +0 -0
  120. package/src/core/components/ui/button/button.ts +0 -0
  121. package/src/core/components/ui/captcha/altchaStyles.ts +0 -0
  122. package/src/core/components/ui/divider/divider.ts +0 -0
  123. package/src/core/components/ui/icon/icon.ts +0 -0
  124. package/src/core/components/ui/menu/menu.md +0 -0
  125. package/src/core/components/ui/modal/modal-close.ts +0 -0
  126. package/src/core/components/ui/modal/modal.md +0 -0
  127. package/src/core/components/ui/table/table-caption.ts +0 -0
  128. package/src/core/decorators/api.ts +3 -3
  129. package/src/core/decorators/subscriber/ancestorAttribute.ts +5 -4
  130. package/src/core/decorators/subscriber/bind.ts +9 -7
  131. package/src/core/decorators/subscriber/common.ts +22 -2
  132. package/src/core/decorators/subscriber/dynamicPropertyWatch.spec.ts +125 -0
  133. package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +157 -72
  134. package/src/core/decorators/subscriber/onAssign.ts +2 -2
  135. package/src/core/decorators/subscriber/publish.ts +2 -2
  136. package/src/core/utils/route.ts +0 -0
  137. package/src/docs/code.ts +0 -0
  138. package/src/docs/example/decorators-demo-bind-demos.ts +6 -2
  139. package/src/docs/example/decorators-demo-geo.ts +10 -9
  140. package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +1 -5
  141. package/src/docs/example/decorators-demo.ts +2 -2
  142. package/src/tsconfig.json +3 -0
  143. package/src/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M576 256C576 305 502.1 336 464.2 336H382.2L282.4 496C276.4 506 266.4 512 254.4 512H189.5C179.5 512 169.5 508 163.5 500C157.6 492 155.6 480.1 158.6 471L201.5 336H152.5L113.6 388C107.6 396 98.61 400 88.62 400H31.7C22.72 400 12.73 396 6.74 388C.7485 380-1.248 370 1.747 360L31.7 256L.7488 152C-1.248 143 .7488 133 6.74 125C12.73 117 22.72 112 31.7 112H88.62C98.61 112 107.6 117 113.6 125L152.5 176H201.5L158.6 41C155.6 32 157.6 21 163.5 13C169.5 5 179.5 0 189.5 0H254.4C265.4 0 277.4 7 281.4 16L381.2 176H463.2C502.1 176 576 208 576 256H576zM527.1 256C525.1 246 489.1 224 463.2 224H355.3L245.4 48H211.5L266.4 224H128.6L80.63 160H53.67L81.63 256L53.67 352H80.63L128.6 288H266.4L211.5 464H245.4L355.3 288H463.2C490.1 288 526.1 267 527.1 256V256z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.2-15.4-20.4l49-171.6L112 320 68.8 377.6c-3 4-7.8 6.4-12.8 6.4l-42 0c-7.8 0-14-6.3-14-14c0-1.3 .2-2.6 .5-3.9L32 256 .5 145.9c-.4-1.3-.5-2.6-.5-3.9c0-7.8 6.3-14 14-14l42 0c5 0 9.8 2.4 12.8 6.4L112 192l102.9 0-49-171.6C162.9 10.2 170.6 0 181.2 0l56.2 0c11.5 0 22.1 6.2 27.8 16.1L365.7 192l116.6 0z"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersoniks/concorde",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "",
@@ -286,6 +286,7 @@
286
286
  "./decorators/subscriber/bind": "./src/core/decorators/subscriber/bind.ts",
287
287
  "./decorators/subscriber/common": "./src/core/decorators/subscriber/common.ts",
288
288
  "./decorators/subscriber/dynamicPath": "./src/core/decorators/subscriber/dynamicPath.ts",
289
+ "./decorators/subscriber/dynamicPropertyWatch.spec": "./src/core/decorators/subscriber/dynamicPropertyWatch.spec.ts",
289
290
  "./decorators/subscriber/dynamicPropertyWatch": "./src/core/decorators/subscriber/dynamicPropertyWatch.ts",
290
291
  "./decorators/subscriber/onAssign": "./src/core/decorators/subscriber/onAssign.ts",
291
292
  "./decorators/subscriber/publish.spec": "./src/core/decorators/subscriber/publish.spec.ts",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -18,7 +18,7 @@ import {
18
18
  } from "./subscriber/dynamicPath";
19
19
  import {
20
20
  getDynamicWatchKeys,
21
- registerDynamicPropertyWatcher,
21
+ observeDynamicProperty,
22
22
  } from "./subscriber/dynamicPropertyWatch";
23
23
  import { getPublisherFromPath } from "./subscriber/publisherPath";
24
24
 
@@ -203,7 +203,7 @@ export function get<T, Ue = any, Uk = any>(
203
203
 
204
204
  if (usesPublisherConfig) {
205
205
  for (const dependency of mergedDynamicDependencies) {
206
- const unsubscribe = registerDynamicPropertyWatcher(
206
+ const unsubscribe = observeDynamicProperty(
207
207
  getDynamicWatchKeys.watcherStore,
208
208
  getDynamicWatchKeys.hooked,
209
209
  component,
@@ -216,7 +216,7 @@ export function get<T, Ue = any, Uk = any>(
216
216
  } else {
217
217
  if (isDynamicPath) {
218
218
  for (const dependency of endpointDynamicDependencies) {
219
- const unsubscribe = registerDynamicPropertyWatcher(
219
+ const unsubscribe = observeDynamicProperty(
220
220
  getDynamicWatchKeys.watcherStore,
221
221
  getDynamicWatchKeys.hooked,
222
222
  component,
@@ -5,13 +5,14 @@ export function ancestorAttribute(attributeName: string) {
5
5
  return function (target: unknown, propertyKey: string) {
6
6
  if (!target) return;
7
7
  setSubscribable(target);
8
-
9
- (target as ConnectedComponent).__onConnected__((component) => {
8
+ (target as ConnectedComponent).__onBeforeConnected__((component) => {
10
9
  const value = HTML.getAncestorAttributeValue(
11
10
  component as any,
12
- attributeName
11
+ attributeName,
13
12
  );
14
- component[propertyKey] = value;
13
+ if (value !== null) {
14
+ component[propertyKey] = value;
15
+ }
15
16
  });
16
17
  };
17
18
  }
@@ -10,13 +10,13 @@ import {
10
10
  } from "./dynamicPath";
11
11
  import {
12
12
  bindDynamicWatchKeys,
13
- registerDynamicPropertyWatcher,
13
+ observeDynamicProperty,
14
14
  } from "./dynamicPropertyWatch";
15
15
  import { getPublisherFromPath } from "./publisherPath";
16
16
 
17
17
  function bindImpl(
18
18
  path: string,
19
- options?: { reflect?: boolean }
19
+ options?: { reflect?: boolean },
20
20
  ): (target: unknown, propertyKey: string) => void {
21
21
  const reflect = options?.reflect ?? false;
22
22
  const dynamicDependencies = extractDynamicDependencies(path);
@@ -34,7 +34,7 @@ function bindImpl(
34
34
  if (reflect) {
35
35
  const existingDescriptor = Object.getOwnPropertyDescriptor(
36
36
  target as any,
37
- propertyKey
37
+ propertyKey,
38
38
  );
39
39
  const internalValueKey = `__bind_${propertyKey}_value__`;
40
40
  const reflectUpdateFlagKey = `__bind_${propertyKey}_updating_from_publisher__`;
@@ -152,12 +152,12 @@ function bindImpl(
152
152
 
153
153
  if (isDynamicPath) {
154
154
  for (const dependency of dynamicDependencies) {
155
- const unsubscribe = registerDynamicPropertyWatcher(
155
+ const unsubscribe = observeDynamicProperty(
156
156
  bindDynamicWatchKeys.watcherStore,
157
157
  bindDynamicWatchKeys.hooked,
158
158
  component,
159
159
  dependency,
160
- () => refreshSubscription()
160
+ () => refreshSubscription(),
161
161
  );
162
162
  state.cleanupWatchers.push(unsubscribe);
163
163
  }
@@ -199,7 +199,10 @@ function bindImpl(
199
199
  * @state()
200
200
  * count: number = 0;
201
201
  */
202
- export function bind(path: string, options?: { reflect?: boolean }): (target: unknown, propertyKey: string) => void;
202
+ export function bind(
203
+ path: string,
204
+ options?: { reflect?: boolean },
205
+ ): (target: unknown, propertyKey: string) => void;
203
206
  export function bind<T, U = any>(
204
207
  key: DataProviderKey<T, U>,
205
208
  options?: { reflect?: boolean },
@@ -214,4 +217,3 @@ export function bind(
214
217
  const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
215
218
  return bindImpl(path, options);
216
219
  }
217
-
@@ -1,12 +1,26 @@
1
1
  type ConnectedCallback = (component: ConnectedComponent) => void;
2
2
 
3
3
  export type ConnectedComponent = Record<string, unknown> & {
4
+ /** Hooks exécutés avant le connectedCallback d'origine (ex. lecture DOM ancêtre). */
5
+ __onBeforeConnected__: (callback: ConnectedCallback) => void;
6
+ /** Hooks exécutés après le connectedCallback d'origine (ex. abonnements). */
4
7
  __onConnected__: (callback: ConnectedCallback) => void;
5
8
  __onDisconnected__: (callback: ConnectedCallback) => void;
9
+ __beforeConnectedCallbackCalls__?: Set<ConnectedCallback>;
6
10
  __connectedCallbackCalls__?: Set<ConnectedCallback>;
7
11
  __disconnectedCallbackCalls__?: Set<ConnectedCallback>;
8
12
  };
9
13
 
14
+ function onBeforeConnected(
15
+ this: ConnectedComponent,
16
+ callback: ConnectedCallback,
17
+ ) {
18
+ if (!this.__beforeConnectedCallbackCalls__) {
19
+ this.__beforeConnectedCallbackCalls__ = new Set();
20
+ }
21
+ this.__beforeConnectedCallbackCalls__.add(callback);
22
+ }
23
+
10
24
  function onConnected(this: ConnectedComponent, callback: ConnectedCallback) {
11
25
  if (!this.__connectedCallbackCalls__) {
12
26
  this.__connectedCallbackCalls__ = new Set();
@@ -25,15 +39,21 @@ export function setSubscribable(target: any) {
25
39
  if (target.__is__setSubscribable__) return;
26
40
  target.__is__setSubscribable__ = true;
27
41
 
42
+ target.__onBeforeConnected__ = onBeforeConnected;
28
43
  target.__onConnected__ = onConnected;
29
44
  target.__onDisconnected__ = onDisconnected;
30
45
 
31
46
  const originalConnectedCallback = target.connectedCallback;
32
47
  target.connectedCallback = function (this: any) {
48
+ if (this.__beforeConnectedCallbackCalls__) {
49
+ this.__beforeConnectedCallbackCalls__.forEach(
50
+ (callback: ConnectedCallback) => callback(this),
51
+ );
52
+ }
33
53
  originalConnectedCallback?.call(this);
34
54
  if (this.__connectedCallbackCalls__) {
35
55
  this.__connectedCallbackCalls__.forEach((callback: ConnectedCallback) =>
36
- callback(this)
56
+ callback(this),
37
57
  );
38
58
  }
39
59
  };
@@ -43,7 +63,7 @@ export function setSubscribable(target: any) {
43
63
  originalDisconnectedCallback?.call(this);
44
64
  if (this.__disconnectedCallbackCalls__) {
45
65
  this.__disconnectedCallbackCalls__.forEach(
46
- (callback: ConnectedCallback) => callback(this)
66
+ (callback: ConnectedCallback) => callback(this),
47
67
  );
48
68
  }
49
69
  };
@@ -0,0 +1,125 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { observeDynamicProperty } from "./dynamicPropertyWatch";
3
+
4
+ const legacyStoreKey = Symbol("legacyStore");
5
+ const legacyHookedKey = Symbol("legacyHooked");
6
+
7
+ async function flushFrames(count = 2): Promise<void> {
8
+ for (let i = 0; i < count; i += 1) {
9
+ await new Promise<void>((resolve) => {
10
+ requestAnimationFrame(() => resolve());
11
+ });
12
+ }
13
+ }
14
+
15
+ describe("observeDynamicProperty (poll rAF)", () => {
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it("déclenche le handler quand une prop observée change", async () => {
21
+ const component = { userIndex: 0 };
22
+ const onDependencyChange = vi.fn();
23
+
24
+ observeDynamicProperty(
25
+ legacyStoreKey,
26
+ legacyHookedKey,
27
+ component,
28
+ "userIndex",
29
+ onDependencyChange,
30
+ );
31
+
32
+ component.userIndex = 1;
33
+ await flushFrames();
34
+
35
+ expect(onDependencyChange).toHaveBeenCalled();
36
+ });
37
+
38
+ it("ne déclenche pas sans changement de valeur", async () => {
39
+ const component = { userIndex: 0 };
40
+ const onDependencyChange = vi.fn();
41
+
42
+ observeDynamicProperty(
43
+ legacyStoreKey,
44
+ legacyHookedKey,
45
+ component,
46
+ "userIndex",
47
+ onDependencyChange,
48
+ );
49
+
50
+ await flushFrames();
51
+
52
+ expect(onDependencyChange).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("propage un changement effectué dans le handler (même frame)", async () => {
56
+ const component = { a: 0, b: 0 };
57
+ const onAChange = vi.fn(() => {
58
+ component.b = 1;
59
+ });
60
+ const onBChange = vi.fn();
61
+
62
+ observeDynamicProperty(
63
+ legacyStoreKey,
64
+ legacyHookedKey,
65
+ component,
66
+ "a",
67
+ onAChange,
68
+ );
69
+ observeDynamicProperty(
70
+ legacyStoreKey,
71
+ legacyHookedKey,
72
+ component,
73
+ "b",
74
+ onBChange,
75
+ );
76
+
77
+ component.a = 1;
78
+ await flushFrames();
79
+
80
+ expect(onAChange).toHaveBeenCalled();
81
+ expect(onBChange).toHaveBeenCalled();
82
+ });
83
+
84
+ it("désinscription : plus de handler après cleanup", async () => {
85
+ const component = { userIndex: 0 };
86
+ const onDependencyChange = vi.fn();
87
+
88
+ const unobserve = observeDynamicProperty(
89
+ legacyStoreKey,
90
+ legacyHookedKey,
91
+ component,
92
+ "userIndex",
93
+ onDependencyChange,
94
+ );
95
+
96
+ unobserve();
97
+ component.userIndex = 2;
98
+ await flushFrames();
99
+
100
+ expect(onDependencyChange).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it("alerte si boucle infinie (handler qui remute la même prop)", async () => {
104
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
105
+ const component = { loop: 0 };
106
+ const onDependencyChange = vi.fn(() => {
107
+ component.loop += 1;
108
+ });
109
+
110
+ observeDynamicProperty(
111
+ legacyStoreKey,
112
+ legacyHookedKey,
113
+ component,
114
+ "loop",
115
+ onDependencyChange,
116
+ );
117
+
118
+ component.loop = 1;
119
+ await flushFrames(3);
120
+
121
+ expect(warn).toHaveBeenCalledWith(
122
+ expect.stringContaining("boucle infinie"),
123
+ );
124
+ });
125
+ });
@@ -1,104 +1,189 @@
1
1
  /**
2
- * Enregistre des callbacks sur des propriétés Lit (via `willUpdate`) pour
3
- * réagir aux changements de segments dynamiques `${…}` dans les chemins.
4
- * Chaque décorateur utilise des clés de stockage distinctes pour éviter les collisions.
2
+ * Surveillance des props utilisées dans les chemins dynamiques (`${userIndex}`, etc.).
3
+ *
4
+ * Modèle :
5
+ * - `observeDynamicProperty()` enregistre une prop + un handler à appeler si elle change ;
6
+ * - un seul `requestAnimationFrame` appelle `notifyObservedPropertyChanges()` ;
7
+ * - pour chaque prop observée : `lastValue` vs valeur lue sur le composant ;
8
+ * - plusieurs passes dans la même frame si un handler en modifie une autre ;
9
+ * - garde anti-boucle si une prop ne cesse de changer.
5
10
  */
6
11
 
7
- type InstanceStores = Record<PropertyKey, unknown>;
12
+ import { getValueFromExpression } from "./dynamicPath";
8
13
 
9
- export function registerDynamicPropertyWatcher(
10
- watcherStoreKey: PropertyKey,
11
- hookedStoreKey: PropertyKey,
12
- instance: object,
13
- propertyName: string,
14
- onChange: () => void,
15
- ): () => void {
16
- const inst = instance as InstanceStores;
17
- const key = String(propertyName);
18
- ensureDynamicPropertiesWillUpdate(
19
- watcherStoreKey,
20
- hookedStoreKey,
21
- instance,
22
- );
23
- if (!inst[watcherStoreKey]) {
24
- Object.defineProperty(inst, watcherStoreKey, {
25
- value: new Map<string, Set<() => void>>(),
26
- enumerable: false,
27
- configurable: false,
28
- writable: false,
29
- });
14
+ /** Une prop du composant lue à chaque frame, avec les handlers à déclencher si elle change. */
15
+ type ObservedProperty = {
16
+ /** Dernière valeur vue lors de la dernière notification. */
17
+ lastValue: unknown;
18
+ /** Handlers enregistrés par les décorateurs (@bind, @get, …). */
19
+ onChangeHandlers: Set<() => void>;
20
+ };
21
+
22
+ /** Nom de prop → état d'observation, pour un composant donné. */
23
+ type ObservedProperties = Map<string, ObservedProperty>;
24
+
25
+ const observedPropertiesByComponent = new WeakMap<object, ObservedProperties>();
26
+ /** Composants ayant au moins une prop observée. */
27
+ const componentsBeingPolled = new Set<object>();
28
+
29
+ /** Un seul rAF pour toutes les instances et toutes les props observées. */
30
+ let animationFrameId: number | null = null;
31
+ /** Passes de réconciliation dans une même frame avant alerte boucle infinie. */
32
+ const MAX_NOTIFICATION_PASSES_PER_FRAME = 8;
33
+
34
+ function getObservedProperties(component: object): ObservedProperties {
35
+ let observedProperties = observedPropertiesByComponent.get(component);
36
+ if (!observedProperties) {
37
+ observedProperties = new Map();
38
+ observedPropertiesByComponent.set(component, observedProperties);
30
39
  }
31
- const watcherMap = inst[watcherStoreKey] as Map<string, Set<() => void>>;
32
- if (!watcherMap.has(key)) {
33
- watcherMap.set(key, new Set());
40
+ return observedProperties;
41
+ }
42
+
43
+ /** Compare lastValue à la valeur actuelle ; retourne les handlers à exécuter. */
44
+ function findHandlersForChangedProperties(
45
+ component: object,
46
+ observedProperties: ObservedProperties,
47
+ ): Set<() => void> {
48
+ const handlersToRun = new Set<() => void>();
49
+ for (const [propertyName, observed] of observedProperties) {
50
+ const currentValue = getValueFromExpression(component, propertyName);
51
+ if (!Object.is(observed.lastValue, currentValue)) {
52
+ observed.lastValue = currentValue;
53
+ observed.onChangeHandlers.forEach((handler) => handlersToRun.add(handler));
54
+ }
34
55
  }
35
- const watchers = watcherMap.get(key)!;
36
- watchers.add(onChange);
37
- return () => {
38
- watchers.delete(onChange);
39
- if (watchers.size === 0) {
40
- watcherMap.delete(key);
56
+ return handlersToRun;
57
+ }
58
+
59
+ /** Détecte les props observées qui ont changé et appelle leurs handlers (plusieurs passes si cascade). */
60
+ function notifyObservedPropertyChanges(): void {
61
+ animationFrameId = null;
62
+
63
+ let pass = 0;
64
+ let propertiesChangedThisFrame = true;
65
+
66
+ while (propertiesChangedThisFrame && pass < MAX_NOTIFICATION_PASSES_PER_FRAME) {
67
+ propertiesChangedThisFrame = false;
68
+ pass += 1;
69
+
70
+ for (const component of componentsBeingPolled) {
71
+ const observedProperties = observedPropertiesByComponent.get(component);
72
+ if (!observedProperties || observedProperties.size === 0) continue;
73
+
74
+ const handlersToRun = findHandlersForChangedProperties(
75
+ component,
76
+ observedProperties,
77
+ );
78
+ if (handlersToRun.size === 0) continue;
79
+
80
+ propertiesChangedThisFrame = true;
81
+ handlersToRun.forEach((handler) => handler());
41
82
  }
42
- };
83
+ }
84
+
85
+ if (propertiesChangedThisFrame) {
86
+ console.warn(
87
+ "[concorde] dynamic property watch: limite de passes atteinte, boucle infinie probable",
88
+ );
89
+ }
90
+
91
+ if (componentsBeingPolled.size > 0) {
92
+ scheduleObservedPropertyChanges();
93
+ }
43
94
  }
44
95
 
45
- export function ensureDynamicPropertiesWillUpdate(
46
- watcherStoreKey: PropertyKey,
47
- hookedStoreKey: PropertyKey,
48
- instance: object,
49
- ): void {
50
- const proto = Object.getPrototypeOf(instance);
51
- if (!proto || (proto as InstanceStores)[hookedStoreKey]) return;
52
- const originalWillUpdate = Object.prototype.hasOwnProperty.call(
53
- proto,
54
- "willUpdate",
55
- )
56
- ? (proto as InstanceStores).willUpdate
57
- : (Object.getPrototypeOf(proto) as InstanceStores)?.willUpdate;
58
- (proto as InstanceStores).willUpdate = function (
59
- changedProperties?: Map<unknown, unknown>,
60
- ) {
61
- const handlers = (this as InstanceStores)[watcherStoreKey] as
62
- | Map<string, Set<() => void>>
63
- | undefined;
64
- if (handlers && handlers.size > 0) {
65
- if (changedProperties && changedProperties.size > 0) {
66
- changedProperties.forEach((_value, dependency) => {
67
- const callbacks = handlers.get(String(dependency));
68
- if (callbacks) {
69
- callbacks.forEach((cb) => cb());
70
- }
71
- });
72
- } else {
73
- handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
74
- }
96
+ function scheduleObservedPropertyChanges(): void {
97
+ if (animationFrameId !== null) return;
98
+ animationFrameId = requestAnimationFrame(notifyObservedPropertyChanges);
99
+ }
100
+
101
+ function cancelScheduledObservedPropertyChangesIfIdle(): void {
102
+ if (componentsBeingPolled.size === 0 && animationFrameId !== null) {
103
+ cancelAnimationFrame(animationFrameId);
104
+ animationFrameId = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Observe une prop du composant utilisée dans un chemin dynamique.
110
+ * Retourne une fonction de désinscription.
111
+ *
112
+ * Les deux premiers paramètres sont ignorés (legacy API des décorateurs).
113
+ */
114
+ export function observeDynamicProperty(
115
+ _legacyWatcherStoreKey: PropertyKey,
116
+ _legacyHookedStoreKey: PropertyKey,
117
+ component: object,
118
+ dependencyPropertyName: string,
119
+ onDependencyChange: () => void,
120
+ ): () => void {
121
+ const propertyName = String(dependencyPropertyName);
122
+ const observedProperties = getObservedProperties(component);
123
+
124
+ let observed = observedProperties.get(propertyName);
125
+ if (!observed) {
126
+ observed = {
127
+ lastValue: getValueFromExpression(component, propertyName),
128
+ onChangeHandlers: new Set(),
129
+ };
130
+ observedProperties.set(propertyName, observed);
131
+ }
132
+ observed.onChangeHandlers.add(onDependencyChange);
133
+
134
+ componentsBeingPolled.add(component);
135
+ scheduleObservedPropertyChanges();
136
+
137
+ return () => {
138
+ const currentObservedProperties =
139
+ observedPropertiesByComponent.get(component);
140
+ if (!currentObservedProperties) return;
141
+
142
+ const currentObserved = currentObservedProperties.get(propertyName);
143
+ if (!currentObserved) return;
144
+
145
+ currentObserved.onChangeHandlers.delete(onDependencyChange);
146
+ if (currentObserved.onChangeHandlers.size === 0) {
147
+ currentObservedProperties.delete(propertyName);
75
148
  }
76
- if (typeof originalWillUpdate === "function") {
77
- originalWillUpdate.call(this, changedProperties);
149
+
150
+ if (currentObservedProperties.size === 0) {
151
+ observedPropertiesByComponent.delete(component);
152
+ componentsBeingPolled.delete(component);
153
+ cancelScheduledObservedPropertyChangesIfIdle();
78
154
  }
79
155
  };
80
- (proto as InstanceStores)[hookedStoreKey] = true;
81
156
  }
82
157
 
83
- /** Clés utilisées par `@bind`. */
158
+ /** @deprecated Alias conservé pour les décorateurs existants. */
159
+ export const registerDynamicPropertyWatcher = observeDynamicProperty;
160
+
161
+ /** @deprecated No-op conservé pour compatibilité API. */
162
+ export function ensureDynamicPropertiesWillUpdate(
163
+ _watcherStoreKey: PropertyKey,
164
+ _hookedStoreKey: PropertyKey,
165
+ _instance: object,
166
+ ): void {}
167
+
168
+ /** Clés legacy ignorées par `@bind`. */
84
169
  export const bindDynamicWatchKeys = {
85
170
  watcherStore: Symbol("__bindDynamicWatcherStore__"),
86
171
  hooked: Symbol("__bindDynamicWillUpdateHooked__"),
87
172
  } as const;
88
173
 
89
- /** Clés utilisées par `@publish`. */
174
+ /** Clés legacy ignorées par `@publish`. */
90
175
  export const publishDynamicWatchKeys = {
91
176
  watcherStore: "__publishDynamicWatcherStore__",
92
177
  hooked: "__publishDynamicWillUpdateHooked__",
93
178
  } as const;
94
179
 
95
- /** Clés utilisées par `@get`. */
180
+ /** Clés legacy ignorées par `@get`. */
96
181
  export const getDynamicWatchKeys = {
97
182
  watcherStore: "__getDynamicWatcherStore__",
98
183
  hooked: "__getDynamicWillUpdateHooked__",
99
184
  } as const;
100
185
 
101
- /** Clés utilisées par `@onAssign`. */
186
+ /** Clés legacy ignorées par `@onAssign`. */
102
187
  export const onAssignDynamicWatchKeys = {
103
188
  watcherStore: Symbol("__onAssignDynamicWatcherStore__"),
104
189
  hooked: Symbol("__onAssignDynamicWillUpdateHooked__"),
@@ -3,7 +3,7 @@ import { ConnectedComponent, setSubscribable } from "./common";
3
3
  import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
4
4
  import {
5
5
  onAssignDynamicWatchKeys,
6
- registerDynamicPropertyWatcher,
6
+ observeDynamicProperty,
7
7
  } from "./dynamicPropertyWatch";
8
8
  import { getPublisherFromPath } from "./publisherPath";
9
9
 
@@ -141,7 +141,7 @@ export function onAssign(...values: Array<string>) {
141
141
  for (const conf of confs) {
142
142
  if (conf.pathConfig.isDynamic) {
143
143
  for (const dependency of conf.pathConfig.dynamicDependencies) {
144
- const unsubscribe = registerDynamicPropertyWatcher(
144
+ const unsubscribe = observeDynamicProperty(
145
145
  onAssignDynamicWatchKeys.watcherStore,
146
146
  onAssignDynamicWatchKeys.hooked,
147
147
  component,
@@ -7,7 +7,7 @@ import { ConnectedComponent, setSubscribable } from "./common";
7
7
  import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
8
8
  import {
9
9
  publishDynamicWatchKeys,
10
- registerDynamicPropertyWatcher,
10
+ observeDynamicProperty,
11
11
  } from "./dynamicPropertyWatch";
12
12
  import { getPublisherFromPath } from "./publisherPath";
13
13
 
@@ -116,7 +116,7 @@ export function publish<T, U = any>(
116
116
  if (dynamicDependencies.length) {
117
117
  for (const dependency of dynamicDependencies) {
118
118
  state.cleanupWatchers.push(
119
- registerDynamicPropertyWatcher(
119
+ observeDynamicProperty(
120
120
  publishDynamicWatchKeys.watcherStore,
121
121
  publishDynamicWatchKeys.hooked,
122
122
  comp,
File without changes
package/src/docs/code.ts CHANGED
File without changes