@supersoniks/concorde 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +175 -171
- package/concorde-core.es.js +2490 -2246
- package/dist/concorde-core.bundle.js +175 -171
- package/dist/concorde-core.es.js +2490 -2246
- package/package.json +22 -1
- 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/src/docs/{_misc → _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.mts +11 -0
- package/src/docs/_misc/bind.md +0 -362
- /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
|
@@ -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;
|
|
@@ -1,149 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import DataProvider, { PublisherManager } from "../../utils/PublisherProxy";
|
|
1
|
+
import DataProvider from "../../utils/PublisherProxy";
|
|
4
2
|
import { ConnectedComponent, setSubscribable } from "./common";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function registerDynamicWatcher(
|
|
12
|
-
instance: any,
|
|
13
|
-
propertyName: string,
|
|
14
|
-
onChange: () => void
|
|
15
|
-
) {
|
|
16
|
-
const key = String(propertyName);
|
|
17
|
-
ensureWillUpdateHook(instance);
|
|
18
|
-
if (!instance[dynamicWatcherStore]) {
|
|
19
|
-
Object.defineProperty(instance, dynamicWatcherStore, {
|
|
20
|
-
value: new Map<string, Set<() => void>>(),
|
|
21
|
-
enumerable: false,
|
|
22
|
-
configurable: false,
|
|
23
|
-
writable: false,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
const watcherMap = instance[dynamicWatcherStore] as Map<
|
|
27
|
-
string,
|
|
28
|
-
Set<() => void>
|
|
29
|
-
>;
|
|
30
|
-
if (!watcherMap.has(key)) {
|
|
31
|
-
watcherMap.set(key, new Set());
|
|
32
|
-
}
|
|
33
|
-
const watchers = watcherMap.get(key)!;
|
|
34
|
-
watchers.add(onChange);
|
|
35
|
-
return () => {
|
|
36
|
-
watchers.delete(onChange);
|
|
37
|
-
if (watchers.size === 0) {
|
|
38
|
-
watcherMap.delete(key);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function ensureWillUpdateHook(instance: any) {
|
|
44
|
-
const proto = Object.getPrototypeOf(instance);
|
|
45
|
-
if (!proto || proto[dynamicWillUpdateHookedStore]) return;
|
|
46
|
-
|
|
47
|
-
const originalWillUpdate = Object.prototype.hasOwnProperty.call(
|
|
48
|
-
proto,
|
|
49
|
-
"willUpdate"
|
|
50
|
-
)
|
|
51
|
-
? proto.willUpdate
|
|
52
|
-
: Object.getPrototypeOf(proto)?.willUpdate;
|
|
53
|
-
|
|
54
|
-
proto.willUpdate = function (changedProperties?: Map<unknown, unknown>) {
|
|
55
|
-
const handlers = this[dynamicWatcherStore] as
|
|
56
|
-
| Map<string, Set<() => void>>
|
|
57
|
-
| undefined;
|
|
58
|
-
if (handlers && handlers.size > 0) {
|
|
59
|
-
if (changedProperties && changedProperties.size > 0) {
|
|
60
|
-
changedProperties.forEach((_value, dependency) => {
|
|
61
|
-
const callbacks = handlers.get(String(dependency));
|
|
62
|
-
if (callbacks) {
|
|
63
|
-
callbacks.forEach((cb) => cb());
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
} else {
|
|
67
|
-
handlers.forEach((callbacks) => callbacks.forEach((cb) => cb()));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
originalWillUpdate?.call(this, changedProperties);
|
|
71
|
-
};
|
|
72
|
-
proto[dynamicWillUpdateHookedStore] = true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function extractDynamicDependencies(path: string) {
|
|
76
|
-
const patterns = [/\$\{([^}]+)\}/g, /\{\$([^}]+)\}/g];
|
|
77
|
-
const deps = new Set<string>();
|
|
78
|
-
for (const pattern of patterns) {
|
|
79
|
-
let match;
|
|
80
|
-
while ((match = pattern.exec(path)) !== null) {
|
|
81
|
-
const cleaned = cleanPlaceholder(match[1]);
|
|
82
|
-
if (!cleaned) continue;
|
|
83
|
-
const [root] = cleaned.split(".");
|
|
84
|
-
if (root) deps.add(root);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return Array.from(deps);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function cleanPlaceholder(value: string) {
|
|
91
|
-
return value.trim().replace(/^this\./, "");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function resolveDynamicPath(component: any, template: string) {
|
|
95
|
-
let missing = false;
|
|
96
|
-
const replaceValue = (_match: string, expression: string) => {
|
|
97
|
-
const cleaned = cleanPlaceholder(expression);
|
|
98
|
-
const resolved = getValueFromExpression(component, cleaned);
|
|
99
|
-
if (resolved === undefined || resolved === null) {
|
|
100
|
-
missing = true;
|
|
101
|
-
return "";
|
|
102
|
-
}
|
|
103
|
-
return `${resolved}`;
|
|
104
|
-
};
|
|
105
|
-
const resolvedPath = template
|
|
106
|
-
.replace(/\$\{([^}]+)\}/g, replaceValue)
|
|
107
|
-
.replace(/\{\$([^}]+)\}/g, replaceValue)
|
|
108
|
-
.trim();
|
|
109
|
-
if (missing || !resolvedPath.length) {
|
|
110
|
-
return { ready: false, path: null };
|
|
111
|
-
}
|
|
112
|
-
const segments = resolvedPath.split(".").filter(Boolean);
|
|
113
|
-
if (segments.length === 0 || !segments[0]) {
|
|
114
|
-
return { ready: false, path: null };
|
|
115
|
-
}
|
|
116
|
-
return { ready: true, path: resolvedPath };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getValueFromExpression(component: any, expression: string) {
|
|
120
|
-
if (!expression) return undefined;
|
|
121
|
-
const segments = expression.split(".").filter(Boolean);
|
|
122
|
-
if (segments.length === 0) return undefined;
|
|
123
|
-
let current: unknown = component;
|
|
124
|
-
for (const segment of segments) {
|
|
125
|
-
if (
|
|
126
|
-
current === undefined ||
|
|
127
|
-
current === null ||
|
|
128
|
-
typeof current !== "object"
|
|
129
|
-
) {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
current = (current as Record<string, unknown>)[segment];
|
|
133
|
-
}
|
|
134
|
-
return current;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getPublisherFromPath(path: string) {
|
|
138
|
-
const segments = path.split(".").filter((segment) => segment.length > 0);
|
|
139
|
-
if (segments.length === 0) return null;
|
|
140
|
-
const dataProvider = segments.shift() || "";
|
|
141
|
-
if (!dataProvider) return null;
|
|
142
|
-
let publisher = PublisherManager.get(dataProvider);
|
|
143
|
-
if (!publisher) return null;
|
|
144
|
-
publisher = Objects.traverse(publisher, segments);
|
|
145
|
-
return publisher as DataProvider | null;
|
|
146
|
-
}
|
|
3
|
+
import { extractDynamicDependencies, resolveDynamicPath } from "./dynamicPath";
|
|
4
|
+
import {
|
|
5
|
+
onAssignDynamicWatchKeys,
|
|
6
|
+
registerDynamicPropertyWatcher,
|
|
7
|
+
} from "./dynamicPropertyWatch";
|
|
8
|
+
import { getPublisherFromPath } from "./publisherPath";
|
|
147
9
|
|
|
148
10
|
type Callback = (...values: unknown[]) => void;
|
|
149
11
|
type PathConfiguration = {
|
|
@@ -279,8 +141,10 @@ export function onAssign(...values: Array<string>) {
|
|
|
279
141
|
for (const conf of confs) {
|
|
280
142
|
if (conf.pathConfig.isDynamic) {
|
|
281
143
|
for (const dependency of conf.pathConfig.dynamicDependencies) {
|
|
282
|
-
const unsubscribe =
|
|
283
|
-
|
|
144
|
+
const unsubscribe = registerDynamicPropertyWatcher(
|
|
145
|
+
onAssignDynamicWatchKeys.watcherStore,
|
|
146
|
+
onAssignDynamicWatchKeys.hooked,
|
|
147
|
+
component,
|
|
284
148
|
dependency,
|
|
285
149
|
() => refreshSubscriptions()
|
|
286
150
|
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DataProviderKey } from "../../utils/dataProviderKey";
|
|
3
|
+
import { publish } from "./publish";
|
|
4
|
+
|
|
5
|
+
type FormData = {
|
|
6
|
+
email: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const formKey = new DataProviderKey<FormData>("formData");
|
|
10
|
+
|
|
11
|
+
describe("publish", () => {
|
|
12
|
+
it("decorates property and allows get/set", () => {
|
|
13
|
+
class TestClass {
|
|
14
|
+
@publish(formKey.email)
|
|
15
|
+
email = "";
|
|
16
|
+
}
|
|
17
|
+
const instance = new TestClass();
|
|
18
|
+
instance.email = "test@example.com";
|
|
19
|
+
expect(instance.email).toBe("test@example.com");
|
|
20
|
+
});
|
|
21
|
+
});
|