@hitachivantara/app-shell-services 1.1.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 ADDED
@@ -0,0 +1,149 @@
1
+ # @hitachivantara/app-shell-services
2
+
3
+ Hitachi Vantara App Shell Services. Support package to manage services at the App Shell ecosystem.
4
+
5
+ ## Overview
6
+
7
+ This package provides service management capabilities including:
8
+
9
+ - Service registration and resolution
10
+ - React hooks for service consumption
11
+ - Type-safe service interfaces
12
+
13
+ ## Usage
14
+
15
+ Services supports a consumer/provider model where:
16
+
17
+ - Consumers, like an app owning a header action for example, will consume a given service _contract_ either from the shared-services package from other app or, if also a provider, its own shared-services package. The services are identified by an `id` that, ideally, should be unique while also providing information about the type it expects like `main-app:AppsMetadata`. This way, the consumers know the type they expect and the providers know what to implement, without collisions in the multi-tenant App Shell ecosystem.
18
+ - Providers implement services that match the consumer's contract and register them under the consumer's `id` in its `app-shell.config.ts` file as example below:
19
+
20
+ ```typescript
21
+ services: {
22
+ "@some-package/main-app-services:AppsMetadata": [{
23
+ instance: {
24
+ value: {
25
+ name: "dummy app",
26
+ description: "This is 'dummy app' description",
27
+ version: 2.0,
28
+ final: false,
29
+ }
30
+ }
31
+ }],
32
+ }
33
+ ```
34
+
35
+ This way, looking at the configuration, it is clear that there is a consumer application (main-app) that expects service-providers to implement the `AppsMetadata` contract and register them under its `@some-package/main-app-services:AppsMetadata` service definition, keeping the consumer resilient: as long as service-providers adhere to the declared contract the consumer code will not break.
36
+
37
+ ## Example (consumer)
38
+
39
+ Exploring the example above, the consumer should register the service contract and definition in a package that can be shared between the consumer and the provider:
40
+
41
+ ```typescript
42
+ // @some-package/main-app-services
43
+ export type AppsMetadata = {
44
+ appId: string;
45
+ appName: string;
46
+ version: number;
47
+ isRelease: boolean;
48
+ };
49
+
50
+ /**
51
+ * Service definitions that the consumer 'main app' exposes on a services shared package.
52
+ */
53
+ export const MainAppServiceDefinitions = {
54
+ /**
55
+ * The {@const AppsMetadata} service represents the service-provider application metadata.
56
+ *
57
+ * The service-providers that implement this contract will have its implementation displayed on a page.
58
+ *
59
+ * Instances of this service are typescript constants of type {@link AppsMetadata}.
60
+ */
61
+ AppsMetadata: {
62
+ id: "@some-package/main-app-services:AppsMetadata",
63
+ },
64
+ };
65
+ ```
66
+
67
+ The main app consumes all the service definitions under its identifier using the `useServices` hook, which returns all the successfully loaded services of the requested identifier, namely the `MainAppServiceDefinitions.AppsMetadata` service definition.
68
+ This way, as long as any provider implements and registers a service that matches the `AppsMetadata` contract, it will be safe to render as expected:
69
+
70
+ ```typescript
71
+ const AboutApps: FC = () => {
72
+ const { services: appsMetadata[], isPending, error } = useServices<AppsMetadata[]>(
73
+ ServiceDefinitions.AppsMetadata.id,
74
+ );
75
+
76
+ if (isPending) {
77
+ return <HvLoading>Loading apps metadata from services...</HvLoading>;
78
+ }
79
+
80
+ if (error) {
81
+ const errorMessage = `Failed to apps metadata: ${ServiceDefinitions.AppsMetadata.id}`;
82
+ return <HvTypography>{errorMessage}</HvTypography>;
83
+ }
84
+
85
+ return (
86
+ <HvContainer maxWidth="lg">
87
+ <HvTypography variant="title1">Applications information</HvTypography>
88
+ <HvGrid container spacing={3}>
89
+ {appsMetadata.map((metadata, index) => {
90
+ return (
91
+ <HvGrid item key={metadata.appId}>
92
+ <HvTypography>
93
+ <strong>Application: </strong> {metadata.appName}
94
+ </HvTypography>
95
+ <HvTypography>
96
+ {metadata.version} - {metadata.isRelease ? "Release" : "In development"}
97
+ </HvTypography>
98
+ </HvGrid>
99
+ );
100
+ })}
101
+ </HvGrid>
102
+ </HvContainer>
103
+ );
104
+ };
105
+ ```
106
+
107
+ ## Example (provider)
108
+
109
+ A simple way to implement a service-provider that matches the `AppsMetadata` contract is a constant like below:
110
+
111
+ ```typescript
112
+ import { AppsMetadata } from "@some-package/main-app-services";
113
+
114
+ const dummyAppMetadata: AppsMetadata = {
115
+ appId: "ProviderApp",
116
+ appName: "A provider app",
117
+ version: 1.0,
118
+ isRelease: true,
119
+ };
120
+ ```
121
+
122
+ and having it registered on its `app-shell.config.ts` file, under the consumer service identifier.
123
+
124
+ ```typescript
125
+ //...
126
+ services: {
127
+ "@some-package/main-app-services:AppsMetadata": [
128
+ {
129
+ instance: {
130
+ bundle: "dummy-app/metadata/dummyAppMetadata.js",
131
+ },
132
+ ranking: 100,
133
+ }
134
+ ],
135
+ },
136
+ //...
137
+ ```
138
+
139
+ ### More examples
140
+
141
+ To see more examples, please check the [Default App](../../apps/default-app) `ServicesDemo` page and the `app-shell.config.ts` file.
142
+
143
+ ## Installation
144
+
145
+ The App Shell Services is available as an NPM package, and can be installed with:
146
+
147
+ ```bash
148
+ npm install @hitachivantara/app-shell-services
149
+ ```
@@ -0,0 +1,347 @@
1
+ import { useState, useEffect, createContext, useMemo, useContext, useCallback, useRef } from "react";
2
+ import { isEqual, cloneDeep } from "lodash";
3
+ import { jsx } from "react/jsx-runtime";
4
+ function useAsync(promiseFactory, options) {
5
+ const dataProp = options?.dataProp ?? "data";
6
+ const pendingData = options?.pendingData;
7
+ const [data, setData] = useState(
8
+ pendingData
9
+ );
10
+ const [error, setError] = useState(null);
11
+ const [isPending, setIsPending] = useState(true);
12
+ useEffect(() => {
13
+ let isMounted = true;
14
+ promiseFactory().then((result) => {
15
+ if (isMounted) {
16
+ setData(() => result);
17
+ setError(null);
18
+ setIsPending(false);
19
+ }
20
+ }).catch((err) => {
21
+ if (isMounted) {
22
+ setError(err);
23
+ setData(void 0);
24
+ setIsPending(false);
25
+ }
26
+ });
27
+ return () => {
28
+ isMounted = false;
29
+ };
30
+ }, [promiseFactory]);
31
+ if (error) {
32
+ return {
33
+ isPending: false,
34
+ error,
35
+ [dataProp]: void 0
36
+ };
37
+ }
38
+ if (isPending) {
39
+ return {
40
+ isPending: true,
41
+ error: null,
42
+ [dataProp]: pendingData
43
+ };
44
+ }
45
+ return {
46
+ isPending: false,
47
+ error: null,
48
+ [dataProp]: data
49
+ };
50
+ }
51
+ function createServiceReferenceBase(serviceId, serviceConfig, getService) {
52
+ return {
53
+ serviceId,
54
+ ranking: serviceConfig.ranking ?? 0,
55
+ getService
56
+ };
57
+ }
58
+ function validateImportedModule(imported, serviceId, bundlePath) {
59
+ if (!imported) {
60
+ throw new Error(
61
+ `Bundle import failed for ${bundlePath} and service ${serviceId}`
62
+ );
63
+ }
64
+ if (typeof imported === "object" && "default" in imported && imported.default != null) {
65
+ return imported.default;
66
+ }
67
+ throw new Error(
68
+ `ESM default export missing for ${bundlePath} (service ${serviceId}). Ensure the module uses a default export.`
69
+ );
70
+ }
71
+ function selectValueOrBundle(provider, serviceId, kind) {
72
+ const hasValue = "value" in provider && provider.value !== void 0;
73
+ const hasBundle = "bundle" in provider && typeof provider.bundle === "string";
74
+ if (hasValue) {
75
+ if (hasBundle) {
76
+ console.warn(
77
+ `${kind} service config for ${serviceId} contains both 'value' and 'bundle'. 'value' takes precedence and 'bundle' will be ignored.`
78
+ );
79
+ }
80
+ return { mode: "value", value: provider.value };
81
+ }
82
+ if (hasBundle) {
83
+ return { mode: "bundle", bundle: provider.bundle, config: provider.config };
84
+ }
85
+ throw new Error(`Invalid ${kind} service config for ${serviceId}`);
86
+ }
87
+ function createInstanceReference(serviceId, config) {
88
+ const selection = selectValueOrBundle(
89
+ config.instance,
90
+ serviceId,
91
+ "instance"
92
+ );
93
+ const serviceLoader = async () => {
94
+ if (selection.mode === "value") {
95
+ return selection.value;
96
+ }
97
+ const imported = await import(
98
+ /* @vite-ignore */
99
+ selection.bundle
100
+ );
101
+ return validateImportedModule(
102
+ imported,
103
+ serviceId,
104
+ selection.bundle
105
+ );
106
+ };
107
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
108
+ }
109
+ function createFactoryReference(serviceId, config) {
110
+ let loaded = false;
111
+ let serviceInstance;
112
+ const selection = selectValueOrBundle(
113
+ config.factory,
114
+ serviceId,
115
+ "factory"
116
+ );
117
+ const serviceLoader = async () => {
118
+ if (!loaded) {
119
+ let factoryExport;
120
+ let providedConfig;
121
+ if (selection.mode === "value") {
122
+ factoryExport = selection.value;
123
+ } else {
124
+ const imported = await import(
125
+ /* @vite-ignore */
126
+ selection.bundle
127
+ );
128
+ factoryExport = validateImportedModule(
129
+ imported,
130
+ serviceId,
131
+ selection.bundle
132
+ );
133
+ providedConfig = selection.config;
134
+ }
135
+ if (typeof factoryExport !== "function") {
136
+ throw new Error(
137
+ `Factory service for ${serviceId} did not resolve to a function. Value: ${factoryExport}`
138
+ );
139
+ }
140
+ const factoryFn = factoryExport;
141
+ serviceInstance = factoryFn(providedConfig);
142
+ loaded = true;
143
+ }
144
+ return serviceInstance;
145
+ };
146
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
147
+ }
148
+ function createComponentReference(serviceId, config) {
149
+ let loaded = false;
150
+ let serviceInstance;
151
+ const selection = selectValueOrBundle(
152
+ config.component,
153
+ serviceId,
154
+ "component"
155
+ );
156
+ const serviceLoader = async () => {
157
+ if (!loaded) {
158
+ let componentExport;
159
+ let providedConfig;
160
+ if (selection.mode === "value") {
161
+ componentExport = selection.value;
162
+ } else {
163
+ const imported = await import(
164
+ /* @vite-ignore */
165
+ selection.bundle
166
+ );
167
+ componentExport = validateImportedModule(
168
+ imported,
169
+ serviceId,
170
+ selection.bundle
171
+ );
172
+ providedConfig = selection.config;
173
+ }
174
+ if (typeof componentExport !== "function") {
175
+ throw new Error(
176
+ `Component definition for service ${serviceId} is not a React component function`
177
+ );
178
+ }
179
+ serviceInstance = bindComponent(
180
+ componentExport,
181
+ providedConfig ?? {}
182
+ );
183
+ loaded = true;
184
+ }
185
+ return serviceInstance;
186
+ };
187
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
188
+ }
189
+ function createServiceReference(serviceId, serviceConfig) {
190
+ if ("instance" in serviceConfig) {
191
+ return createInstanceReference(serviceId, serviceConfig);
192
+ }
193
+ if ("factory" in serviceConfig) {
194
+ return createFactoryReference(serviceId, serviceConfig);
195
+ }
196
+ if ("component" in serviceConfig) {
197
+ return createComponentReference(
198
+ serviceId,
199
+ serviceConfig
200
+ );
201
+ }
202
+ throw new Error(
203
+ `Unsupported service provider configuration for service ${serviceId}`
204
+ );
205
+ }
206
+ function bindComponent(Component, configProps) {
207
+ const BoundComponent = (props) => {
208
+ const mergedProps = { ...configProps, ...props };
209
+ return /* @__PURE__ */ jsx(Component, { ...mergedProps });
210
+ };
211
+ return BoundComponent;
212
+ }
213
+ function createServiceReferenceMap(services = {}) {
214
+ return Object.entries(services).reduce(
215
+ (map, [serviceId, serviceConfigs]) => {
216
+ const serviceReferences = serviceConfigs.map(
217
+ (serviceConfig) => createServiceReference(serviceId, serviceConfig)
218
+ ).sort((a, b) => b.ranking - a.ranking);
219
+ map.set(serviceId, serviceReferences);
220
+ return map;
221
+ },
222
+ /* @__PURE__ */ new Map()
223
+ );
224
+ }
225
+ function createServiceManager({
226
+ services
227
+ }) {
228
+ const serviceReferenceMap = createServiceReferenceMap(services);
229
+ function queryServiceReferences(serviceId, _options) {
230
+ return serviceReferenceMap.get(serviceId) ?? [];
231
+ }
232
+ function queryServiceReference(serviceId, options) {
233
+ const references = queryServiceReferences(serviceId);
234
+ if (references.length === 0) {
235
+ throw new Error(`Service ${serviceId} has no service providers.`);
236
+ }
237
+ return references[0];
238
+ }
239
+ return {
240
+ getServiceReference(serviceId, options) {
241
+ return queryServiceReference(serviceId);
242
+ },
243
+ getServiceReferences(serviceId, options) {
244
+ return queryServiceReferences(serviceId);
245
+ },
246
+ async getService(serviceId, options) {
247
+ const reference = queryServiceReference(serviceId);
248
+ return reference.getService();
249
+ },
250
+ async getServices(serviceId, options) {
251
+ const references = queryServiceReferences(serviceId);
252
+ if (references.length === 0) {
253
+ return [];
254
+ }
255
+ const errorHandling = options?.errorHandling ?? "reject-on-all-failures";
256
+ const results = await Promise.allSettled(
257
+ references.map((reference) => reference.getService())
258
+ );
259
+ const successful = [];
260
+ const errors = [];
261
+ results.forEach((result) => {
262
+ if (result.status === "fulfilled") {
263
+ successful.push(result.value);
264
+ } else {
265
+ errors.push(
266
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason))
267
+ );
268
+ }
269
+ });
270
+ switch (errorHandling) {
271
+ case "reject-on-any-failure":
272
+ if (errors.length > 0) {
273
+ throw errors[0];
274
+ }
275
+ break;
276
+ case "reject-on-all-failures":
277
+ if (errors.length === results.length) {
278
+ throw new Error(
279
+ `All service providers failed for service '${serviceId}'. First error: ${errors[0].message}`
280
+ );
281
+ }
282
+ break;
283
+ }
284
+ return successful;
285
+ }
286
+ };
287
+ }
288
+ const ServicesContext = createContext(
289
+ void 0
290
+ );
291
+ const ServiceManagerProvider = ({ config, children }) => {
292
+ const serviceManager = useMemo(() => createServiceManager(config), [config]);
293
+ return /* @__PURE__ */ jsx(ServicesContext.Provider, { value: serviceManager, children });
294
+ };
295
+ const useServicesContext = () => {
296
+ const context = useContext(ServicesContext);
297
+ if (!context) {
298
+ throw new Error(
299
+ "useServicesContext must be used within an ServiceManagerProvider"
300
+ );
301
+ }
302
+ return context;
303
+ };
304
+ function useDeepMemo(value) {
305
+ const ref = useRef();
306
+ if (!isEqual(value, ref.current)) {
307
+ ref.current = cloneDeep(value);
308
+ }
309
+ return ref.current;
310
+ }
311
+ function useService(serviceIdOrRef, options) {
312
+ const { getService } = useServicesContext();
313
+ const opts = useDeepMemo(options || {});
314
+ const promiseFactory = useCallback(
315
+ () => isServiceReference(serviceIdOrRef) ? serviceIdOrRef.getService() : getService(serviceIdOrRef, opts),
316
+ [serviceIdOrRef, getService, opts]
317
+ );
318
+ return useAsync(promiseFactory, { dataProp: "service" });
319
+ }
320
+ function useServices(serviceId, options) {
321
+ const { getServices } = useServicesContext();
322
+ const opts = useDeepMemo(options || {});
323
+ const promiseFactory = useCallback(
324
+ () => getServices(serviceId, opts),
325
+ [getServices, serviceId, opts]
326
+ );
327
+ return useAsync(promiseFactory, { dataProp: "services", pendingData: [] });
328
+ }
329
+ function useServiceReference(serviceId, options = {}) {
330
+ const { getServiceReference } = useServicesContext();
331
+ return getServiceReference(serviceId, options);
332
+ }
333
+ function useServiceReferences(serviceId, options = {}) {
334
+ const { getServiceReferences } = useServicesContext();
335
+ return getServiceReferences(serviceId, options);
336
+ }
337
+ function isServiceReference(value) {
338
+ return !!value && typeof value === "object" && typeof value.getService === "function";
339
+ }
340
+ export {
341
+ ServiceManagerProvider as default,
342
+ useService,
343
+ useServiceReference,
344
+ useServiceReferences,
345
+ useServices,
346
+ useServicesContext
347
+ };
@@ -0,0 +1,46 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { isEqual, cloneDeep } from "lodash";
3
+ import { useAsync } from "./useAsync.js";
4
+ import { useServicesContext } from "./useServicesContext.js";
5
+ function useDeepMemo(value) {
6
+ const ref = useRef();
7
+ if (!isEqual(value, ref.current)) {
8
+ ref.current = cloneDeep(value);
9
+ }
10
+ return ref.current;
11
+ }
12
+ function useService(serviceIdOrRef, options) {
13
+ const { getService } = useServicesContext();
14
+ const opts = useDeepMemo(options || {});
15
+ const promiseFactory = useCallback(
16
+ () => isServiceReference(serviceIdOrRef) ? serviceIdOrRef.getService() : getService(serviceIdOrRef, opts),
17
+ [serviceIdOrRef, getService, opts]
18
+ );
19
+ return useAsync(promiseFactory, { dataProp: "service" });
20
+ }
21
+ function useServices(serviceId, options) {
22
+ const { getServices } = useServicesContext();
23
+ const opts = useDeepMemo(options || {});
24
+ const promiseFactory = useCallback(
25
+ () => getServices(serviceId, opts),
26
+ [getServices, serviceId, opts]
27
+ );
28
+ return useAsync(promiseFactory, { dataProp: "services", pendingData: [] });
29
+ }
30
+ function useServiceReference(serviceId, options = {}) {
31
+ const { getServiceReference } = useServicesContext();
32
+ return getServiceReference(serviceId, options);
33
+ }
34
+ function useServiceReferences(serviceId, options = {}) {
35
+ const { getServiceReferences } = useServicesContext();
36
+ return getServiceReferences(serviceId, options);
37
+ }
38
+ function isServiceReference(value) {
39
+ return !!value && typeof value === "object" && typeof value.getService === "function";
40
+ }
41
+ export {
42
+ useService,
43
+ useServiceReference,
44
+ useServiceReferences,
45
+ useServices
46
+ };
@@ -0,0 +1,51 @@
1
+ import { useState, useEffect } from "react";
2
+ function useAsync(promiseFactory, options) {
3
+ const dataProp = options?.dataProp ?? "data";
4
+ const pendingData = options?.pendingData;
5
+ const [data, setData] = useState(
6
+ pendingData
7
+ );
8
+ const [error, setError] = useState(null);
9
+ const [isPending, setIsPending] = useState(true);
10
+ useEffect(() => {
11
+ let isMounted = true;
12
+ promiseFactory().then((result) => {
13
+ if (isMounted) {
14
+ setData(() => result);
15
+ setError(null);
16
+ setIsPending(false);
17
+ }
18
+ }).catch((err) => {
19
+ if (isMounted) {
20
+ setError(err);
21
+ setData(void 0);
22
+ setIsPending(false);
23
+ }
24
+ });
25
+ return () => {
26
+ isMounted = false;
27
+ };
28
+ }, [promiseFactory]);
29
+ if (error) {
30
+ return {
31
+ isPending: false,
32
+ error,
33
+ [dataProp]: void 0
34
+ };
35
+ }
36
+ if (isPending) {
37
+ return {
38
+ isPending: true,
39
+ error: null,
40
+ [dataProp]: pendingData
41
+ };
42
+ }
43
+ return {
44
+ isPending: false,
45
+ error: null,
46
+ [dataProp]: data
47
+ };
48
+ }
49
+ export {
50
+ useAsync
51
+ };
@@ -0,0 +1,14 @@
1
+ import { useContext } from "react";
2
+ import { ServicesContext } from "../providers/ServiceManagerProvider.js";
3
+ const useServicesContext = () => {
4
+ const context = useContext(ServicesContext);
5
+ if (!context) {
6
+ throw new Error(
7
+ "useServicesContext must be used within an ServiceManagerProvider"
8
+ );
9
+ }
10
+ return context;
11
+ };
12
+ export {
13
+ useServicesContext
14
+ };
@@ -0,0 +1,11 @@
1
+ import { useService, useServiceReference, useServiceReferences, useServices } from "./hooks/Hooks.js";
2
+ import { useServicesContext } from "./hooks/useServicesContext.js";
3
+ import { default as default2 } from "./providers/ServiceManagerProvider.js";
4
+ export {
5
+ default2 as default,
6
+ useService,
7
+ useServiceReference,
8
+ useServiceReferences,
9
+ useServices,
10
+ useServicesContext
11
+ };
@@ -0,0 +1,89 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { createContext, useMemo } from "react";
3
+ import { createServiceReference } from "../utils/serviceReference.js";
4
+ function createServiceReferenceMap(services = {}) {
5
+ return Object.entries(services).reduce(
6
+ (map, [serviceId, serviceConfigs]) => {
7
+ const serviceReferences = serviceConfigs.map(
8
+ (serviceConfig) => createServiceReference(serviceId, serviceConfig)
9
+ ).sort((a, b) => b.ranking - a.ranking);
10
+ map.set(serviceId, serviceReferences);
11
+ return map;
12
+ },
13
+ /* @__PURE__ */ new Map()
14
+ );
15
+ }
16
+ function createServiceManager({
17
+ services
18
+ }) {
19
+ const serviceReferenceMap = createServiceReferenceMap(services);
20
+ function queryServiceReferences(serviceId, _options) {
21
+ return serviceReferenceMap.get(serviceId) ?? [];
22
+ }
23
+ function queryServiceReference(serviceId, options) {
24
+ const references = queryServiceReferences(serviceId);
25
+ if (references.length === 0) {
26
+ throw new Error(`Service ${serviceId} has no service providers.`);
27
+ }
28
+ return references[0];
29
+ }
30
+ return {
31
+ getServiceReference(serviceId, options) {
32
+ return queryServiceReference(serviceId);
33
+ },
34
+ getServiceReferences(serviceId, options) {
35
+ return queryServiceReferences(serviceId);
36
+ },
37
+ async getService(serviceId, options) {
38
+ const reference = queryServiceReference(serviceId);
39
+ return reference.getService();
40
+ },
41
+ async getServices(serviceId, options) {
42
+ const references = queryServiceReferences(serviceId);
43
+ if (references.length === 0) {
44
+ return [];
45
+ }
46
+ const errorHandling = options?.errorHandling ?? "reject-on-all-failures";
47
+ const results = await Promise.allSettled(
48
+ references.map((reference) => reference.getService())
49
+ );
50
+ const successful = [];
51
+ const errors = [];
52
+ results.forEach((result) => {
53
+ if (result.status === "fulfilled") {
54
+ successful.push(result.value);
55
+ } else {
56
+ errors.push(
57
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason))
58
+ );
59
+ }
60
+ });
61
+ switch (errorHandling) {
62
+ case "reject-on-any-failure":
63
+ if (errors.length > 0) {
64
+ throw errors[0];
65
+ }
66
+ break;
67
+ case "reject-on-all-failures":
68
+ if (errors.length === results.length) {
69
+ throw new Error(
70
+ `All service providers failed for service '${serviceId}'. First error: ${errors[0].message}`
71
+ );
72
+ }
73
+ break;
74
+ }
75
+ return successful;
76
+ }
77
+ };
78
+ }
79
+ const ServicesContext = createContext(
80
+ void 0
81
+ );
82
+ const ServiceManagerProvider = ({ config, children }) => {
83
+ const serviceManager = useMemo(() => createServiceManager(config), [config]);
84
+ return /* @__PURE__ */ jsx(ServicesContext.Provider, { value: serviceManager, children });
85
+ };
86
+ export {
87
+ ServicesContext,
88
+ ServiceManagerProvider as default
89
+ };
@@ -0,0 +1,166 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ function createServiceReferenceBase(serviceId, serviceConfig, getService) {
3
+ return {
4
+ serviceId,
5
+ ranking: serviceConfig.ranking ?? 0,
6
+ getService
7
+ };
8
+ }
9
+ function validateImportedModule(imported, serviceId, bundlePath) {
10
+ if (!imported) {
11
+ throw new Error(
12
+ `Bundle import failed for ${bundlePath} and service ${serviceId}`
13
+ );
14
+ }
15
+ if (typeof imported === "object" && "default" in imported && imported.default != null) {
16
+ return imported.default;
17
+ }
18
+ throw new Error(
19
+ `ESM default export missing for ${bundlePath} (service ${serviceId}). Ensure the module uses a default export.`
20
+ );
21
+ }
22
+ function selectValueOrBundle(provider, serviceId, kind) {
23
+ const hasValue = "value" in provider && provider.value !== void 0;
24
+ const hasBundle = "bundle" in provider && typeof provider.bundle === "string";
25
+ if (hasValue) {
26
+ if (hasBundle) {
27
+ console.warn(
28
+ `${kind} service config for ${serviceId} contains both 'value' and 'bundle'. 'value' takes precedence and 'bundle' will be ignored.`
29
+ );
30
+ }
31
+ return { mode: "value", value: provider.value };
32
+ }
33
+ if (hasBundle) {
34
+ return { mode: "bundle", bundle: provider.bundle, config: provider.config };
35
+ }
36
+ throw new Error(`Invalid ${kind} service config for ${serviceId}`);
37
+ }
38
+ function createInstanceReference(serviceId, config) {
39
+ const selection = selectValueOrBundle(
40
+ config.instance,
41
+ serviceId,
42
+ "instance"
43
+ );
44
+ const serviceLoader = async () => {
45
+ if (selection.mode === "value") {
46
+ return selection.value;
47
+ }
48
+ const imported = await import(
49
+ /* @vite-ignore */
50
+ selection.bundle
51
+ );
52
+ return validateImportedModule(
53
+ imported,
54
+ serviceId,
55
+ selection.bundle
56
+ );
57
+ };
58
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
59
+ }
60
+ function createFactoryReference(serviceId, config) {
61
+ let loaded = false;
62
+ let serviceInstance;
63
+ const selection = selectValueOrBundle(
64
+ config.factory,
65
+ serviceId,
66
+ "factory"
67
+ );
68
+ const serviceLoader = async () => {
69
+ if (!loaded) {
70
+ let factoryExport;
71
+ let providedConfig;
72
+ if (selection.mode === "value") {
73
+ factoryExport = selection.value;
74
+ } else {
75
+ const imported = await import(
76
+ /* @vite-ignore */
77
+ selection.bundle
78
+ );
79
+ factoryExport = validateImportedModule(
80
+ imported,
81
+ serviceId,
82
+ selection.bundle
83
+ );
84
+ providedConfig = selection.config;
85
+ }
86
+ if (typeof factoryExport !== "function") {
87
+ throw new Error(
88
+ `Factory service for ${serviceId} did not resolve to a function. Value: ${factoryExport}`
89
+ );
90
+ }
91
+ const factoryFn = factoryExport;
92
+ serviceInstance = factoryFn(providedConfig);
93
+ loaded = true;
94
+ }
95
+ return serviceInstance;
96
+ };
97
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
98
+ }
99
+ function createComponentReference(serviceId, config) {
100
+ let loaded = false;
101
+ let serviceInstance;
102
+ const selection = selectValueOrBundle(
103
+ config.component,
104
+ serviceId,
105
+ "component"
106
+ );
107
+ const serviceLoader = async () => {
108
+ if (!loaded) {
109
+ let componentExport;
110
+ let providedConfig;
111
+ if (selection.mode === "value") {
112
+ componentExport = selection.value;
113
+ } else {
114
+ const imported = await import(
115
+ /* @vite-ignore */
116
+ selection.bundle
117
+ );
118
+ componentExport = validateImportedModule(
119
+ imported,
120
+ serviceId,
121
+ selection.bundle
122
+ );
123
+ providedConfig = selection.config;
124
+ }
125
+ if (typeof componentExport !== "function") {
126
+ throw new Error(
127
+ `Component definition for service ${serviceId} is not a React component function`
128
+ );
129
+ }
130
+ serviceInstance = bindComponent(
131
+ componentExport,
132
+ providedConfig ?? {}
133
+ );
134
+ loaded = true;
135
+ }
136
+ return serviceInstance;
137
+ };
138
+ return createServiceReferenceBase(serviceId, config, serviceLoader);
139
+ }
140
+ function createServiceReference(serviceId, serviceConfig) {
141
+ if ("instance" in serviceConfig) {
142
+ return createInstanceReference(serviceId, serviceConfig);
143
+ }
144
+ if ("factory" in serviceConfig) {
145
+ return createFactoryReference(serviceId, serviceConfig);
146
+ }
147
+ if ("component" in serviceConfig) {
148
+ return createComponentReference(
149
+ serviceId,
150
+ serviceConfig
151
+ );
152
+ }
153
+ throw new Error(
154
+ `Unsupported service provider configuration for service ${serviceId}`
155
+ );
156
+ }
157
+ function bindComponent(Component, configProps) {
158
+ const BoundComponent = (props) => {
159
+ const mergedProps = { ...configProps, ...props };
160
+ return /* @__PURE__ */ jsx(Component, { ...mergedProps });
161
+ };
162
+ return BoundComponent;
163
+ }
164
+ export {
165
+ createServiceReference
166
+ };
@@ -0,0 +1,249 @@
1
+ import { FC } from 'react';
2
+ import { PropsWithChildren } from 'react';
3
+ import { ServiceManager as ServiceManager_2 } from '..';
4
+
5
+ /** Bundle for lazy loading */
6
+ export declare type Bundle = {
7
+ bundle: string;
8
+ };
9
+
10
+ export declare type BundleConfig = Record<string, unknown>;
11
+
12
+ export declare type ComponentServiceConfig = ServiceConfigBase & {
13
+ component: ComponentValueOrBundle;
14
+ };
15
+
16
+ export declare type ComponentValueOrBundle<TBundleConfig = BundleConfig> = Value | (Bundle & {
17
+ config?: TBundleConfig;
18
+ });
19
+
20
+ export declare type ErrorBase = NonNullable<unknown>;
21
+
22
+ export declare type FactoryExport<TService = unknown, TConfig = BundleConfig> = FactoryServiceFunction<TService, TConfig>;
23
+
24
+ export declare type FactoryServiceConfig = ServiceConfigBase & {
25
+ factory: FactoryValueOrBundle;
26
+ };
27
+
28
+ export declare type FactoryServiceFunction<TService = unknown, TConfig = BundleConfig> = (config?: TConfig) => TService;
29
+
30
+ export declare type FactoryValueOrBundle<TBundleConfig = BundleConfig> = {
31
+ value: FactoryServiceFunction;
32
+ } | (Bundle & {
33
+ config?: TBundleConfig;
34
+ });
35
+
36
+ /**
37
+ * Base options for service operations.
38
+ * Used as a foundation for more specific service option types.
39
+ */
40
+ export declare type GetServiceBaseOptions = {};
41
+
42
+ /**
43
+ * Options when getting a single service.
44
+ */
45
+ export declare type GetServiceOptions = GetServiceBaseOptions;
46
+
47
+ /**
48
+ * Options when getting a service reference.
49
+ */
50
+ export declare type GetServiceReferenceOptions = GetServiceBaseOptions;
51
+
52
+ /**
53
+ * Options when getting multiple service references.
54
+ */
55
+ export declare type GetServiceReferencesOptions = GetServiceBaseOptions;
56
+
57
+ /**
58
+ * Options when getting multiple services, with error handling configuration.
59
+ */
60
+ export declare type GetServicesOptions = GetServiceBaseOptions & {
61
+ errorHandling?: ServicesErrorHandling;
62
+ };
63
+
64
+ export declare type InstanceServiceConfig = ServiceConfigBase & {
65
+ instance: InstanceValueOrBundle;
66
+ };
67
+
68
+ export declare type InstanceValueOrBundle = Value | Bundle;
69
+
70
+ export declare type PropertyWithName<K extends string, T> = {
71
+ [P in K]: T;
72
+ };
73
+
74
+ declare interface Props extends PropsWithChildren {
75
+ config: ServiceManagerConfig;
76
+ }
77
+
78
+ /**
79
+ * A service configuration can be one of several kinds (instance, factory, component),
80
+ * each of which supports two declaration modes:
81
+ * - Value: directly provide the instance, factory or component
82
+ * - Bundle: module to be lazy loaded with optional config object
83
+ */
84
+ export declare type ServiceConfig = InstanceServiceConfig | FactoryServiceConfig | ComponentServiceConfig;
85
+
86
+ export declare type ServiceConfigBase = {
87
+ ranking?: number;
88
+ };
89
+
90
+ export declare type ServiceId = string;
91
+
92
+ /**
93
+ * Loader function type for asynchronously loading of a service that returns a promise resolving to the service instance.
94
+ */
95
+ export declare type ServiceLoader<TService> = () => Promise<TService>;
96
+
97
+ export declare type ServiceManager = {
98
+ /**
99
+ * Accepts a service ID and optional options. Returns a promise resolving to the highest-ranked service instance, or rejects if resolution fails.
100
+ */
101
+ getService<TService>(serviceId: ServiceId, options?: GetServiceOptions): Promise<TService>;
102
+ /**
103
+ * Accepts a service ID and optional options. Returns a promise resolving to all successfully resolved service instances, following the error handling strategy.
104
+ */
105
+ getServices<TService>(serviceId: ServiceId, options?: GetServicesOptions): Promise<TService[]>;
106
+ /**
107
+ * Accepts a service ID and optional options. Returns a reference to the highest-ranked service.
108
+ */
109
+ getServiceReference<TService>(serviceId: ServiceId, options?: GetServiceReferenceOptions): ServiceReference<TService>;
110
+ /**
111
+ * Accepts a service ID and optional options. Returns all service references for the ID, sorted by descending ranking.
112
+ */
113
+ getServiceReferences<TService>(serviceId: ServiceId, options?: GetServiceReferencesOptions): ServiceReference<TService>[];
114
+ };
115
+
116
+ declare type ServiceManagerConfig = {
117
+ services?: ServicesConfig;
118
+ };
119
+
120
+ declare const ServiceManagerProvider: FC<Props>;
121
+ export default ServiceManagerProvider;
122
+
123
+ /**
124
+ * Reference to a service, including metadata and a loader function.
125
+ * Can be used to access service details and also resolve its service instance.
126
+ */
127
+ export declare type ServiceReference<TService = unknown> = {
128
+ serviceId: ServiceId;
129
+ ranking: number;
130
+ getService(): Promise<TService>;
131
+ };
132
+
133
+ export declare type ServicesConfig = Record<ServiceId, ServiceConfig[]>;
134
+
135
+ /**
136
+ * Error handling strategies that determine how errors are managed when loading services:
137
+ * - "reject-on-any-failure": Rejects if any service resolution fails (use for critical services);
138
+ * - "reject-on-all-failures" (default): Only rejects if all service resolutions fail (optimistic approach);
139
+ * - "ignore-failures": Never rejects, returns only successfully resolved services.
140
+ */
141
+ export declare type ServicesErrorHandling = "reject-on-any-failure" | "reject-on-all-failures" | "ignore-failures";
142
+
143
+ export declare type UseAsyncBaseResult<TData, TError extends ErrorBase = Error, TDataProp extends string = "data", TDataPending extends TData | undefined = undefined> = {
144
+ isPending: boolean;
145
+ error: TError | null;
146
+ } & PropertyWithName<TDataProp, TData | TDataPending | undefined>;
147
+
148
+ export declare type UseAsyncErrorResult<TData, TError extends ErrorBase = Error, TDataProp extends string = "data", TDataPending extends TData | undefined = undefined> = UseAsyncBaseResult<TData, TError, TDataProp, TDataPending> & {
149
+ isPending: false;
150
+ error: TError;
151
+ } & PropertyWithName<TDataProp, undefined>;
152
+
153
+ export declare type UseAsyncPendingResult<TData, TError extends ErrorBase = Error, TDataProp extends string = "data", TDataPending extends TData | undefined = undefined> = UseAsyncBaseResult<TData, TError, TDataProp> & {
154
+ isPending: true;
155
+ error: null;
156
+ } & PropertyWithName<TDataProp, TDataPending>;
157
+
158
+ export declare type UseAsyncResult<TData, TError extends ErrorBase = Error, TDataProp extends string = "data", TDataPending extends TData | undefined = undefined> = UseAsyncPendingResult<TData, TError, TDataProp, TDataPending> | UseAsyncErrorResult<TData, TError, TDataProp, TDataPending> | UseAsyncSuccessResult<TData, TError, TDataProp, TDataPending>;
159
+
160
+ export declare type UseAsyncSuccessResult<TData, TError extends ErrorBase = Error, TDataProp extends string = "data", TDataPending extends TData | undefined = undefined> = UseAsyncBaseResult<TData, TError, TDataProp, TDataPending> & {
161
+ isPending: false;
162
+ error: null;
163
+ } & PropertyWithName<TDataProp, TData>;
164
+
165
+ /**
166
+ * Resolves and returns the service instance for a given service id or reference.
167
+ *
168
+ * Overloads:
169
+ * - useService<TService>(serviceReference: ServiceReference<TService>): UseServiceResult<TService>
170
+ * - useService<TService>(serviceId: ServiceId, options?: UseServiceOptions): UseServiceResult<TService>
171
+ *
172
+ * @param serviceReference - A {@link ServiceReference} object to resolve to a service instance.
173
+ * @param serviceId - The identifier of the service to resolve.
174
+ * @param options - Options to control service resolution.
175
+ * @returns A {@link UseServiceResult} object representing the loading state, error, and resolved service instance.
176
+ *
177
+ * When called with a {@link ServiceReference}, resolves that reference using its getService().
178
+ * When called with a {@link ServiceId}, resolves and returns the highest-ranked service instance for that id.
179
+ * If resolution fails, the error is available in the result.
180
+ */
181
+ export declare function useService<TService>(serviceReference: ServiceReference<TService>): UseServiceResult<TService>;
182
+
183
+ export declare function useService<TService>(serviceId: ServiceId, options?: UseServiceOptions): UseServiceResult<TService>;
184
+
185
+ /**
186
+ * Options to control service resolution
187
+ */
188
+ export declare type UseServiceOptions = GetServiceOptions;
189
+
190
+ /**
191
+ * Gets the highest-ranked service reference for the given id.
192
+ *
193
+ * @param serviceId - The identifier of the service to get a reference for.
194
+ * @param options - Options to control the service reference to return.
195
+ * @returns A {@link ServiceReference} object.
196
+ */
197
+ export declare function useServiceReference<TService>(serviceId: ServiceId, options?: UseServiceReferenceOptions): ServiceReference<TService>;
198
+
199
+ /**
200
+ * Options to control the service reference to return.
201
+ */
202
+ export declare type UseServiceReferenceOptions = GetServiceReferenceOptions;
203
+
204
+ /**
205
+ * Gets all service references for the given service id, sorted by descending ranking.
206
+ *
207
+ * @param serviceId - The identifier of the service to get references for.
208
+ * @param options - Options to control the service references to return.
209
+ * @returns An array of {@link ServiceReference} objects.
210
+ */
211
+ export declare function useServiceReferences<TService>(serviceId: ServiceId, options?: UseServiceReferencesOptions): ServiceReference<TService>[];
212
+
213
+ /**
214
+ * Options to control the service references to return.
215
+ */
216
+ export declare type UseServiceReferencesOptions = GetServiceReferencesOptions;
217
+
218
+ /**
219
+ * Result type for the {@link useService} hook that represents the async state of a single service fetch operation.
220
+ */
221
+ export declare type UseServiceResult<TService> = UseAsyncResult<TService, Error, "service">;
222
+
223
+ /**
224
+ * Gets all service instances for a given service id.
225
+ *
226
+ * @param serviceId - The identifier of the services to resolve.
227
+ * @param options - Options to control services resolution, like error handling strategy.
228
+ * @returns A {@link UseServicesResult} object representing the loading state, error, and resolved array of service instances.
229
+ */
230
+ export declare function useServices<TService>(serviceId: ServiceId, options?: UseServicesOptions): UseServicesResult<TService>;
231
+
232
+ export declare const useServicesContext: () => ServiceManager_2;
233
+
234
+ /**
235
+ * Options to control services resolution, like error handling strategy.
236
+ */
237
+ export declare type UseServicesOptions = GetServicesOptions;
238
+
239
+ /**
240
+ * Result type for the {@link useServices} hook that represents the async state of fetching multiple services.
241
+ */
242
+ export declare type UseServicesResult<TService> = UseAsyncResult<TService[], Error, "services", TService[]>;
243
+
244
+ /** Directly provided value */
245
+ export declare type Value = {
246
+ value: unknown;
247
+ };
248
+
249
+ export { }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@hitachivantara/app-shell-services",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "author": "Hitachi Vantara UI Kit Team",
7
+ "description": "AppShell Services",
8
+ "homepage": "https://github.com/lumada-design/hv-uikit-react",
9
+ "sideEffects": false,
10
+ "license": "Apache-2.0",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/lumada-design/hv-uikit-react.git",
14
+ "directory": "packages/app-shell-services"
15
+ },
16
+ "bugs": "https://github.com/lumada-design/hv-uikit-react/issues",
17
+ "dependencies": {
18
+ "lodash": "^4.17.21"
19
+ },
20
+ "peerDependencies": {
21
+ "react": "^18.2.0"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/types/index.d.ts",
29
+ "import": "./dist/esm/index.js",
30
+ "default": "./dist/esm/index.js"
31
+ },
32
+ "./package.json": "./package.json",
33
+ "./bundles/*": "./dist/bundles/*"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "directory": "package"
38
+ },
39
+ "gitHead": "18bba1bbe7a6ecddfcb9f56d1613c547bfd0e6d7",
40
+ "types": "./dist/types/index.d.ts",
41
+ "module": "dist/esm/index.js"
42
+ }