@nubitio/core 0.5.10 → 0.5.13
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/dist/index.cjs +334 -140
- package/dist/index.d.cts +73 -5
- package/dist/index.d.mts +73 -5
- package/dist/index.mjs +327 -142
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import i18next from "i18next";
|
|
@@ -7,7 +7,8 @@ const _coreConfig = {
|
|
|
7
7
|
locale: "es",
|
|
8
8
|
timezone: "UTC",
|
|
9
9
|
apiBaseUrl: "/api/",
|
|
10
|
-
currency: void 0
|
|
10
|
+
currency: void 0,
|
|
11
|
+
mercureTopicOrigin: void 0
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
14
|
* Configure core runtime values (locale, timezone, apiBaseUrl).
|
|
@@ -19,6 +20,7 @@ function configureCore(config) {
|
|
|
19
20
|
if (config.timezone !== void 0) _coreConfig.timezone = config.timezone;
|
|
20
21
|
if (config.apiBaseUrl !== void 0) _coreConfig.apiBaseUrl = config.apiBaseUrl;
|
|
21
22
|
if ("currency" in config) _coreConfig.currency = config.currency;
|
|
23
|
+
if ("mercureTopicOrigin" in config) _coreConfig.mercureTopicOrigin = config.mercureTopicOrigin;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* @deprecated Use configureCore() instead.
|
|
@@ -37,24 +39,30 @@ function getCoreApiBaseUrl() {
|
|
|
37
39
|
function getCoreCurrency() {
|
|
38
40
|
return _coreConfig.currency;
|
|
39
41
|
}
|
|
42
|
+
function getMercureTopicOrigin() {
|
|
43
|
+
return _coreConfig.mercureTopicOrigin;
|
|
44
|
+
}
|
|
40
45
|
const CoreConfigContext = React.createContext(_coreConfig);
|
|
41
|
-
const CoreConfigProvider = ({ locale, timezone, apiBaseUrl, currency, children }) => {
|
|
46
|
+
const CoreConfigProvider = ({ locale, timezone, apiBaseUrl, currency, mercureTopicOrigin, children }) => {
|
|
42
47
|
configureCore({
|
|
43
48
|
locale,
|
|
44
49
|
timezone,
|
|
45
50
|
apiBaseUrl,
|
|
46
|
-
currency
|
|
51
|
+
currency,
|
|
52
|
+
mercureTopicOrigin
|
|
47
53
|
});
|
|
48
54
|
const value = React.useMemo(() => ({
|
|
49
55
|
locale,
|
|
50
56
|
timezone,
|
|
51
57
|
apiBaseUrl,
|
|
52
|
-
currency
|
|
58
|
+
currency,
|
|
59
|
+
mercureTopicOrigin
|
|
53
60
|
}), [
|
|
54
61
|
locale,
|
|
55
62
|
timezone,
|
|
56
63
|
apiBaseUrl,
|
|
57
|
-
currency
|
|
64
|
+
currency,
|
|
65
|
+
mercureTopicOrigin
|
|
58
66
|
]);
|
|
59
67
|
return /* @__PURE__ */ jsx(CoreConfigContext.Provider, {
|
|
60
68
|
value,
|
|
@@ -489,6 +497,230 @@ function initCoreI18n() {
|
|
|
489
497
|
i18next.addResourceBundle("en", "core", coreTranslationsEn, true, false);
|
|
490
498
|
}
|
|
491
499
|
//#endregion
|
|
500
|
+
//#region packages/core/mercure/MercureManager.ts
|
|
501
|
+
var MercureManager = class {
|
|
502
|
+
hubUrl = null;
|
|
503
|
+
topics = /* @__PURE__ */ new Map();
|
|
504
|
+
hubUrlListeners = /* @__PURE__ */ new Set();
|
|
505
|
+
/**
|
|
506
|
+
* Update the hub URL (called by the Axios interceptor when the `Link` header is discovered,
|
|
507
|
+
* or by MercureProvider). Notifies all registered listeners.
|
|
508
|
+
*
|
|
509
|
+
* Idempotent: if the URL is already set to the same value, listeners are NOT re-notified.
|
|
510
|
+
*/
|
|
511
|
+
setHubUrl(url) {
|
|
512
|
+
if (this.hubUrl === url) return;
|
|
513
|
+
this.disconnectAll();
|
|
514
|
+
this.hubUrl = url;
|
|
515
|
+
this.hubUrlListeners.forEach((cb) => cb(url));
|
|
516
|
+
}
|
|
517
|
+
getHubUrl() {
|
|
518
|
+
return this.hubUrl;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Register a callback that fires whenever the hub URL changes.
|
|
522
|
+
*
|
|
523
|
+
* Returns an unsubscribe function — call it in a `useEffect` cleanup to avoid memory leaks.
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```tsx
|
|
527
|
+
* useEffect(() => {
|
|
528
|
+
* return MercureManager.onHubUrlChange(setHubUrl);
|
|
529
|
+
* }, []);
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
onHubUrlChange(callback) {
|
|
533
|
+
this.hubUrlListeners.add(callback);
|
|
534
|
+
return () => {
|
|
535
|
+
this.hubUrlListeners.delete(callback);
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Subscribe to a Mercure topic.
|
|
540
|
+
*
|
|
541
|
+
* @param topic Full topic URI or URI Template, e.g. `https://host/api/products/{id}`.
|
|
542
|
+
* @param callback Called with the parsed JSON payload on each SSE message.
|
|
543
|
+
*
|
|
544
|
+
* If `hubUrl` is null (hub not configured), this is a no-op (graceful degradation).
|
|
545
|
+
* If an EventSource for this topic already exists, it is reused (ref-counting).
|
|
546
|
+
*/
|
|
547
|
+
subscribe(topic, callback) {
|
|
548
|
+
if (this.hubUrl === null) return;
|
|
549
|
+
const existing = this.topics.get(topic);
|
|
550
|
+
if (existing) {
|
|
551
|
+
const listener = (e) => {
|
|
552
|
+
try {
|
|
553
|
+
callback(JSON.parse(e.data));
|
|
554
|
+
} catch {
|
|
555
|
+
callback(e.data);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
existing.eventSource.addEventListener("message", listener);
|
|
559
|
+
existing.listeners.set(callback, listener);
|
|
560
|
+
existing.count += 1;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const url = new URL(this.hubUrl);
|
|
564
|
+
url.searchParams.append("topic", topic);
|
|
565
|
+
const eventSource = new EventSource(url.toString(), { withCredentials: true });
|
|
566
|
+
const listener = (e) => {
|
|
567
|
+
try {
|
|
568
|
+
callback(JSON.parse(e.data));
|
|
569
|
+
} catch {
|
|
570
|
+
callback(e.data);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
eventSource.addEventListener("message", listener);
|
|
574
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
575
|
+
listeners.set(callback, listener);
|
|
576
|
+
this.topics.set(topic, {
|
|
577
|
+
eventSource,
|
|
578
|
+
count: 1,
|
|
579
|
+
listeners
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Unsubscribe a specific callback from a topic.
|
|
584
|
+
*
|
|
585
|
+
* Decrements the ref count. When count reaches 0, the EventSource is closed
|
|
586
|
+
* and removed from the internal map.
|
|
587
|
+
*/
|
|
588
|
+
unsubscribe(topic, callback) {
|
|
589
|
+
const entry = this.topics.get(topic);
|
|
590
|
+
if (!entry) return;
|
|
591
|
+
const listener = entry.listeners.get(callback);
|
|
592
|
+
if (listener) {
|
|
593
|
+
entry.eventSource.removeEventListener("message", listener);
|
|
594
|
+
entry.listeners.delete(callback);
|
|
595
|
+
}
|
|
596
|
+
entry.count -= 1;
|
|
597
|
+
if (entry.count <= 0) {
|
|
598
|
+
entry.eventSource.close();
|
|
599
|
+
this.topics.delete(topic);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Close all active EventSource connections and clear the topic map.
|
|
604
|
+
*
|
|
605
|
+
* Called internally when the hub URL changes so stale connections
|
|
606
|
+
* bound to the previous hub are not leaked.
|
|
607
|
+
*/
|
|
608
|
+
disconnectAll() {
|
|
609
|
+
this.topics.forEach((entry) => {
|
|
610
|
+
entry.eventSource.close();
|
|
611
|
+
});
|
|
612
|
+
this.topics.clear();
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
/** Singleton instance — shared across the entire application. */
|
|
616
|
+
const mercureManager = new MercureManager();
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region packages/core/mercure/mercureDiscovery.ts
|
|
619
|
+
let discoveredTopicOrigin;
|
|
620
|
+
const topicOriginListeners = /* @__PURE__ */ new Set();
|
|
621
|
+
/**
|
|
622
|
+
* Parse a `Link` response header value into `{ url, rel }` pairs.
|
|
623
|
+
* Handles API Platform's `</.well-known/mercure>; rel="mercure"` format.
|
|
624
|
+
*/
|
|
625
|
+
function parseLinkHeader(header) {
|
|
626
|
+
const links = [];
|
|
627
|
+
for (const segment of header.split(/,(?=\s*<)/)) {
|
|
628
|
+
const urlMatch = segment.match(/<([^>]+)>/);
|
|
629
|
+
const relMatch = segment.match(/rel=(?:"([^"]+)"|([^;\s,]+))/i);
|
|
630
|
+
if (urlMatch && relMatch) links.push({
|
|
631
|
+
url: urlMatch[1],
|
|
632
|
+
rel: (relMatch[1] ?? relMatch[2]).trim()
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
return links;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* When the API advertises an absolute hub on another origin (common in Docker:
|
|
639
|
+
* `MERCURE_PUBLIC_URL=http://localhost:3000/...` while the SPA proxies the hub
|
|
640
|
+
* at `/.well-known/mercure`), prefer the same-origin path so EventSource and
|
|
641
|
+
* cookies go through the dev proxy.
|
|
642
|
+
*/
|
|
643
|
+
function normalizeMercureHubUrl(hubUrl) {
|
|
644
|
+
if (typeof window === "undefined") return hubUrl;
|
|
645
|
+
try {
|
|
646
|
+
const parsed = new URL(hubUrl, window.location.origin);
|
|
647
|
+
if (parsed.pathname === "/.well-known/mercure" && parsed.origin !== window.location.origin) return `${window.location.origin}${parsed.pathname}`;
|
|
648
|
+
return parsed.toString();
|
|
649
|
+
} catch {
|
|
650
|
+
return hubUrl;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Extract the Mercure hub URL from response headers, if present.
|
|
655
|
+
* Relative URLs are resolved against the request URL.
|
|
656
|
+
*/
|
|
657
|
+
function extractMercureHubUrl(headers, responseUrl) {
|
|
658
|
+
const linkHeader = headers.get("link") ?? headers.get("Link");
|
|
659
|
+
if (!linkHeader) return null;
|
|
660
|
+
const mercureLink = parseLinkHeader(linkHeader).find((link) => link.rel === "mercure");
|
|
661
|
+
if (!mercureLink) return null;
|
|
662
|
+
try {
|
|
663
|
+
return normalizeMercureHubUrl(new URL(mercureLink.url, responseUrl).toString());
|
|
664
|
+
} catch {
|
|
665
|
+
return normalizeMercureHubUrl(mercureLink.url);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function originFromAbsoluteIri(iri) {
|
|
669
|
+
if (!/^https?:\/\//i.test(iri)) return null;
|
|
670
|
+
try {
|
|
671
|
+
return new URL(iri).origin;
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Infer the API origin from Hydra `@id` values (entrypoint or collection items).
|
|
678
|
+
* API Platform publishes absolute IRIs when `DEFAULT_URI` is configured.
|
|
679
|
+
*/
|
|
680
|
+
function extractTopicOriginFromPayload(data) {
|
|
681
|
+
if (!data || typeof data !== "object") return null;
|
|
682
|
+
const record = data;
|
|
683
|
+
if (typeof record["@id"] === "string") {
|
|
684
|
+
const origin = originFromAbsoluteIri(record["@id"]);
|
|
685
|
+
if (origin) return origin;
|
|
686
|
+
}
|
|
687
|
+
const members = record["hydra:member"];
|
|
688
|
+
if (Array.isArray(members)) for (const member of members) {
|
|
689
|
+
if (!member || typeof member !== "object") continue;
|
|
690
|
+
const memberId = member["@id"];
|
|
691
|
+
if (typeof memberId === "string") {
|
|
692
|
+
const origin = originFromAbsoluteIri(memberId);
|
|
693
|
+
if (origin) return origin;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
function getDiscoveredMercureTopicOrigin() {
|
|
699
|
+
return discoveredTopicOrigin;
|
|
700
|
+
}
|
|
701
|
+
/** Subscribe to autodiscovered topic-origin changes (for React hooks). */
|
|
702
|
+
function onMercureTopicOriginChange(callback) {
|
|
703
|
+
topicOriginListeners.add(callback);
|
|
704
|
+
return () => {
|
|
705
|
+
topicOriginListeners.delete(callback);
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function setDiscoveredTopicOrigin(origin) {
|
|
709
|
+
if (discoveredTopicOrigin === origin) return;
|
|
710
|
+
discoveredTopicOrigin = origin;
|
|
711
|
+
topicOriginListeners.forEach((listener) => listener(origin));
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Inspect an API response and update Mercure hub URL / topic origin when found.
|
|
715
|
+
* Called from `CoreHttpClient` on every successful response.
|
|
716
|
+
*/
|
|
717
|
+
function discoverMercureFromResponse(response, data) {
|
|
718
|
+
const hubUrl = extractMercureHubUrl(response.headers, response.url);
|
|
719
|
+
if (hubUrl) mercureManager.setHubUrl(hubUrl);
|
|
720
|
+
const topicOrigin = extractTopicOriginFromPayload(data);
|
|
721
|
+
if (topicOrigin) setDiscoveredTopicOrigin(topicOrigin);
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
492
724
|
//#region packages/core/http/CoreHttpClient.ts
|
|
493
725
|
function joinUrl(baseUrl, url) {
|
|
494
726
|
if (/^https?:\/\//.test(url) || url.startsWith("/")) return url;
|
|
@@ -584,12 +816,16 @@ var CoreHttpClient = class {
|
|
|
584
816
|
signal: config?.signal,
|
|
585
817
|
body: body === void 0 || body === null ? void 0 : body instanceof FormData ? body : JSON.stringify(body)
|
|
586
818
|
});
|
|
587
|
-
if (response.ok)
|
|
588
|
-
response,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
819
|
+
if (response.ok) {
|
|
820
|
+
const data = await readResponseBody(response, config?.responseType);
|
|
821
|
+
if (config?.responseType === void 0 || config.responseType === "json") discoverMercureFromResponse(response, data);
|
|
822
|
+
return {
|
|
823
|
+
response,
|
|
824
|
+
data,
|
|
825
|
+
headers: response.headers,
|
|
826
|
+
status: response.status
|
|
827
|
+
};
|
|
828
|
+
}
|
|
593
829
|
const errorData = await this.safeErrorData(response);
|
|
594
830
|
const error = createHttpError(errorData.detail ?? errorData.message ?? "HTTP request failed", response.status, errorData);
|
|
595
831
|
const loginPath = this.config.loginPath ?? "auth/login";
|
|
@@ -688,124 +924,6 @@ function CoreProvider({ children, http, httpClient, runtime }) {
|
|
|
688
924
|
});
|
|
689
925
|
}
|
|
690
926
|
//#endregion
|
|
691
|
-
//#region packages/core/mercure/MercureManager.ts
|
|
692
|
-
var MercureManager = class {
|
|
693
|
-
hubUrl = null;
|
|
694
|
-
topics = /* @__PURE__ */ new Map();
|
|
695
|
-
hubUrlListeners = /* @__PURE__ */ new Set();
|
|
696
|
-
/**
|
|
697
|
-
* Update the hub URL (called by the Axios interceptor when the `Link` header is discovered,
|
|
698
|
-
* or by MercureProvider). Notifies all registered listeners.
|
|
699
|
-
*
|
|
700
|
-
* Idempotent: if the URL is already set to the same value, listeners are NOT re-notified.
|
|
701
|
-
*/
|
|
702
|
-
setHubUrl(url) {
|
|
703
|
-
if (this.hubUrl === url) return;
|
|
704
|
-
this.disconnectAll();
|
|
705
|
-
this.hubUrl = url;
|
|
706
|
-
this.hubUrlListeners.forEach((cb) => cb(url));
|
|
707
|
-
}
|
|
708
|
-
getHubUrl() {
|
|
709
|
-
return this.hubUrl;
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Register a callback that fires whenever the hub URL changes.
|
|
713
|
-
*
|
|
714
|
-
* Returns an unsubscribe function — call it in a `useEffect` cleanup to avoid memory leaks.
|
|
715
|
-
*
|
|
716
|
-
* @example
|
|
717
|
-
* ```tsx
|
|
718
|
-
* useEffect(() => {
|
|
719
|
-
* return MercureManager.onHubUrlChange(setHubUrl);
|
|
720
|
-
* }, []);
|
|
721
|
-
* ```
|
|
722
|
-
*/
|
|
723
|
-
onHubUrlChange(callback) {
|
|
724
|
-
this.hubUrlListeners.add(callback);
|
|
725
|
-
return () => {
|
|
726
|
-
this.hubUrlListeners.delete(callback);
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Subscribe to a Mercure topic.
|
|
731
|
-
*
|
|
732
|
-
* @param topic Full topic URI or URI Template, e.g. `https://host/api/products/{id}`.
|
|
733
|
-
* @param callback Called with the parsed JSON payload on each SSE message.
|
|
734
|
-
*
|
|
735
|
-
* If `hubUrl` is null (hub not configured), this is a no-op (graceful degradation).
|
|
736
|
-
* If an EventSource for this topic already exists, it is reused (ref-counting).
|
|
737
|
-
*/
|
|
738
|
-
subscribe(topic, callback) {
|
|
739
|
-
if (this.hubUrl === null) return;
|
|
740
|
-
const existing = this.topics.get(topic);
|
|
741
|
-
if (existing) {
|
|
742
|
-
const listener = (e) => {
|
|
743
|
-
try {
|
|
744
|
-
callback(JSON.parse(e.data));
|
|
745
|
-
} catch {
|
|
746
|
-
callback(e.data);
|
|
747
|
-
}
|
|
748
|
-
};
|
|
749
|
-
existing.eventSource.addEventListener("message", listener);
|
|
750
|
-
existing.listeners.set(callback, listener);
|
|
751
|
-
existing.count += 1;
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
const url = new URL(this.hubUrl);
|
|
755
|
-
url.searchParams.append("topic", topic);
|
|
756
|
-
const eventSource = new EventSource(url.toString(), { withCredentials: true });
|
|
757
|
-
const listener = (e) => {
|
|
758
|
-
try {
|
|
759
|
-
callback(JSON.parse(e.data));
|
|
760
|
-
} catch {
|
|
761
|
-
callback(e.data);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
eventSource.addEventListener("message", listener);
|
|
765
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
766
|
-
listeners.set(callback, listener);
|
|
767
|
-
this.topics.set(topic, {
|
|
768
|
-
eventSource,
|
|
769
|
-
count: 1,
|
|
770
|
-
listeners
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Unsubscribe a specific callback from a topic.
|
|
775
|
-
*
|
|
776
|
-
* Decrements the ref count. When count reaches 0, the EventSource is closed
|
|
777
|
-
* and removed from the internal map.
|
|
778
|
-
*/
|
|
779
|
-
unsubscribe(topic, callback) {
|
|
780
|
-
const entry = this.topics.get(topic);
|
|
781
|
-
if (!entry) return;
|
|
782
|
-
const listener = entry.listeners.get(callback);
|
|
783
|
-
if (listener) {
|
|
784
|
-
entry.eventSource.removeEventListener("message", listener);
|
|
785
|
-
entry.listeners.delete(callback);
|
|
786
|
-
}
|
|
787
|
-
entry.count -= 1;
|
|
788
|
-
if (entry.count <= 0) {
|
|
789
|
-
entry.eventSource.close();
|
|
790
|
-
this.topics.delete(topic);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Close all active EventSource connections and clear the topic map.
|
|
795
|
-
*
|
|
796
|
-
* Called internally when the hub URL changes so stale connections
|
|
797
|
-
* bound to the previous hub are not leaked.
|
|
798
|
-
*/
|
|
799
|
-
disconnectAll() {
|
|
800
|
-
this.topics.forEach((entry) => {
|
|
801
|
-
entry.eventSource.close();
|
|
802
|
-
});
|
|
803
|
-
this.topics.clear();
|
|
804
|
-
}
|
|
805
|
-
};
|
|
806
|
-
/** Singleton instance — shared across the entire application. */
|
|
807
|
-
const mercureManager = new MercureManager();
|
|
808
|
-
//#endregion
|
|
809
927
|
//#region packages/core/mercure/MercureProvider.tsx
|
|
810
928
|
/**
|
|
811
929
|
* MercureProvider — React context provider for the Mercure hub URL.
|
|
@@ -818,14 +936,19 @@ const mercureManager = new MercureManager();
|
|
|
818
936
|
* ## Usage
|
|
819
937
|
*
|
|
820
938
|
* ```tsx
|
|
821
|
-
* //
|
|
822
|
-
* <MercureProvider
|
|
939
|
+
* // Autodiscovery (recommended): hub URL comes from API `Link` headers.
|
|
940
|
+
* <MercureProvider>
|
|
941
|
+
* <App />
|
|
942
|
+
* </MercureProvider>
|
|
943
|
+
*
|
|
944
|
+
* // Manual override (e.g. before the first API response):
|
|
945
|
+
* <MercureProvider hubUrl="/.well-known/mercure">
|
|
823
946
|
* <App />
|
|
824
947
|
* </MercureProvider>
|
|
825
948
|
* ```
|
|
826
949
|
*
|
|
827
|
-
* The
|
|
828
|
-
* (
|
|
950
|
+
* The hub URL is autodiscovered from the `Link` header of API responses
|
|
951
|
+
* (API Platform + Mercure). Pass `hubUrl` only when you need an explicit override.
|
|
829
952
|
*/
|
|
830
953
|
/** The hub URL, or null if Mercure is not configured / not yet discovered. */
|
|
831
954
|
const MercureContext = createContext(null);
|
|
@@ -833,10 +956,17 @@ const MercureContext = createContext(null);
|
|
|
833
956
|
* Provides the Mercure hub URL to the React tree and keeps `MercureManager`
|
|
834
957
|
* in sync whenever the URL changes.
|
|
835
958
|
*/
|
|
836
|
-
function MercureProvider({ hubUrl, children }) {
|
|
959
|
+
function MercureProvider({ hubUrl: hubUrlOverride, children }) {
|
|
960
|
+
const [discoveredHubUrl, setDiscoveredHubUrl] = useState(() => mercureManager.getHubUrl());
|
|
837
961
|
useEffect(() => {
|
|
838
|
-
|
|
839
|
-
|
|
962
|
+
if (hubUrlOverride !== void 0) {
|
|
963
|
+
mercureManager.setHubUrl(hubUrlOverride);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
setDiscoveredHubUrl(mercureManager.getHubUrl());
|
|
967
|
+
return mercureManager.onHubUrlChange(setDiscoveredHubUrl);
|
|
968
|
+
}, [hubUrlOverride]);
|
|
969
|
+
const hubUrl = hubUrlOverride !== void 0 ? hubUrlOverride : discoveredHubUrl;
|
|
840
970
|
return /* @__PURE__ */ jsx(MercureContext.Provider, {
|
|
841
971
|
value: hubUrl,
|
|
842
972
|
children
|
|
@@ -853,7 +983,8 @@ function MercureProvider({ hubUrl, children }) {
|
|
|
853
983
|
* - Mercure is not configured in the backend.
|
|
854
984
|
*
|
|
855
985
|
* This hook does NOT perform any fetch or side effect — it only reads from context.
|
|
856
|
-
* Hub discovery
|
|
986
|
+
* Hub discovery happens automatically via `CoreHttpClient` (`Link` header parsing).
|
|
987
|
+
* Use `<MercureProvider>` without a `hubUrl` prop to expose the discovered URL.
|
|
857
988
|
*
|
|
858
989
|
* ## Usage
|
|
859
990
|
*
|
|
@@ -870,6 +1001,52 @@ function useMercureHub() {
|
|
|
870
1001
|
return useContext(MercureContext);
|
|
871
1002
|
}
|
|
872
1003
|
//#endregion
|
|
1004
|
+
//#region packages/core/mercure/mercureTopics.ts
|
|
1005
|
+
/**
|
|
1006
|
+
* Resolves the origin used to build Mercure collection topic URIs.
|
|
1007
|
+
*
|
|
1008
|
+
* API Platform publishes item updates to IRIs such as
|
|
1009
|
+
* `http://localhost:8000/api/products/42`. The SPA may run on another origin
|
|
1010
|
+
* in dev (Vite on :5173, API on :8000). The origin is normally autodiscovered
|
|
1011
|
+
* from absolute Hydra `@id` values on API responses. Set `mercureTopicOrigin`
|
|
1012
|
+
* on CoreConfig only when autodiscovery cannot infer the correct origin.
|
|
1013
|
+
*/
|
|
1014
|
+
function resolveMercureTopicOrigin(apiBaseUrl = getCoreApiBaseUrl(), configuredOrigin, discoveredOrigin = getDiscoveredMercureTopicOrigin()) {
|
|
1015
|
+
const explicit = configuredOrigin?.trim().replace(/\/+$/, "");
|
|
1016
|
+
if (explicit) return explicit;
|
|
1017
|
+
const discovered = discoveredOrigin?.trim().replace(/\/+$/, "");
|
|
1018
|
+
if (discovered) return discovered;
|
|
1019
|
+
const trimmedBase = apiBaseUrl.trim();
|
|
1020
|
+
if (/^https?:\/\//i.test(trimmedBase)) try {
|
|
1021
|
+
return new URL(trimmedBase).origin;
|
|
1022
|
+
} catch {}
|
|
1023
|
+
if (typeof window !== "undefined") return window.location.origin;
|
|
1024
|
+
return "";
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Wildcard collection topic (RFC 6570 URI Template) for a resource API path.
|
|
1028
|
+
* e.g. `/api/products` → `http://localhost:8000/api/products/{id}`
|
|
1029
|
+
*/
|
|
1030
|
+
function buildMercureCollectionTopic(apiUrl, configuredOrigin, apiBaseUrl = getCoreApiBaseUrl()) {
|
|
1031
|
+
if (!apiUrl) return null;
|
|
1032
|
+
const origin = resolveMercureTopicOrigin(apiBaseUrl, configuredOrigin);
|
|
1033
|
+
if (!origin) return null;
|
|
1034
|
+
const normalizedPath = apiUrl.replace(/^\//, "").replace(/\/+$/, "");
|
|
1035
|
+
if (!normalizedPath) return null;
|
|
1036
|
+
return `${origin}/${normalizedPath}/{id}`;
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region packages/core/mercure/useDiscoveredMercureTopicOrigin.ts
|
|
1040
|
+
/**
|
|
1041
|
+
* Returns the Mercure topic origin autodiscovered from Hydra `@id` IRIs.
|
|
1042
|
+
* Re-renders when a new origin is discovered from a subsequent API response.
|
|
1043
|
+
*/
|
|
1044
|
+
function useDiscoveredMercureTopicOrigin() {
|
|
1045
|
+
const [origin, setOrigin] = useState(getDiscoveredMercureTopicOrigin);
|
|
1046
|
+
useEffect(() => onMercureTopicOriginChange(setOrigin), []);
|
|
1047
|
+
return origin;
|
|
1048
|
+
}
|
|
1049
|
+
//#endregion
|
|
873
1050
|
//#region packages/core/mercure/useMercureSubscription.ts
|
|
874
1051
|
/**
|
|
875
1052
|
* useMercureSubscription — Hook that subscribes to a Mercure topic and calls
|
|
@@ -879,6 +1056,9 @@ function useMercureHub() {
|
|
|
879
1056
|
* - If `hubUrl` is null (Mercure not configured) or `enabled` is false → no-op.
|
|
880
1057
|
* - Subscribes to the wildcard topic `<origin>/<apiUrl>/{id}` (URI Template RFC 6570),
|
|
881
1058
|
* which captures any item-level event (create / update / delete) for the collection.
|
|
1059
|
+
* - `<origin>` comes from `CoreConfig.mercureTopicOrigin`, autodiscovered Hydra
|
|
1060
|
+
* `@id`, an absolute `apiBaseUrl`, or `window.location.origin`
|
|
1061
|
+
* (see `buildMercureCollectionTopic`).
|
|
882
1062
|
* - Cleanup: unsubscribes on unmount or when dependencies change (no memory leaks).
|
|
883
1063
|
*
|
|
884
1064
|
* ## Usage
|
|
@@ -898,10 +1078,12 @@ function useMercureHub() {
|
|
|
898
1078
|
*/
|
|
899
1079
|
function useMercureSubscription(apiUrl, onUpdate, enabled = true) {
|
|
900
1080
|
const hubUrl = useMercureHub();
|
|
1081
|
+
const { mercureTopicOrigin, apiBaseUrl } = useCoreConfig();
|
|
1082
|
+
const discoveredTopicOrigin = useDiscoveredMercureTopicOrigin();
|
|
901
1083
|
useEffect(() => {
|
|
902
1084
|
if (!hubUrl || !enabled || !apiUrl) return;
|
|
903
|
-
const
|
|
904
|
-
|
|
1085
|
+
const topic = buildMercureCollectionTopic(apiUrl, mercureTopicOrigin ?? discoveredTopicOrigin, apiBaseUrl);
|
|
1086
|
+
if (!topic) return;
|
|
905
1087
|
const handler = (_data) => {
|
|
906
1088
|
onUpdate();
|
|
907
1089
|
};
|
|
@@ -912,8 +1094,11 @@ function useMercureSubscription(apiUrl, onUpdate, enabled = true) {
|
|
|
912
1094
|
}, [
|
|
913
1095
|
hubUrl,
|
|
914
1096
|
enabled,
|
|
915
|
-
apiUrl
|
|
1097
|
+
apiUrl,
|
|
1098
|
+
mercureTopicOrigin,
|
|
1099
|
+
discoveredTopicOrigin,
|
|
1100
|
+
apiBaseUrl
|
|
916
1101
|
]);
|
|
917
1102
|
}
|
|
918
1103
|
//#endregion
|
|
919
|
-
export { CoreConfigProvider, CoreHttpClient, CoreHttpProvider, CoreProvider, CoreRuntimeProvider, DEFAULT_TIMEZONE, DateUtils, mercureManager as MercureManager, MercureProvider, configureCore, configureCoreDate, coreTranslationsEn, coreTranslationsEs, createCoreHttpClient, createCrudEvents, createScopedEventBus, dispatch, getCoreApiBaseUrl, getCoreCurrency, getCoreLocale, getCoreTimezone, initCoreI18n, useCoreConfig, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureHub, useMercureSubscription };
|
|
1104
|
+
export { CoreConfigProvider, CoreHttpClient, CoreHttpProvider, CoreProvider, CoreRuntimeProvider, DEFAULT_TIMEZONE, DateUtils, mercureManager as MercureManager, MercureProvider, buildMercureCollectionTopic, configureCore, configureCoreDate, coreTranslationsEn, coreTranslationsEs, createCoreHttpClient, createCrudEvents, createScopedEventBus, discoverMercureFromResponse, dispatch, extractMercureHubUrl, extractTopicOriginFromPayload, getCoreApiBaseUrl, getCoreCurrency, getCoreLocale, getCoreTimezone, getDiscoveredMercureTopicOrigin, getMercureTopicOrigin, initCoreI18n, parseLinkHeader, resolveMercureTopicOrigin, useCoreConfig, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useDiscoveredMercureTopicOrigin, useEvents, useMercureHub, useMercureSubscription };
|
package/package.json
CHANGED