@supersoniks/concorde 4.9.0 → 4.9.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.
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +195 -195
- package/concorde-core.es.js +3390 -3370
- package/dist/concorde-core.bundle.js +195 -195
- package/dist/concorde-core.es.js +3390 -3370
- package/dist/docs-mock-api-sw.js +3 -1
- package/dist/docs-mock-api-sw.js.map +2 -2
- package/dist/js/core/decorators/api.js +64 -12
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/utils/HTML.js +4 -5
- package/dist/js/core/utils/HTML.js.map +1 -1
- package/dist/robots.txt +15 -1
- package/dist/types/core/decorators/api.d.ts +14 -1
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/utils/HTML.d.ts.map +1 -1
- package/docs/assets/{index-t0-i22oI.css → index-Bnp7gVaA.css} +1 -1
- package/docs/assets/{index-BEgZEIbv.js → index-hElUgV9_.js} +894 -552
- package/docs/crawl/core/components/functional/date/date.html +190 -0
- package/docs/crawl/core/components/functional/fetch/fetch.html +109 -0
- package/docs/crawl/core/components/functional/if/if.html +27 -0
- package/docs/crawl/core/components/functional/list/list.html +70 -0
- package/docs/crawl/core/components/functional/mix/mix.html +45 -0
- package/docs/crawl/core/components/functional/queue/queue.html +96 -0
- package/docs/crawl/core/components/functional/router/router.html +106 -0
- package/docs/crawl/core/components/functional/sdui/sdui.html +269 -0
- package/docs/crawl/core/components/functional/states/states.html +66 -0
- package/docs/crawl/core/components/functional/submit/submit.html +160 -0
- package/docs/crawl/core/components/functional/subscriber/subscriber.html +67 -0
- package/docs/crawl/core/components/functional/value/value.html +51 -0
- package/docs/crawl/core/components/ui/alert/alert.html +110 -0
- package/docs/crawl/core/components/ui/alert-messages/alert-messages.html +23 -0
- package/docs/crawl/core/components/ui/badge/badge.html +119 -0
- package/docs/crawl/core/components/ui/button/button.html +149 -0
- package/docs/crawl/core/components/ui/captcha/captcha.html +30 -0
- package/docs/crawl/core/components/ui/card/card.html +99 -0
- package/docs/crawl/core/components/ui/divider/divider.html +58 -0
- package/docs/crawl/core/components/ui/form/checkbox/checkbox.html +73 -0
- package/docs/crawl/core/components/ui/form/fieldset/fieldset.html +135 -0
- package/docs/crawl/core/components/ui/form/form-actions/form-actions.html +79 -0
- package/docs/crawl/core/components/ui/form/form-layout/form-layout.html +62 -0
- package/docs/crawl/core/components/ui/form/input/input.html +109 -0
- package/docs/crawl/core/components/ui/form/input-autocomplete/input-autocomplete.html +106 -0
- package/docs/crawl/core/components/ui/form/radio/radio.html +56 -0
- package/docs/crawl/core/components/ui/form/select/select.html +73 -0
- package/docs/crawl/core/components/ui/form/switch/switch.html +56 -0
- package/docs/crawl/core/components/ui/form/textarea/textarea.html +59 -0
- package/docs/crawl/core/components/ui/group/group.html +82 -0
- package/docs/crawl/core/components/ui/icon/icon.html +133 -0
- package/docs/crawl/core/components/ui/image/image.html +110 -0
- package/docs/crawl/core/components/ui/link/link.html +42 -0
- package/docs/crawl/core/components/ui/loader/loader.html +50 -0
- package/docs/crawl/core/components/ui/menu/menu.html +322 -0
- package/docs/crawl/core/components/ui/modal/modal.html +117 -0
- package/docs/crawl/core/components/ui/pop/pop.html +102 -0
- package/docs/crawl/core/components/ui/progress/progress.html +58 -0
- package/docs/crawl/core/components/ui/table/table.html +413 -0
- package/docs/crawl/core/components/ui/toast/toast.html +122 -0
- package/docs/crawl/core/components/ui/tooltip/tooltip.html +79 -0
- package/docs/crawl/docs/_core-concept/dataFlow.html +166 -0
- package/docs/crawl/docs/_core-concept/overview.html +66 -0
- package/docs/crawl/docs/_core-concept/subscriber.html +75 -0
- package/docs/crawl/docs/_decorators/ancestor-attribute.html +116 -0
- package/docs/crawl/docs/_decorators/auto-subscribe.html +202 -0
- package/docs/crawl/docs/_decorators/bind.html +141 -0
- package/docs/crawl/docs/_decorators/get.html +120 -0
- package/docs/crawl/docs/_decorators/handle.html +165 -0
- package/docs/crawl/docs/_decorators/on-assign.html +355 -0
- package/docs/crawl/docs/_decorators/patch.html +52 -0
- package/docs/crawl/docs/_decorators/post.html +102 -0
- package/docs/crawl/docs/_decorators/publish.html +61 -0
- package/docs/crawl/docs/_decorators/put.html +49 -0
- package/docs/crawl/docs/_decorators/subscribe.html +109 -0
- package/docs/crawl/docs/_decorators/wait-for-ancestors.html +137 -0
- package/docs/crawl/docs/_directives/sub.html +93 -0
- package/docs/crawl/docs/_getting-started/ai-agents.html +134 -0
- package/docs/crawl/docs/_getting-started/concorde-manual-install.html +100 -0
- package/docs/crawl/docs/_getting-started/concorde-outside.html +58 -0
- package/docs/crawl/docs/_getting-started/create-a-component.html +111 -0
- package/docs/crawl/docs/_getting-started/my-first-component.html +189 -0
- package/docs/crawl/docs/_getting-started/my-first-subscriber.html +110 -0
- package/docs/crawl/docs/_getting-started/pubsub.html +62 -0
- package/docs/crawl/docs/_getting-started/start.html +76 -0
- package/docs/crawl/docs/_getting-started/theming.html +85 -0
- package/docs/crawl/docs/_misc/api-configuration.html +149 -0
- package/docs/crawl/docs/_misc/dataProviderKey.html +135 -0
- package/docs/crawl/docs/_misc/docs-mock-api.html +146 -0
- package/docs/crawl/docs/_misc/dynamic-path.html +180 -0
- package/docs/crawl/docs/_misc/endpoint.html +50 -0
- package/docs/crawl/docs/_misc/html-integration.html +47 -0
- package/docs/crawl/hubs/decorators.html +467 -0
- package/docs/crawl/hubs/form-components.html +357 -0
- package/docs/crawl/hubs/remote-data.html +374 -0
- package/docs/crawl/index.html +99 -0
- package/docs/docs-mock-api-sw.js +3 -1
- package/docs/docs-mock-api-sw.js.map +2 -2
- package/docs/index.html +3 -3
- package/docs/llms-full.txt +5423 -0
- package/docs/llms.txt +102 -0
- package/docs/robots.txt +15 -1
- package/docs/sitemap.xml +231 -0
- package/docs/src/docs/_decorators/ancestor-attribute.md +56 -8
- package/docs/src/docs/_decorators/get.md +47 -1
- package/docs/src/docs/search/docs-search.json +92 -2
- package/docs/src/tsconfig.declarations.json +22 -0
- package/docs/src/tsconfig.emit.json +23 -0
- package/docs/src/tsconfig.json +3 -285
- package/index.html +1 -0
- package/package.json +1 -1
- package/public/docs-mock-api-sw.js +3 -1
- package/public/docs-mock-api-sw.js.map +2 -2
- package/public/robots.txt +15 -1
- package/scripts/generate-crawl-docs.mjs +569 -0
- package/scripts/post-build-docs.js +18 -14
- package/src/core/decorators/api.spec.ts +74 -4
- package/src/core/decorators/api.ts +103 -20
- package/src/core/decorators/subscriber/ancestorAttribute.spec.ts +42 -0
- package/src/core/utils/HTML.ts +4 -5
- package/src/docs/_decorators/get.md +47 -1
- package/src/docs/components/docs-demo-sources.ts +7 -2
- package/src/docs/example/decorators-demo-geo.ts +5 -0
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +72 -31
- package/src/docs/mock-api/router.ts +3 -1
- package/src/docs/search/docs-search.json +92 -2
|
@@ -22,6 +22,15 @@ async function flushGetMicrotasks() {
|
|
|
22
22
|
await Promise.resolve();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function flushGetFrames() {
|
|
26
|
+
await new Promise<void>((resolve) => {
|
|
27
|
+
requestAnimationFrame(() => {
|
|
28
|
+
requestAnimationFrame(() => resolve());
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
await Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
function mockPayload<T>(result: T) {
|
|
26
35
|
return {
|
|
27
36
|
request: new Request("https://api.example.test/items/1"),
|
|
@@ -61,7 +70,7 @@ describe("get", () => {
|
|
|
61
70
|
}
|
|
62
71
|
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
63
72
|
document.body.appendChild(el);
|
|
64
|
-
await
|
|
73
|
+
await flushGetFrames();
|
|
65
74
|
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
66
75
|
expect(el.payload?.response?.ok).toBe(true);
|
|
67
76
|
expect(el.payload?.request.url).toContain("api.example.test");
|
|
@@ -88,7 +97,7 @@ describe("get", () => {
|
|
|
88
97
|
|
|
89
98
|
const el = document.createElement(tag) as InstanceType<typeof TestRow>;
|
|
90
99
|
document.body.appendChild(el);
|
|
91
|
-
await
|
|
100
|
+
await flushGetFrames();
|
|
92
101
|
|
|
93
102
|
expect(API.prototype.getDetailed).toHaveBeenCalledWith("rows/42");
|
|
94
103
|
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
@@ -136,15 +145,76 @@ describe("get", () => {
|
|
|
136
145
|
}
|
|
137
146
|
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
138
147
|
document.body.appendChild(el);
|
|
139
|
-
await
|
|
148
|
+
await flushGetFrames();
|
|
140
149
|
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
|
|
141
150
|
|
|
142
151
|
pub.set({
|
|
143
152
|
...staticConfig,
|
|
144
153
|
serviceURL: "https://api.example.test/v2",
|
|
145
154
|
});
|
|
146
|
-
await
|
|
155
|
+
await flushGetFrames();
|
|
147
156
|
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(2);
|
|
148
157
|
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
149
158
|
});
|
|
159
|
+
|
|
160
|
+
it("relance le GET quand le publisher trigger est invalidé", async () => {
|
|
161
|
+
const triggerPubId = "getSpecTrigger1";
|
|
162
|
+
const triggerKey = new DataProviderKey<number>(triggerPubId);
|
|
163
|
+
const itemEp = new Endpoint<{ id: string }>("items/1");
|
|
164
|
+
const tag = "test-api-get-trigger";
|
|
165
|
+
|
|
166
|
+
class C extends HTMLElement {
|
|
167
|
+
@get(itemEp, { triggerKey })
|
|
168
|
+
payload: ApiResult<{ id: string }> | null = null;
|
|
169
|
+
}
|
|
170
|
+
if (!customElements.get(tag)) {
|
|
171
|
+
customElements.define(tag, C);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const wrap = document.createElement("div");
|
|
175
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
176
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
177
|
+
wrap.appendChild(el);
|
|
178
|
+
document.body.appendChild(wrap);
|
|
179
|
+
|
|
180
|
+
await flushGetMicrotasks();
|
|
181
|
+
const callsBeforeInvalidate = vi.mocked(API.prototype.getDetailed).mock
|
|
182
|
+
.calls.length;
|
|
183
|
+
expect(callsBeforeInvalidate).toBeGreaterThanOrEqual(1);
|
|
184
|
+
|
|
185
|
+
PublisherManager.get(triggerPubId).invalidate();
|
|
186
|
+
await flushGetFrames();
|
|
187
|
+
expect(
|
|
188
|
+
vi.mocked(API.prototype.getDetailed).mock.calls.length,
|
|
189
|
+
).toBeGreaterThan(callsBeforeInvalidate);
|
|
190
|
+
expect(el.payload?.result).toEqual({ id: "loaded" });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("refetch automatique avec refetchEveryMs", async () => {
|
|
194
|
+
vi.useFakeTimers();
|
|
195
|
+
const itemEp = new Endpoint<{ id: string }>("items/1");
|
|
196
|
+
const tag = "test-api-get-refetch";
|
|
197
|
+
|
|
198
|
+
class C extends HTMLElement {
|
|
199
|
+
@get(itemEp, { refetchEveryMs: 1000 })
|
|
200
|
+
payload: ApiResult<{ id: string }> | null = null;
|
|
201
|
+
}
|
|
202
|
+
if (!customElements.get(tag)) {
|
|
203
|
+
customElements.define(tag, C);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const wrap = document.createElement("div");
|
|
207
|
+
wrap.setAttribute("serviceURL", "https://api.example.test");
|
|
208
|
+
wrap.appendChild(document.createElement(tag));
|
|
209
|
+
document.body.appendChild(wrap);
|
|
210
|
+
|
|
211
|
+
await flushGetMicrotasks();
|
|
212
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(1);
|
|
213
|
+
|
|
214
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
215
|
+
await flushGetMicrotasks();
|
|
216
|
+
expect(API.prototype.getDetailed).toHaveBeenCalledTimes(2);
|
|
217
|
+
|
|
218
|
+
vi.useRealTimers();
|
|
219
|
+
});
|
|
150
220
|
});
|
|
@@ -162,14 +162,25 @@ function watchScopedConfiguration(
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
type
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
type TriggerPublisherBinding = {
|
|
166
|
+
triggerPublisher: DataProvider | null;
|
|
167
|
+
triggerInvalidateHandler: (() => void) | null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
type RefetchScheduleState = {
|
|
171
|
+
refetchTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
172
|
+
refetchEveryMs: number;
|
|
171
173
|
};
|
|
172
174
|
|
|
175
|
+
type ApiGetState = TriggerPublisherBinding &
|
|
176
|
+
RefetchScheduleState & {
|
|
177
|
+
cleanupWatchers: Array<() => void>;
|
|
178
|
+
requestGeneration: number;
|
|
179
|
+
configPublisher: DataProvider | null;
|
|
180
|
+
configMutationHandler: (() => void) | null;
|
|
181
|
+
scopeWatchCleanup: (() => void) | null;
|
|
182
|
+
};
|
|
183
|
+
|
|
173
184
|
function detachConfigPublisher(state: ApiGetState): void {
|
|
174
185
|
if (state.configPublisher && state.configMutationHandler) {
|
|
175
186
|
state.configPublisher.offInternalMutation(state.configMutationHandler);
|
|
@@ -191,6 +202,9 @@ function detachConfigPublisher(state: ApiGetState): void {
|
|
|
191
202
|
* `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
|
|
192
203
|
* Toute mutation interne du publisher (`onInternalMutation`) relance le GET.
|
|
193
204
|
*
|
|
205
|
+
* **Options** (`GetOptions`) : `triggerKey` — `PublisherManager.get(...).invalidate()` relance le GET ;
|
|
206
|
+
* `refetchEveryMs` — polling automatique.
|
|
207
|
+
*
|
|
194
208
|
* @example
|
|
195
209
|
* @get(new Endpoint<User, { userId: string }>("users/${userId}"))
|
|
196
210
|
* payload?: ApiResult<User>;
|
|
@@ -200,6 +214,11 @@ function detachConfigPublisher(state: ApiGetState): void {
|
|
|
200
214
|
* PublisherManager.get("myApiConf").set({ serviceURL: "...", token: null, ... });
|
|
201
215
|
* @get(new Endpoint<Thing, { id: string }>("things/${id}"), apiConf)
|
|
202
216
|
* payload?: ApiResult<Thing>;
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* @get(usersEndpoint, { triggerKey: refreshUsersKey })
|
|
220
|
+
* payload?: ApiResult<User[]>;
|
|
221
|
+
* // PublisherManager.get("users.refresh").invalidate() → refetch
|
|
203
222
|
*/
|
|
204
223
|
export function get<T, Ue = any>(
|
|
205
224
|
endpoint: Endpoint<T, Ue>,
|
|
@@ -260,18 +279,24 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
260
279
|
skipEmptyPlaceholder: options?.skipEmptyPlaceholder,
|
|
261
280
|
};
|
|
262
281
|
const configurationKeyPath = configurationKey?.path;
|
|
282
|
+
const triggerKeyPath = options?.triggerKey?.path;
|
|
263
283
|
const endpointDynamicDependencies = extractDynamicDependencies(pathTemplate);
|
|
264
284
|
const configKeyDynamicDependencies = configurationKeyPath
|
|
265
285
|
? extractDynamicDependencies(configurationKeyPath)
|
|
266
286
|
: [];
|
|
287
|
+
const triggerKeyDynamicDependencies = triggerKeyPath
|
|
288
|
+
? extractDynamicDependencies(triggerKeyPath)
|
|
289
|
+
: [];
|
|
267
290
|
const mergedDynamicDependencies = [
|
|
268
291
|
...new Set([
|
|
269
292
|
...endpointDynamicDependencies,
|
|
270
293
|
...configKeyDynamicDependencies,
|
|
294
|
+
...triggerKeyDynamicDependencies,
|
|
271
295
|
]),
|
|
272
296
|
];
|
|
273
297
|
const isDynamicPath = endpointDynamicDependencies.length > 0;
|
|
274
298
|
const usesPublisherConfig = Boolean(configurationKeyPath);
|
|
299
|
+
const refetchEveryMs = options?.refetchEveryMs ?? 0;
|
|
275
300
|
|
|
276
301
|
return function (target: object, propertyKey: string) {
|
|
277
302
|
if (!target) return;
|
|
@@ -288,6 +313,10 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
288
313
|
configPublisher: null,
|
|
289
314
|
configMutationHandler: null,
|
|
290
315
|
scopeWatchCleanup: null,
|
|
316
|
+
triggerPublisher: null,
|
|
317
|
+
triggerInvalidateHandler: null,
|
|
318
|
+
refetchTimeoutId: null,
|
|
319
|
+
refetchEveryMs,
|
|
291
320
|
};
|
|
292
321
|
comp[stateKey] = state;
|
|
293
322
|
}
|
|
@@ -295,6 +324,17 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
295
324
|
state.cleanupWatchers.forEach((cleanup) => cleanup());
|
|
296
325
|
state.cleanupWatchers = [];
|
|
297
326
|
state.requestGeneration++;
|
|
327
|
+
state.refetchEveryMs = refetchEveryMs;
|
|
328
|
+
clearRefetch(state);
|
|
329
|
+
|
|
330
|
+
const scheduleRefetch = () => {
|
|
331
|
+
clearRefetch(state);
|
|
332
|
+
if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
|
|
333
|
+
state.refetchTimeoutId = setTimeout(() => {
|
|
334
|
+
state.refetchTimeoutId = null;
|
|
335
|
+
runFetch();
|
|
336
|
+
}, state.refetchEveryMs);
|
|
337
|
+
};
|
|
298
338
|
|
|
299
339
|
const runFetch = () => {
|
|
300
340
|
const resolution = isDynamicPath
|
|
@@ -302,6 +342,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
302
342
|
: { ready: true, path: pathTemplate };
|
|
303
343
|
if (!resolution.ready || !resolution.path) {
|
|
304
344
|
comp[propertyKey] = undefined;
|
|
345
|
+
scheduleRefetch();
|
|
305
346
|
return;
|
|
306
347
|
}
|
|
307
348
|
let config: APIConfiguration | null = null;
|
|
@@ -313,6 +354,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
313
354
|
);
|
|
314
355
|
if (!configRes.ready || !configRes.path) {
|
|
315
356
|
comp[propertyKey] = undefined;
|
|
357
|
+
scheduleRefetch();
|
|
316
358
|
return;
|
|
317
359
|
}
|
|
318
360
|
const configPublisher = getPublisherFromPath(configRes.path);
|
|
@@ -329,6 +371,7 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
329
371
|
state.scopeWatchCleanup = null;
|
|
330
372
|
});
|
|
331
373
|
}
|
|
374
|
+
scheduleRefetch();
|
|
332
375
|
return;
|
|
333
376
|
}
|
|
334
377
|
const generation = ++state.requestGeneration;
|
|
@@ -338,9 +381,31 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
338
381
|
.then((payload?: ApiResult<T>) => {
|
|
339
382
|
if (generation !== state.requestGeneration) return;
|
|
340
383
|
comp[propertyKey] = payload;
|
|
384
|
+
scheduleRefetch();
|
|
341
385
|
});
|
|
342
386
|
};
|
|
343
387
|
|
|
388
|
+
const scheduleFetch = createAnimationFrameCoalescedRunner(runFetch);
|
|
389
|
+
|
|
390
|
+
const rebindTriggerPublisher = () => {
|
|
391
|
+
detachTriggerPublisher(state);
|
|
392
|
+
if (!triggerKeyPath) return;
|
|
393
|
+
const triggerResolution = resolveDynamicPath(
|
|
394
|
+
component,
|
|
395
|
+
triggerKeyPath,
|
|
396
|
+
pathOptions,
|
|
397
|
+
);
|
|
398
|
+
if (!triggerResolution.ready || !triggerResolution.path) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const publisher = getPublisherFromPath(triggerResolution.path);
|
|
402
|
+
if (!publisher) return;
|
|
403
|
+
const invalidateHandler = () => scheduleFetch();
|
|
404
|
+
publisher.onInvalidate(invalidateHandler);
|
|
405
|
+
state.triggerPublisher = publisher;
|
|
406
|
+
state.triggerInvalidateHandler = invalidateHandler;
|
|
407
|
+
};
|
|
408
|
+
|
|
344
409
|
const rebindPublisherConfig = () => {
|
|
345
410
|
if (!usesPublisherConfig || !configurationKeyPath) return;
|
|
346
411
|
detachConfigPublisher(state);
|
|
@@ -358,26 +423,32 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
358
423
|
comp[propertyKey] = undefined;
|
|
359
424
|
return;
|
|
360
425
|
}
|
|
361
|
-
const mutationHandler = () =>
|
|
362
|
-
runFetch();
|
|
363
|
-
};
|
|
426
|
+
const mutationHandler = () => scheduleFetch();
|
|
364
427
|
publisher.onInternalMutation(mutationHandler);
|
|
365
428
|
state.configPublisher = publisher;
|
|
366
429
|
state.configMutationHandler = mutationHandler;
|
|
367
430
|
};
|
|
368
431
|
|
|
369
|
-
|
|
432
|
+
const rebindAll = () => {
|
|
433
|
+
rebindTriggerPublisher();
|
|
434
|
+
rebindPublisherConfig();
|
|
435
|
+
if (!usesPublisherConfig) {
|
|
436
|
+
scheduleFetch();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (usesPublisherConfig || triggerKeyDynamicDependencies.length) {
|
|
370
441
|
for (const dependency of mergedDynamicDependencies) {
|
|
371
442
|
const unsubscribe = observeDynamicProperty(
|
|
372
443
|
getDynamicWatchKeys.watcherStore,
|
|
373
444
|
getDynamicWatchKeys.hooked,
|
|
374
445
|
component,
|
|
375
446
|
dependency,
|
|
376
|
-
() =>
|
|
447
|
+
() => rebindAll(),
|
|
377
448
|
);
|
|
378
449
|
state.cleanupWatchers.push(unsubscribe);
|
|
379
450
|
}
|
|
380
|
-
|
|
451
|
+
rebindAll();
|
|
381
452
|
} else {
|
|
382
453
|
const startScopedFetch = () => {
|
|
383
454
|
if (isDynamicPath) {
|
|
@@ -387,11 +458,12 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
387
458
|
getDynamicWatchKeys.hooked,
|
|
388
459
|
component,
|
|
389
460
|
dependency,
|
|
390
|
-
() =>
|
|
461
|
+
() => scheduleFetch(),
|
|
391
462
|
);
|
|
392
463
|
state.cleanupWatchers.push(unsubscribe);
|
|
393
464
|
}
|
|
394
465
|
}
|
|
466
|
+
rebindTriggerPublisher();
|
|
395
467
|
runFetch();
|
|
396
468
|
};
|
|
397
469
|
state.cleanupWatchers.push(
|
|
@@ -405,6 +477,8 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
405
477
|
const state = comp[stateKey] as ApiGetState | undefined;
|
|
406
478
|
if (!state) return;
|
|
407
479
|
detachConfigPublisher(state);
|
|
480
|
+
detachTriggerPublisher(state);
|
|
481
|
+
clearRefetch(state);
|
|
408
482
|
state.cleanupWatchers.forEach((cleanup) => cleanup());
|
|
409
483
|
state.cleanupWatchers = [];
|
|
410
484
|
state.requestGeneration++;
|
|
@@ -413,7 +487,12 @@ export function get<T, Ue = any, Uk = any, _Ut = any>(
|
|
|
413
487
|
};
|
|
414
488
|
}
|
|
415
489
|
|
|
416
|
-
export type GetOptions = DynamicPathOptions
|
|
490
|
+
export type GetOptions<Uk = any> = DynamicPathOptions & {
|
|
491
|
+
/** Intervalle de refetch automatique (ms). 0 ou absent = pas de polling. */
|
|
492
|
+
refetchEveryMs?: number;
|
|
493
|
+
/** Publisher dont `invalidate()` relance le GET. */
|
|
494
|
+
triggerKey?: DataProviderKey<unknown, Uk>;
|
|
495
|
+
};
|
|
417
496
|
|
|
418
497
|
export type ApiSendOptions<Uk = any> = DynamicPathOptions & {
|
|
419
498
|
/** Intervalle de renvoi automatique (ms). 0 ou absent = pas de polling. */
|
|
@@ -436,7 +515,11 @@ export type PatchOptions<Uk = any> = ApiSendOptions<Uk>;
|
|
|
436
515
|
|
|
437
516
|
function isGetOptions(value: unknown): value is GetOptions {
|
|
438
517
|
if (!value || typeof value !== "object") return false;
|
|
439
|
-
return
|
|
518
|
+
return (
|
|
519
|
+
"skipEmptyPlaceholder" in value ||
|
|
520
|
+
"refetchEveryMs" in value ||
|
|
521
|
+
"triggerKey" in value
|
|
522
|
+
);
|
|
440
523
|
}
|
|
441
524
|
|
|
442
525
|
function isApiSendOptions(value: unknown): value is ApiSendOptions {
|
|
@@ -469,7 +552,7 @@ function detachBodyPublisher(state: ApiSendState): void {
|
|
|
469
552
|
state.bodyMutationHandler = null;
|
|
470
553
|
}
|
|
471
554
|
|
|
472
|
-
function detachTriggerPublisher(state:
|
|
555
|
+
function detachTriggerPublisher(state: TriggerPublisherBinding): void {
|
|
473
556
|
if (state.triggerPublisher && state.triggerInvalidateHandler) {
|
|
474
557
|
state.triggerPublisher.offInvalidate(state.triggerInvalidateHandler);
|
|
475
558
|
}
|
|
@@ -477,7 +560,7 @@ function detachTriggerPublisher(state: ApiSendState): void {
|
|
|
477
560
|
state.triggerInvalidateHandler = null;
|
|
478
561
|
}
|
|
479
562
|
|
|
480
|
-
function
|
|
563
|
+
function clearRefetch(state: RefetchScheduleState): void {
|
|
481
564
|
if (state.refetchTimeoutId) {
|
|
482
565
|
clearTimeout(state.refetchTimeoutId);
|
|
483
566
|
state.refetchTimeoutId = null;
|
|
@@ -582,10 +665,10 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
|
|
|
582
665
|
state.refetchEveryMs = refetchEveryMs;
|
|
583
666
|
state.skipIfBodyMissing = skipIfBodyMissing;
|
|
584
667
|
state.autoPostOnBodyMutation = autoPostOnBodyMutation;
|
|
585
|
-
|
|
668
|
+
clearRefetch(state);
|
|
586
669
|
|
|
587
670
|
const scheduleRefetch = () => {
|
|
588
|
-
|
|
671
|
+
clearRefetch(state);
|
|
589
672
|
if (!state.refetchEveryMs || state.refetchEveryMs <= 0) return;
|
|
590
673
|
state.refetchTimeoutId = setTimeout(() => {
|
|
591
674
|
state.refetchTimeoutId = null;
|
|
@@ -789,7 +872,7 @@ function createApiSendDecorator<T, B, Ue = any, Uk = any, Ut = any>(
|
|
|
789
872
|
detachConfigPublisher(state);
|
|
790
873
|
detachBodyPublisher(state);
|
|
791
874
|
detachTriggerPublisher(state);
|
|
792
|
-
|
|
875
|
+
clearRefetch(state);
|
|
793
876
|
state.cleanupWatchers.forEach((cleanup) => cleanup());
|
|
794
877
|
state.cleanupWatchers = [];
|
|
795
878
|
state.requestGeneration++;
|
|
@@ -55,6 +55,25 @@ describe("ancestorAttribute", () => {
|
|
|
55
55
|
expect(el.dataProvider).toBe("scope-a");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it("assigne une chaine vide si l'attribut ancêtre est présent sans valeur (mode statique)", () => {
|
|
59
|
+
const tag = "test-ancestor-attr-static-empty";
|
|
60
|
+
class C extends HTMLElement {
|
|
61
|
+
@ancestorAttribute("is_guichet")
|
|
62
|
+
isGuichet: string | null = null;
|
|
63
|
+
}
|
|
64
|
+
if (!customElements.get(tag)) {
|
|
65
|
+
customElements.define(tag, C);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const wrap = document.createElement("div");
|
|
69
|
+
wrap.setAttribute("is_guichet", "");
|
|
70
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
71
|
+
wrap.appendChild(el);
|
|
72
|
+
document.body.appendChild(wrap);
|
|
73
|
+
|
|
74
|
+
expect(el.isGuichet).toBe("");
|
|
75
|
+
});
|
|
76
|
+
|
|
58
77
|
it("met à jour la propriété quand un ancêtre change (mode dynamique)", async () => {
|
|
59
78
|
const tag = "test-ancestor-attr-dynamic-mutation";
|
|
60
79
|
class C extends HTMLElement {
|
|
@@ -101,6 +120,29 @@ describe("ancestorAttribute", () => {
|
|
|
101
120
|
expect(el.dataProvider).toBeNull();
|
|
102
121
|
});
|
|
103
122
|
|
|
123
|
+
it("met à jour avec une chaine vide si l'attribut ancêtre devient présent sans valeur (mode dynamique)", async () => {
|
|
124
|
+
const tag = "test-ancestor-attr-dynamic-empty";
|
|
125
|
+
class C extends HTMLElement {
|
|
126
|
+
@ancestorAttribute("is_guichet", { dynamic: true })
|
|
127
|
+
isGuichet: string | null = null;
|
|
128
|
+
}
|
|
129
|
+
if (!customElements.get(tag)) {
|
|
130
|
+
customElements.define(tag, C);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const wrap = document.createElement("div");
|
|
134
|
+
const el = document.createElement(tag) as InstanceType<typeof C>;
|
|
135
|
+
wrap.appendChild(el);
|
|
136
|
+
document.body.appendChild(wrap);
|
|
137
|
+
|
|
138
|
+
expect(el.isGuichet).toBeNull();
|
|
139
|
+
|
|
140
|
+
wrap.setAttribute("is_guichet", "");
|
|
141
|
+
await flushFrames();
|
|
142
|
+
|
|
143
|
+
expect(el.isGuichet).toBe("");
|
|
144
|
+
});
|
|
145
|
+
|
|
104
146
|
it("retombe sur l'ancêtre suivant quand le plus proche perd l'attribut", async () => {
|
|
105
147
|
const tag = "test-ancestor-attr-dynamic-fallback";
|
|
106
148
|
class C extends HTMLElement {
|
package/src/core/utils/HTML.ts
CHANGED
|
@@ -71,10 +71,9 @@ class HTML {
|
|
|
71
71
|
): string | null {
|
|
72
72
|
for (const attrName of HTML.scopeAttributeNames(attributeName)) {
|
|
73
73
|
if (element.hasAttribute(attrName)) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
74
|
+
// Un attribut HTML peut exister sans valeur (ex: <x is_guichet>),
|
|
75
|
+
// on retourne donc "" plutôt que null pour préserver ce cas.
|
|
76
|
+
return element.getAttribute(attrName) ?? "";
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
return null;
|
|
@@ -94,7 +93,7 @@ class HTML {
|
|
|
94
93
|
while (current) {
|
|
95
94
|
if (current instanceof HTMLElement) {
|
|
96
95
|
const value = HTML.readScopeAttributeOnElement(current, attributeName);
|
|
97
|
-
if (value != null
|
|
96
|
+
if (value != null) {
|
|
98
97
|
return value;
|
|
99
98
|
}
|
|
100
99
|
}
|
|
@@ -13,10 +13,53 @@ Pass an [`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint) as the first argument.
|
|
|
13
13
|
|
|
14
14
|
`${prop}` on the endpoint or config key is resolved on the host. While a placeholder is `null` or `undefined`, no GET runs. Optional `skipEmptyPlaceholder: true` also blocks `""` (empty string only). Details: [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path).
|
|
15
15
|
|
|
16
|
+
## Options (`GetOptions`)
|
|
17
|
+
|
|
18
|
+
Pass as the **second** argument (scoped config) or **third** (with a configuration key). Same shape as send decorators where applicable:
|
|
19
|
+
|
|
20
|
+
| Option | Description |
|
|
21
|
+
|--------|-------------|
|
|
22
|
+
| `skipEmptyPlaceholder` | Treat `""` as not ready (see [Dynamic path](#docs/_misc/dynamic-path.md/dynamic-path)). |
|
|
23
|
+
| `refetchEveryMs` | Automatic polling interval (ms). `0` or omitted = no polling. |
|
|
24
|
+
| `triggerKey` | `DataProviderKey` — `PublisherManager.get(...).invalidate()` on that publisher re-runs the GET. |
|
|
25
|
+
|
|
26
|
+
### Manual refetch (`triggerKey`)
|
|
27
|
+
|
|
28
|
+
Same pattern as [@post](#docs/_decorators/post.md/post): a publisher acts as a **signal**, independent of the response payload.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { get, type ApiResult } from "@supersoniks/concorde/decorators";
|
|
32
|
+
import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
|
|
33
|
+
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
34
|
+
import { dp } from "@supersoniks/concorde/core/utils/PublisherProxy";
|
|
35
|
+
|
|
36
|
+
const geoCommunesEndpoint = new Endpoint<GeoCommuneRow[]>(
|
|
37
|
+
"communes?limit=5&fields=nom,code",
|
|
38
|
+
);
|
|
39
|
+
const geoCommunesRefreshKey = new DataProviderKey<void>(
|
|
40
|
+
"docsDemoGeoCommunesRefresh",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
@get(geoCommunesEndpoint, apiConfigurationKey, {
|
|
44
|
+
triggerKey: geoCommunesRefreshKey,
|
|
45
|
+
})
|
|
46
|
+
@state()
|
|
47
|
+
payload: ApiResult<GeoCommuneRow[]> | null = null;
|
|
48
|
+
|
|
49
|
+
// Same component or anywhere else:
|
|
50
|
+
dp(geoCommunesRefreshKey).invalidate();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Prefer `triggerKey` over mutating the API configuration publisher when you only want to **reload the same URL** — no need to touch `serviceURL`, tokens, etc.
|
|
54
|
+
|
|
55
|
+
The docs mock adds an **`X-Fetched-At`** response header on `/docs-mock-api/geo/communes` so live demos can show when the last fetch happened (`response.headers.get("X-Fetched-At")`).
|
|
56
|
+
|
|
16
57
|
## When the GET runs again
|
|
17
58
|
|
|
18
59
|
- A referenced Lit property changes (endpoint path and/or config key contains `${...}`).
|
|
19
60
|
- `set` on the active configuration publisher (`onInternalMutation`).
|
|
61
|
+
- `triggerKey` publisher `invalidate()`.
|
|
62
|
+
- `refetchEveryMs` polling (after each successful fetch).
|
|
20
63
|
|
|
21
64
|
## Import
|
|
22
65
|
|
|
@@ -42,6 +85,8 @@ payload: ApiResult<User> | null = null;
|
|
|
42
85
|
|
|
43
86
|
## Live demos
|
|
44
87
|
|
|
88
|
+
`@get` with **`triggerKey`** — refresh button + `X-Fetched-At` timestamp in the UI:
|
|
89
|
+
|
|
45
90
|
<sonic-code>
|
|
46
91
|
<template>
|
|
47
92
|
<docs-demo-sources for="demo-api-get"></docs-demo-sources>
|
|
@@ -49,12 +94,13 @@ payload: ApiResult<User> | null = null;
|
|
|
49
94
|
</template>
|
|
50
95
|
</sonic-code>
|
|
51
96
|
|
|
52
|
-
Dynamic config and endpoint path (`
|
|
97
|
+
Dynamic config and endpoint path. Manual refetch is triggered **from a separate component** (same `triggerKey` publisher):
|
|
53
98
|
|
|
54
99
|
<sonic-code>
|
|
55
100
|
<template>
|
|
56
101
|
<docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
|
|
57
102
|
<demo-api-get-configuration-key></demo-api-get-configuration-key>
|
|
103
|
+
<demo-api-get-refresh-remote></demo-api-get-refresh-remote>
|
|
58
104
|
</template>
|
|
59
105
|
</sonic-code>
|
|
60
106
|
|
|
@@ -255,10 +255,15 @@ export const DOCS_DEMO_SOURCE_REGISTRY: Record<string, DocsSource[]> = {
|
|
|
255
255
|
"demo-api-get-configuration-key": [
|
|
256
256
|
{
|
|
257
257
|
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
258
|
-
line:
|
|
258
|
+
line: 147,
|
|
259
259
|
label: "demo-api-get-configuration-key",
|
|
260
260
|
},
|
|
261
|
-
{
|
|
261
|
+
{
|
|
262
|
+
path: "src/docs/example/decorators-demo-subscribe-publish-get-demos.ts",
|
|
263
|
+
line: 239,
|
|
264
|
+
label: "demo-api-get-refresh-remote",
|
|
265
|
+
},
|
|
266
|
+
{ path: "src/docs/example/decorators-demo-geo.ts", label: "geo API + triggerKey" },
|
|
262
267
|
],
|
|
263
268
|
"demo-api-get-publish-subscribe": [
|
|
264
269
|
{
|
|
@@ -49,3 +49,8 @@ export const geoCommunesApiGetEndpointDynamic = new Endpoint<GeoCommuneRow[]>(
|
|
|
49
49
|
export const geoCommunesApiGetPublishKey = new DataProviderKey<
|
|
50
50
|
ApiResult<GeoCommuneRow[]>
|
|
51
51
|
>(geoCommunesApiGetEndpoint.path);
|
|
52
|
+
|
|
53
|
+
/** Signal de refetch manuel pour les démos `@get` (voir `triggerKey`). */
|
|
54
|
+
export const docsDemoGeoCommunesRefreshKey = new DataProviderKey<void>(
|
|
55
|
+
"docsDemoGeoCommunesRefresh",
|
|
56
|
+
);
|