@supersoniks/concorde 4.2.1 → 4.4.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.
- package/README.md +163 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +585 -670
- package/concorde-core.es.js +7165 -9505
- package/dist/concorde-core.bundle.js +585 -670
- package/dist/concorde-core.es.js +7165 -9505
- package/docs/assets/index-DP1oMukw.js +4949 -0
- package/docs/assets/index-DZtxIZCW.css +1 -0
- package/docs/index.html +2 -2
- package/{src/docs/_misc → docs/src/docs/_decorators}/ancestor-attribute.md +15 -31
- package/docs/src/docs/_decorators/bind.md +164 -0
- package/docs/src/docs/_decorators/get.md +65 -0
- package/docs/src/docs/_decorators/publish.md +54 -0
- package/docs/src/docs/_decorators/subscribe.md +36 -0
- package/docs/src/docs/_misc/dataProviderKey.md +135 -0
- package/docs/src/docs/_misc/endpoint.md +42 -0
- package/docs/src/docs/search/docs-search.json +850 -710
- package/docs/src/tsconfig.json +43 -4
- package/package.json +25 -4
- package/php/get-challenge.php +34 -0
- package/php/some-service.php +42 -0
- package/scripts/pre-build.mjs +4 -0
- package/src/core/_types/endpoint.ts +4 -0
- package/src/core/_types/key.ts +1 -0
- package/src/core/components/functional/example/example.ts +38 -6
- package/src/core/decorators/Subscriber.ts +2 -0
- package/src/core/decorators/api.spec.ts +150 -0
- package/src/core/decorators/api.ts +244 -0
- package/src/core/decorators/subscriber/bind.ts +57 -145
- package/src/core/decorators/subscriber/dynamicPath.ts +77 -0
- package/src/core/decorators/subscriber/dynamicPropertyWatch.ts +105 -0
- package/src/core/decorators/subscriber/onAssign.ts +11 -147
- package/src/core/decorators/subscriber/publish.spec.ts +21 -0
- package/src/core/decorators/subscriber/publish.ts +148 -0
- package/src/core/decorators/subscriber/publisherPath.ts +13 -0
- package/src/core/decorators/subscriber/subscribe.spec.ts +21 -0
- package/src/core/decorators/subscriber/subscribe.ts +32 -0
- package/src/core/decorators/subscriber/subscribe.type-test.ts +32 -0
- package/src/core/utils/api.ts +83 -15
- package/src/core/utils/dataProviderKey.spec.ts +34 -0
- package/src/core/utils/dataProviderKey.ts +86 -0
- package/src/core/utils/endpoint.spec.ts +41 -0
- package/src/core/utils/endpoint.ts +87 -0
- package/src/decorators.ts +14 -0
- package/{docs/src/docs/_misc → src/docs/_decorators}/ancestor-attribute.md +15 -31
- package/src/docs/_decorators/bind.md +164 -0
- package/src/docs/_decorators/get.md +65 -0
- package/src/docs/_decorators/publish.md +54 -0
- package/src/docs/_decorators/subscribe.md +36 -0
- package/src/docs/_misc/dataProviderKey.md +135 -0
- package/src/docs/_misc/endpoint.md +42 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +210 -0
- package/src/docs/example/decorators-demo-geo.ts +45 -0
- package/src/docs/example/decorators-demo-init.ts +228 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +324 -0
- package/src/docs/example/decorators-demo.ts +12 -459
- package/src/docs/navigation/navigation.ts +27 -10
- package/src/docs/search/docs-search.json +1059 -609
- package/src/tsconfig-model.json +1 -1
- package/src/tsconfig.json +65 -1
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +8 -1
- package/vite/config.js +25 -6
- package/vite.config.mts +13 -0
- package/docs/assets/index-B0IJ9I_B.js +0 -4918
- package/docs/assets/index-B3QHEJTV.css +0 -1
- package/docs/src/docs/_misc/bind.md +0 -436
- package/docs/src/docs/_misc/key.md +0 -135
- package/src/docs/_misc/bind.md +0 -362
- /package/docs/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
- /package/docs/src/docs/{_misc → _decorators}/on-assign.md +0 -0
- /package/docs/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
- /package/src/docs/{_misc → _decorators}/auto-subscribe.md +0 -0
- /package/src/docs/{_misc → _decorators}/on-assign.md +0 -0
- /package/src/docs/{_misc → _decorators}/wait-for-ancestors.md +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,147 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DataProviderKey,
|
|
3
|
+
DataProviderKeyHost,
|
|
4
|
+
} from "../../utils/dataProviderKey";
|
|
4
5
|
import { ConnectedComponent, setSubscribable } from "./common";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
configurable: false,
|
|
21
|
-
writable: false,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
const watcherMap = instance[dynamicWatcherStore] as Map<
|
|
25
|
-
string,
|
|
26
|
-
Set<() => void>
|
|
27
|
-
>;
|
|
28
|
-
if (!watcherMap.has(key)) {
|
|
29
|
-
watcherMap.set(key, new Set());
|
|
30
|
-
}
|
|
31
|
-
const watchers = watcherMap.get(key)!;
|
|
32
|
-
watchers.add(onChange);
|
|
33
|
-
return () => {
|
|
34
|
-
watchers.delete(onChange);
|
|
35
|
-
if (watchers.size === 0) {
|
|
36
|
-
watcherMap.delete(key);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function ensureWillUpdateHook(instance: any) {
|
|
42
|
-
const proto = Object.getPrototypeOf(instance);
|
|
43
|
-
if (!proto || proto[dynamicWillUpdateHookedStore]) return;
|
|
44
|
-
const originalWillUpdate = Object.prototype.hasOwnProperty.call(
|
|
45
|
-
proto,
|
|
46
|
-
"willUpdate"
|
|
47
|
-
)
|
|
48
|
-
? proto.willUpdate
|
|
49
|
-
: Object.getPrototypeOf(proto)?.willUpdate;
|
|
50
|
-
proto.willUpdate = function (changedProperties?: Map<unknown, unknown>) {
|
|
51
|
-
const handlers = this[dynamicWatcherStore] as
|
|
52
|
-
| Map<string, Set<() => void>>
|
|
53
|
-
| undefined;
|
|
54
|
-
if (handlers && handlers.size > 0) {
|
|
55
|
-
if (changedProperties && changedProperties.size > 0) {
|
|
56
|
-
changedProperties.forEach((_value, dependency) => {
|
|
57
|
-
const callbacks = handlers.get(String(dependency));
|
|
58
|
-
if (callbacks) {
|
|
59
|
-
callbacks.forEach((cb) => cb());
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
} else {
|
|
63
|
-
handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
originalWillUpdate?.call(this, changedProperties);
|
|
67
|
-
};
|
|
68
|
-
proto[dynamicWillUpdateHookedStore] = true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function extractDynamicDependencies(path: string) {
|
|
72
|
-
const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
|
|
73
|
-
const deps = new Set<string>();
|
|
74
|
-
for (const pattern of patterns) {
|
|
75
|
-
let match;
|
|
76
|
-
while ((match = pattern.exec(path)) !== null) {
|
|
77
|
-
const cleaned = cleanPlaceholder(match[1]);
|
|
78
|
-
if (!cleaned) continue;
|
|
79
|
-
const [root] = cleaned.split(".");
|
|
80
|
-
if (root) deps.add(root);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return Array.from(deps);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function cleanPlaceholder(value: string) {
|
|
87
|
-
return value.trim().replace(/^this\./, "");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function resolveDynamicPath(component: any, template: string) {
|
|
91
|
-
let missing = false;
|
|
92
|
-
const replaceValue = (_match: string, expression: string) => {
|
|
93
|
-
const cleaned = cleanPlaceholder(expression);
|
|
94
|
-
const resolved = getValueFromExpression(component, cleaned);
|
|
95
|
-
if (resolved === undefined || resolved === null) {
|
|
96
|
-
missing = true;
|
|
97
|
-
return "";
|
|
98
|
-
}
|
|
99
|
-
return `${resolved}`;
|
|
100
|
-
};
|
|
101
|
-
const resolvedPath = template
|
|
102
|
-
.replace(/\$\{([^}]+)\}/g, replaceValue)
|
|
103
|
-
.replace(/\{\$([^}]+)\}/g, replaceValue)
|
|
104
|
-
.trim();
|
|
105
|
-
if (missing || !resolvedPath.length) {
|
|
106
|
-
return { ready: false, path: null };
|
|
107
|
-
}
|
|
108
|
-
const segments = resolvedPath.split(".").filter(Boolean);
|
|
109
|
-
if (segments.length === 0 || !segments[0]) {
|
|
110
|
-
return { ready: false, path: null };
|
|
111
|
-
}
|
|
112
|
-
return { ready: true, path: resolvedPath };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getValueFromExpression(component: any, expression: string) {
|
|
116
|
-
if (!expression) return undefined;
|
|
117
|
-
const segments = expression.split(".").filter(Boolean);
|
|
118
|
-
if (segments.length === 0) return undefined;
|
|
119
|
-
let current: unknown = component;
|
|
120
|
-
for (const segment of segments) {
|
|
121
|
-
if (
|
|
122
|
-
current === undefined ||
|
|
123
|
-
current === null ||
|
|
124
|
-
typeof current !== "object"
|
|
125
|
-
) {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
current = (current as Record<string, unknown>)[segment];
|
|
129
|
-
}
|
|
130
|
-
return current;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function getPublisherFromPath(path: string) {
|
|
134
|
-
const segments = path.split(".").filter((segment) => segment.length > 0);
|
|
135
|
-
if (segments.length === 0) return null;
|
|
136
|
-
const dataProvider = segments.shift() || "";
|
|
137
|
-
if (!dataProvider) return null;
|
|
138
|
-
let publisher = PublisherManager.get(dataProvider);
|
|
139
|
-
if (!publisher) return null;
|
|
140
|
-
publisher = Objects.traverse(publisher, segments);
|
|
141
|
-
return publisher as DataProvider | null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function bind(path: string, options?: { reflect?: boolean }) {
|
|
6
|
+
import {
|
|
7
|
+
extractDynamicDependencies,
|
|
8
|
+
hasPath,
|
|
9
|
+
resolveDynamicPath,
|
|
10
|
+
} from "./dynamicPath";
|
|
11
|
+
import {
|
|
12
|
+
bindDynamicWatchKeys,
|
|
13
|
+
registerDynamicPropertyWatcher,
|
|
14
|
+
} from "./dynamicPropertyWatch";
|
|
15
|
+
import { getPublisherFromPath } from "./publisherPath";
|
|
16
|
+
|
|
17
|
+
function bindImpl(
|
|
18
|
+
path: string,
|
|
19
|
+
options?: { reflect?: boolean }
|
|
20
|
+
): (target: unknown, propertyKey: string) => void {
|
|
145
21
|
const reflect = options?.reflect ?? false;
|
|
146
22
|
const dynamicDependencies = extractDynamicDependencies(path);
|
|
147
23
|
const isDynamicPath = dynamicDependencies.length > 0;
|
|
@@ -276,8 +152,10 @@ export function bind(path: string, options?: { reflect?: boolean }) {
|
|
|
276
152
|
|
|
277
153
|
if (isDynamicPath) {
|
|
278
154
|
for (const dependency of dynamicDependencies) {
|
|
279
|
-
const unsubscribe =
|
|
280
|
-
|
|
155
|
+
const unsubscribe = registerDynamicPropertyWatcher(
|
|
156
|
+
bindDynamicWatchKeys.watcherStore,
|
|
157
|
+
bindDynamicWatchKeys.hooked,
|
|
158
|
+
component,
|
|
281
159
|
dependency,
|
|
282
160
|
() => refreshSubscription()
|
|
283
161
|
);
|
|
@@ -303,3 +181,37 @@ export function bind(path: string, options?: { reflect?: boolean }) {
|
|
|
303
181
|
};
|
|
304
182
|
}
|
|
305
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Bidirectional binding to a publisher path. Subscribes to changes and optionally reflects writes back.
|
|
186
|
+
* Accepts either a string path (legacy) or DataProviderKey<T> for type-safe binding.
|
|
187
|
+
* Supports dynamic paths: use placeholders like "users.${userIndex}" in the path or DataProviderKey.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* // String path (legacy):
|
|
191
|
+
* @bind("demoData.firstName")
|
|
192
|
+
* @state()
|
|
193
|
+
* firstName = "";
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* // DataProviderKey with type validation:
|
|
197
|
+
* const dataKey = new DataProviderKey<Data>("data");
|
|
198
|
+
* @bind(dataKey.count, { reflect: true })
|
|
199
|
+
* @state()
|
|
200
|
+
* count: number = 0;
|
|
201
|
+
*/
|
|
202
|
+
export function bind(path: string, options?: { reflect?: boolean }): (target: unknown, propertyKey: string) => void;
|
|
203
|
+
export function bind<T, U = any>(
|
|
204
|
+
key: DataProviderKey<T, U>,
|
|
205
|
+
options?: { reflect?: boolean },
|
|
206
|
+
): <K extends string>(
|
|
207
|
+
target: DataProviderKeyHost<U> & { [P in K]?: T | null | undefined },
|
|
208
|
+
propertyKey: K,
|
|
209
|
+
) => void;
|
|
210
|
+
export function bind(
|
|
211
|
+
pathOrKey: string | DataProviderKey<unknown, unknown>,
|
|
212
|
+
options?: { reflect?: boolean },
|
|
213
|
+
): (target: unknown, propertyKey: string) => void {
|
|
214
|
+
const path = hasPath(pathOrKey) ? pathOrKey.path : pathOrKey;
|
|
215
|
+
return bindImpl(path, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Lit / décorateurs : chemins publisher avec `${prop}` ou `{$prop}`. */
|
|
2
|
+
|
|
3
|
+
export function cleanPlaceholder(value: string): string {
|
|
4
|
+
return value.trim().replace(/^this\./, "");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getValueFromExpression(
|
|
8
|
+
component: unknown,
|
|
9
|
+
expression: string,
|
|
10
|
+
): unknown {
|
|
11
|
+
if (!expression) return undefined;
|
|
12
|
+
const segments = expression.split(".").filter(Boolean);
|
|
13
|
+
if (segments.length === 0) return undefined;
|
|
14
|
+
let current: unknown = component;
|
|
15
|
+
for (const segment of segments) {
|
|
16
|
+
if (
|
|
17
|
+
current === undefined ||
|
|
18
|
+
current === null ||
|
|
19
|
+
typeof current !== "object"
|
|
20
|
+
) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
current = (current as Record<string, unknown>)[segment];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveDynamicPath(
|
|
29
|
+
component: unknown,
|
|
30
|
+
template: string,
|
|
31
|
+
): { ready: boolean; path: string | null } {
|
|
32
|
+
let missing = false;
|
|
33
|
+
const replaceValue = (_match: string, expression: string) => {
|
|
34
|
+
const cleaned = cleanPlaceholder(expression);
|
|
35
|
+
const resolved = getValueFromExpression(component, cleaned);
|
|
36
|
+
if (resolved === undefined || resolved === null) {
|
|
37
|
+
missing = true;
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
return `${resolved}`;
|
|
41
|
+
};
|
|
42
|
+
const resolvedPath = template
|
|
43
|
+
.replace(/\$\{([^}]+)\}/g, replaceValue)
|
|
44
|
+
.replace(/\{\$([^}]+)\}/g, replaceValue)
|
|
45
|
+
.trim();
|
|
46
|
+
if (missing || !resolvedPath.length) {
|
|
47
|
+
return { ready: false, path: null };
|
|
48
|
+
}
|
|
49
|
+
const segments = resolvedPath.split(".").filter(Boolean);
|
|
50
|
+
if (segments.length === 0 || !segments[0]) {
|
|
51
|
+
return { ready: false, path: null };
|
|
52
|
+
}
|
|
53
|
+
return { ready: true, path: resolvedPath };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function extractDynamicDependencies(path: string): string[] {
|
|
57
|
+
const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
|
|
58
|
+
const deps = new Set<string>();
|
|
59
|
+
for (const pattern of patterns) {
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = pattern.exec(path)) !== null) {
|
|
62
|
+
const cleaned = (match[1] || "").trim().replace(/^this\./, "");
|
|
63
|
+
if (!cleaned) continue;
|
|
64
|
+
const [root] = cleaned.split(".");
|
|
65
|
+
if (root) deps.add(root);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Array.from(deps);
|
|
69
|
+
}
|
|
70
|
+
export function hasPath(obj: unknown): obj is { path: string } {
|
|
71
|
+
return (
|
|
72
|
+
typeof obj === "object" &&
|
|
73
|
+
obj !== null &&
|
|
74
|
+
"path" in obj &&
|
|
75
|
+
typeof (obj as { path: unknown }).path === "string"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type InstanceStores = Record<PropertyKey, unknown>;
|
|
8
|
+
|
|
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
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const watcherMap = inst[watcherStoreKey] as Map<string, Set<() => void>>;
|
|
32
|
+
if (!watcherMap.has(key)) {
|
|
33
|
+
watcherMap.set(key, new Set());
|
|
34
|
+
}
|
|
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);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
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
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (typeof originalWillUpdate === "function") {
|
|
77
|
+
originalWillUpdate.call(this, changedProperties);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
(proto as InstanceStores)[hookedStoreKey] = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Clés utilisées par `@bind`. */
|
|
84
|
+
export const bindDynamicWatchKeys = {
|
|
85
|
+
watcherStore: Symbol("__bindDynamicWatcherStore__"),
|
|
86
|
+
hooked: Symbol("__bindDynamicWillUpdateHooked__"),
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
/** Clés utilisées par `@publish`. */
|
|
90
|
+
export const publishDynamicWatchKeys = {
|
|
91
|
+
watcherStore: "__publishDynamicWatcherStore__",
|
|
92
|
+
hooked: "__publishDynamicWillUpdateHooked__",
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/** Clés utilisées par `@get`. */
|
|
96
|
+
export const getDynamicWatchKeys = {
|
|
97
|
+
watcherStore: "__getDynamicWatcherStore__",
|
|
98
|
+
hooked: "__getDynamicWillUpdateHooked__",
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
/** Clés utilisées par `@onAssign`. */
|
|
102
|
+
export const onAssignDynamicWatchKeys = {
|
|
103
|
+
watcherStore: Symbol("__onAssignDynamicWatcherStore__"),
|
|
104
|
+
hooked: Symbol("__onAssignDynamicWillUpdateHooked__"),
|
|
105
|
+
} as const;
|