ptech-shell-dev 1.6.7 → 1.8.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/dist/index.d.ts +110 -3
- package/dist/index.js +212 -76
- package/package.json +45 -45
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LogLevel, RealtimeEnvelope, UserService, RequestContextService, RealtimeService, Lang, User, I18nService, ApiClient, NavigationService, ConfigService, PermissionService, SharedStateService, ObservabilityService, NotificationService, AnalyticsService, ObservabilityConfigPatch, ApiErrorPayload, ApiValidationErrors, ApiRetryOptions, MsalAccountInfoLike, NavigationAction, NavigationTo, NavigationNavigateOptions } from 'ptech-shell-sdk';
|
|
1
|
+
import { LogLevel, RealtimeEnvelope, UserService, RequestContextService, RealtimeService, Lang, User, I18nService, ApiClient, NavigationService, ConfigService, PermissionService, SharedStateService, ObservabilityService, NotificationService, AnalyticsService, TenantService, AppSettingsService, ObservabilityConfigPatch, AppSettings, ApiErrorPayload, ApiValidationErrors, ApiRetryOptions, MsalAccountInfoLike, NavigationAction, NavigationTo, NavigationNavigateOptions } from 'ptech-shell-sdk';
|
|
2
2
|
|
|
3
3
|
type ObservabilityTelemetryEvent = {
|
|
4
4
|
level: LogLevel;
|
|
@@ -74,20 +74,24 @@ type StandaloneServices = {
|
|
|
74
74
|
observability: ObservabilityService;
|
|
75
75
|
notification: NotificationService;
|
|
76
76
|
analytics: AnalyticsService;
|
|
77
|
+
tenantService: TenantService;
|
|
78
|
+
appSettingsService: AppSettingsService;
|
|
77
79
|
};
|
|
78
80
|
type ServiceRegisterMode = 'if-missing' | 'always';
|
|
79
81
|
type StandaloneInitOptions = {
|
|
80
82
|
/**
|
|
81
83
|
* API base URL when running standalone.
|
|
82
|
-
*
|
|
84
|
+
* @default 'http://localhost:4000'
|
|
83
85
|
*/
|
|
84
86
|
apiBase?: string;
|
|
85
87
|
/**
|
|
86
88
|
* Default language for standalone.
|
|
89
|
+
* @default 'vi'
|
|
87
90
|
*/
|
|
88
91
|
lang?: Lang;
|
|
89
92
|
/**
|
|
90
93
|
* Seed user for standalone. (Optional)
|
|
94
|
+
* @default { id: 'dev', name: 'Dev User' }
|
|
91
95
|
*/
|
|
92
96
|
user?: User | null;
|
|
93
97
|
/**
|
|
@@ -116,8 +120,47 @@ type StandaloneInitOptions = {
|
|
|
116
120
|
* - always: replace any existing service for the token.
|
|
117
121
|
*/
|
|
118
122
|
registerMode?: ServiceRegisterMode;
|
|
123
|
+
/**
|
|
124
|
+
* Default path for standalone navigation service.
|
|
125
|
+
* @default '/standalone'
|
|
126
|
+
*/
|
|
127
|
+
navigationPath?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Feature flags seed for standalone config service.
|
|
130
|
+
* Merged with built-in defaults.
|
|
131
|
+
*/
|
|
132
|
+
featureFlags?: Record<string, boolean>;
|
|
133
|
+
/**
|
|
134
|
+
* Runtime config overrides for standalone config service.
|
|
135
|
+
*/
|
|
136
|
+
runtimeConfig?: {
|
|
137
|
+
envName?: string;
|
|
138
|
+
apiBase?: string;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Seed values for the standalone tenant service.
|
|
142
|
+
* Defaults to `{ tenantId: 'dev-tenant', tenantSlug: 'dev' }`.
|
|
143
|
+
*/
|
|
144
|
+
tenant?: {
|
|
145
|
+
tenantId?: string | null;
|
|
146
|
+
tenantSlug?: string | null;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Seed/loader for the standalone app settings service.
|
|
150
|
+
* - `seed`: static per-appKey map returned by `load()`.
|
|
151
|
+
* - `fetcher`: override that takes precedence over seed.
|
|
152
|
+
*/
|
|
153
|
+
appSettings?: {
|
|
154
|
+
seed?: Readonly<Record<string, AppSettings>>;
|
|
155
|
+
fetcher?: (appKey: string) => Promise<AppSettings>;
|
|
156
|
+
};
|
|
119
157
|
};
|
|
120
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Creates all standalone service instances without registering them.
|
|
161
|
+
* Useful for testing or when you need direct access to services.
|
|
162
|
+
*/
|
|
163
|
+
declare function createTestServices(options?: StandaloneInitOptions): StandaloneServices;
|
|
121
164
|
/**
|
|
122
165
|
* Initialize "safe defaults" so a remote app can run standalone without the host.
|
|
123
166
|
*
|
|
@@ -125,6 +168,21 @@ type StandaloneInitOptions = {
|
|
|
125
168
|
* But if host doesn't (or remote is standalone), this provides mocks.
|
|
126
169
|
*/
|
|
127
170
|
declare function initStandaloneServices(options?: StandaloneInitOptions): void;
|
|
171
|
+
/**
|
|
172
|
+
* Create a preset factory with default options baked in.
|
|
173
|
+
* Useful for projects that want consistent defaults across remotes.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // In a shared config file:
|
|
177
|
+
* export const initHrmServices = createDevPreset({
|
|
178
|
+
* apiBase: 'http://localhost:5000',
|
|
179
|
+
* lang: 'en',
|
|
180
|
+
* });
|
|
181
|
+
*
|
|
182
|
+
* // In each remote's bootstrap:
|
|
183
|
+
* initHrmServices({ user: { id: 'admin', name: 'Admin' } });
|
|
184
|
+
*/
|
|
185
|
+
declare function createDevPreset(defaults: StandaloneInitOptions): (overrides?: StandaloneInitOptions) => void;
|
|
128
186
|
|
|
129
187
|
type CreateStandaloneApiClientOptions = {
|
|
130
188
|
apiBase: string;
|
|
@@ -143,6 +201,12 @@ type CreateStandaloneApiClientOptions = {
|
|
|
143
201
|
*/
|
|
144
202
|
getRequestContext?: () => RequestContextService | undefined;
|
|
145
203
|
requestContext?: RequestContextService;
|
|
204
|
+
/**
|
|
205
|
+
* Optional dynamic lookup for tenant service; header `X-Tenant-Id` is injected
|
|
206
|
+
* when the resolved snapshot has a non-empty `tenantId`.
|
|
207
|
+
*/
|
|
208
|
+
getTenantService?: () => TenantService | undefined;
|
|
209
|
+
tenantService?: TenantService;
|
|
146
210
|
defaultAuthScopes?: string[];
|
|
147
211
|
defaultTimeoutMs?: number;
|
|
148
212
|
defaultRetries?: ApiRetryOptions | false;
|
|
@@ -194,6 +258,32 @@ declare function isApiError(value: unknown): value is ApiError;
|
|
|
194
258
|
*/
|
|
195
259
|
declare function createStandaloneApiClient(options: string | CreateStandaloneApiClientOptions): ApiClient;
|
|
196
260
|
|
|
261
|
+
type CreateStandaloneAppSettingsServiceOptions = {
|
|
262
|
+
/**
|
|
263
|
+
* Static seed settings keyed by appKey.
|
|
264
|
+
* Returned instantly from `load()` and `get()` for dev/test scenarios.
|
|
265
|
+
*/
|
|
266
|
+
seed?: Readonly<Record<string, AppSettings>>;
|
|
267
|
+
/**
|
|
268
|
+
* Optional async loader, overrides seed when provided.
|
|
269
|
+
* Useful when tests want to simulate latency or network failures.
|
|
270
|
+
*/
|
|
271
|
+
fetcher?: (appKey: string) => Promise<AppSettings>;
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* WHY:
|
|
275
|
+
* - In-memory per-appKey settings store for standalone remote development.
|
|
276
|
+
* WHEN TO USE:
|
|
277
|
+
* - Use via initStandaloneServices so remotes can call `load(appKey)` without a host.
|
|
278
|
+
* WHEN NOT TO USE:
|
|
279
|
+
* - Do not use in production — host fetches from backend and owns cache invalidation.
|
|
280
|
+
* INVARIANTS:
|
|
281
|
+
* - `get()` returns null until `load()` resolves for that appKey.
|
|
282
|
+
* - `invalidate()` with no args drops all cached entries.
|
|
283
|
+
* - Listeners are scoped per appKey; invalidate emits for affected keys only.
|
|
284
|
+
*/
|
|
285
|
+
declare function createStandaloneAppSettingsService(options?: CreateStandaloneAppSettingsServiceOptions): AppSettingsService;
|
|
286
|
+
|
|
197
287
|
type UiApiError = {
|
|
198
288
|
i18nKey: string;
|
|
199
289
|
params: Record<string, string | number>;
|
|
@@ -362,6 +452,23 @@ type CreateRequestContextServiceOptions = {
|
|
|
362
452
|
*/
|
|
363
453
|
declare function createRequestContextService(options?: CreateRequestContextServiceOptions): RequestContextService;
|
|
364
454
|
|
|
455
|
+
type CreateStandaloneTenantServiceOptions = {
|
|
456
|
+
tenantId?: string | null;
|
|
457
|
+
tenantSlug?: string | null;
|
|
458
|
+
};
|
|
459
|
+
/**
|
|
460
|
+
* WHY:
|
|
461
|
+
* - Provides a minimal in-memory TenantService for standalone remote dev.
|
|
462
|
+
* WHEN TO USE:
|
|
463
|
+
* - Use via initStandaloneServices when a remote runs without host.
|
|
464
|
+
* WHEN NOT TO USE:
|
|
465
|
+
* - Do not use in production — host registers its own tenant service.
|
|
466
|
+
* INVARIANTS:
|
|
467
|
+
* - getSnapshot returns a copy, never the mutable internal reference.
|
|
468
|
+
* - setTenant emits only when either field actually changes.
|
|
469
|
+
*/
|
|
470
|
+
declare function createStandaloneTenantService(options?: CreateStandaloneTenantServiceOptions): TenantService;
|
|
471
|
+
|
|
365
472
|
/**
|
|
366
473
|
* WHY:
|
|
367
474
|
* - Provides mock user/session/auth token behavior for standalone shell development.
|
|
@@ -376,4 +483,4 @@ declare function createRequestContextService(options?: CreateRequestContextServi
|
|
|
376
483
|
*/
|
|
377
484
|
declare function createStandaloneUserService(seedUser: User | null): UserService;
|
|
378
485
|
|
|
379
|
-
export { ApiError, type CreateMsalUserServiceOptions, type CreateReactRouterNavigationAdapterOptions, type CreateStandaloneApiClientOptions, type CreateStandaloneObservabilityServiceOptions, type CreateStandaloneRealtimeServiceOptions, type MsalBrowserInstanceLike, type ObservabilityTelemetryEvent, type ObservabilityTelemetrySink, type ReactRouterCreateHrefFunctionLike, type ReactRouterLocationLike, type ReactRouterNavigateFunctionLike, type ReactRouterNavigationAdapter, type ReactRouterNavigationCapabilities, type RealtimeReconnectOptions, type RealtimeTransport, type RealtimeTransportConnectContext, type ServiceRegisterMode, type StandaloneInitOptions, type StandaloneServices, type UiApiError, createMsalUserService, createReactRouterNavigationAdapter, createRequestContextService, createStandaloneApiClient, createStandaloneI18nService, createStandaloneRealtimeService, createStandaloneUserService, initStandaloneServices, isApiError, mapApiErrorToUiError };
|
|
486
|
+
export { ApiError, type CreateMsalUserServiceOptions, type CreateReactRouterNavigationAdapterOptions, type CreateStandaloneApiClientOptions, type CreateStandaloneObservabilityServiceOptions, type CreateStandaloneRealtimeServiceOptions, type MsalBrowserInstanceLike, type ObservabilityTelemetryEvent, type ObservabilityTelemetrySink, type ReactRouterCreateHrefFunctionLike, type ReactRouterLocationLike, type ReactRouterNavigateFunctionLike, type ReactRouterNavigationAdapter, type ReactRouterNavigationCapabilities, type RealtimeReconnectOptions, type RealtimeTransport, type RealtimeTransportConnectContext, type ServiceRegisterMode, type StandaloneInitOptions, type StandaloneServices, type UiApiError, createDevPreset, createMsalUserService, createReactRouterNavigationAdapter, createRequestContextService, createStandaloneApiClient, createStandaloneAppSettingsService, createStandaloneI18nService, createStandaloneRealtimeService, createStandaloneTenantService, createStandaloneUserService, createTestServices, initStandaloneServices, isApiError, mapApiErrorToUiError };
|
package/dist/index.js
CHANGED
|
@@ -66,6 +66,7 @@ var DEFAULT_RETRY_OPTIONS = {
|
|
|
66
66
|
var TRACE_HEADER = "traceparent";
|
|
67
67
|
var CORRELATION_HEADER = "x-correlation-id";
|
|
68
68
|
var CORRELATION_HEADERS = ["x-correlation-id", "x-request-id"];
|
|
69
|
+
var TENANT_HEADER = "x-tenant-id";
|
|
69
70
|
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
70
71
|
function isApiError(value) {
|
|
71
72
|
return value instanceof ApiError;
|
|
@@ -354,6 +355,15 @@ function normalizeRetryOptions(input, defaultRetryOptions) {
|
|
|
354
355
|
function shouldRetryResponse(response, retry) {
|
|
355
356
|
return retry.retryOnStatuses.has(response.status);
|
|
356
357
|
}
|
|
358
|
+
async function disposeRetryResponse(response) {
|
|
359
|
+
if (!response.body || typeof response.body.cancel !== "function") {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await response.body.cancel();
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
357
367
|
function shouldRetryError(error) {
|
|
358
368
|
return error.isNetworkError && error.code !== "aborted";
|
|
359
369
|
}
|
|
@@ -409,6 +419,7 @@ function resolveCreateOptions(input) {
|
|
|
409
419
|
getUserService: input.getUserService ?? (input.userService ? () => input.userService : void 0),
|
|
410
420
|
getNotificationService: input.getNotificationService ?? (input.notificationService ? () => input.notificationService : void 0),
|
|
411
421
|
getRequestContext: input.getRequestContext ?? (input.requestContext ? () => input.requestContext : void 0),
|
|
422
|
+
getTenantService: input.getTenantService ?? (input.tenantService ? () => input.tenantService : void 0),
|
|
412
423
|
defaultAuthScopes: input.defaultAuthScopes ?? [],
|
|
413
424
|
defaultTimeoutMs: input.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
414
425
|
defaultRetries: input.defaultRetries ?? DEFAULT_RETRY_OPTIONS
|
|
@@ -430,6 +441,19 @@ function applyRequestContextHeaders(headers, options) {
|
|
|
430
441
|
headers.set(CORRELATION_HEADER, context.correlationId);
|
|
431
442
|
}
|
|
432
443
|
}
|
|
444
|
+
function applyTenantHeader(headers, options) {
|
|
445
|
+
if (headers.has(TENANT_HEADER)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const tenantService = options.getTenantService?.();
|
|
449
|
+
if (!tenantService) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const tenantId = tenantService.getSnapshot().tenantId;
|
|
453
|
+
if (tenantId && tenantId.length > 0) {
|
|
454
|
+
headers.set(TENANT_HEADER, tenantId);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
433
457
|
function syncResponseContext(response, options) {
|
|
434
458
|
const contextService = options.getRequestContext?.();
|
|
435
459
|
if (!contextService) {
|
|
@@ -522,6 +546,7 @@ async function executeRaw(requestOptions, options) {
|
|
|
522
546
|
const timeoutMs = requestOptions.timeoutMs ?? options.defaultTimeoutMs;
|
|
523
547
|
const headers = new Headers(requestOptions.headers);
|
|
524
548
|
applyRequestContextHeaders(headers, options);
|
|
549
|
+
applyTenantHeader(headers, options);
|
|
525
550
|
await applyAuthorization(headers, requestOptions, options);
|
|
526
551
|
const url = resolveUrl(options.apiBase, requestOptions.url);
|
|
527
552
|
const body = resolveBody(requestOptions.body, headers);
|
|
@@ -541,6 +566,7 @@ async function executeRaw(requestOptions, options) {
|
|
|
541
566
|
);
|
|
542
567
|
syncResponseContext(response, options);
|
|
543
568
|
if (retry && shouldUseRetry && attempt < maxAttempts && shouldRetryResponse(response, retry)) {
|
|
569
|
+
await disposeRetryResponse(response);
|
|
544
570
|
const delayMs = computeRetryDelayMs(attempt, retry);
|
|
545
571
|
await sleep(delayMs, requestOptions.signal);
|
|
546
572
|
continue;
|
|
@@ -601,6 +627,7 @@ function createStandaloneApiClient(options) {
|
|
|
601
627
|
fetch: async (input, init) => {
|
|
602
628
|
const headers = new Headers(init?.headers);
|
|
603
629
|
applyRequestContextHeaders(headers, resolvedOptions);
|
|
630
|
+
applyTenantHeader(headers, resolvedOptions);
|
|
604
631
|
await applyAuthorization(
|
|
605
632
|
headers,
|
|
606
633
|
{
|
|
@@ -635,6 +662,69 @@ function createStandaloneApiClient(options) {
|
|
|
635
662
|
};
|
|
636
663
|
}
|
|
637
664
|
|
|
665
|
+
// src/services/appSettings.ts
|
|
666
|
+
function createStandaloneAppSettingsService(options = {}) {
|
|
667
|
+
const cache = /* @__PURE__ */ new Map();
|
|
668
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
669
|
+
function emit(appKey) {
|
|
670
|
+
const group = listeners.get(appKey);
|
|
671
|
+
if (!group) return;
|
|
672
|
+
for (const listener of group) {
|
|
673
|
+
listener();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function resolveSettings(appKey) {
|
|
677
|
+
if (options.fetcher) {
|
|
678
|
+
return options.fetcher(appKey);
|
|
679
|
+
}
|
|
680
|
+
return options.seed?.[appKey] ?? {};
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
get: (appKey) => cache.get(appKey) ?? null,
|
|
684
|
+
load: async (appKey) => {
|
|
685
|
+
const existing = cache.get(appKey);
|
|
686
|
+
if (existing) {
|
|
687
|
+
return existing;
|
|
688
|
+
}
|
|
689
|
+
const settings = await resolveSettings(appKey);
|
|
690
|
+
const snapshot = {
|
|
691
|
+
settings,
|
|
692
|
+
version: 1
|
|
693
|
+
};
|
|
694
|
+
cache.set(appKey, snapshot);
|
|
695
|
+
emit(appKey);
|
|
696
|
+
return snapshot;
|
|
697
|
+
},
|
|
698
|
+
subscribe: (appKey, listener) => {
|
|
699
|
+
let group = listeners.get(appKey);
|
|
700
|
+
if (!group) {
|
|
701
|
+
group = /* @__PURE__ */ new Set();
|
|
702
|
+
listeners.set(appKey, group);
|
|
703
|
+
}
|
|
704
|
+
group.add(listener);
|
|
705
|
+
return () => {
|
|
706
|
+
group?.delete(listener);
|
|
707
|
+
if (group && group.size === 0) {
|
|
708
|
+
listeners.delete(appKey);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
invalidate: (appKey) => {
|
|
713
|
+
if (appKey === void 0) {
|
|
714
|
+
const affected = Array.from(cache.keys());
|
|
715
|
+
cache.clear();
|
|
716
|
+
for (const key of affected) {
|
|
717
|
+
emit(key);
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (cache.delete(appKey)) {
|
|
722
|
+
emit(appKey);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
638
728
|
// src/services/config.ts
|
|
639
729
|
import { FEATURE_FLAGS } from "ptech-shell-sdk";
|
|
640
730
|
var DEFAULT_FLAGS = {
|
|
@@ -1079,14 +1169,13 @@ import {
|
|
|
1079
1169
|
} from "ptech-shell-sdk";
|
|
1080
1170
|
var DEFAULT_PERMISSIONS = listPermissionsByRole(USER_ROLES.authenticated);
|
|
1081
1171
|
function createStandalonePermissionService(initialPermissions = DEFAULT_PERMISSIONS) {
|
|
1082
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
1083
1172
|
const permissions = new Set(initialPermissions);
|
|
1084
1173
|
return {
|
|
1085
1174
|
can: (permission) => permissions.has(permission),
|
|
1086
1175
|
list: () => Array.from(permissions.values()),
|
|
1087
|
-
subscribe: (
|
|
1088
|
-
|
|
1089
|
-
|
|
1176
|
+
subscribe: () => {
|
|
1177
|
+
return () => {
|
|
1178
|
+
};
|
|
1090
1179
|
}
|
|
1091
1180
|
};
|
|
1092
1181
|
}
|
|
@@ -1121,8 +1210,8 @@ function createNoopTransport() {
|
|
|
1121
1210
|
let disconnectListener;
|
|
1122
1211
|
return {
|
|
1123
1212
|
start: async () => {
|
|
1124
|
-
|
|
1125
|
-
|
|
1213
|
+
void messageListener;
|
|
1214
|
+
void disconnectListener;
|
|
1126
1215
|
},
|
|
1127
1216
|
stop: async () => void 0,
|
|
1128
1217
|
onMessage: (listener) => {
|
|
@@ -1169,6 +1258,7 @@ function createStandaloneRealtimeService(options = {}) {
|
|
|
1169
1258
|
let unbindMessage;
|
|
1170
1259
|
let unbindDisconnect;
|
|
1171
1260
|
let inFlightStart = null;
|
|
1261
|
+
let lifecycleVersion = 0;
|
|
1172
1262
|
function emitStateChanged() {
|
|
1173
1263
|
for (const listener of listeners) {
|
|
1174
1264
|
listener();
|
|
@@ -1259,6 +1349,7 @@ function createStandaloneRealtimeService(options = {}) {
|
|
|
1259
1349
|
return inFlightStart;
|
|
1260
1350
|
}
|
|
1261
1351
|
inFlightStart = (async () => {
|
|
1352
|
+
const startVersion = lifecycleVersion;
|
|
1262
1353
|
stopRequested = false;
|
|
1263
1354
|
clearReconnectTimer();
|
|
1264
1355
|
setState(activeAttempt > 0 ? "reconnecting" : "connecting");
|
|
@@ -1271,6 +1362,14 @@ function createStandaloneRealtimeService(options = {}) {
|
|
|
1271
1362
|
traceId: context.traceId,
|
|
1272
1363
|
correlationId: context.correlationId
|
|
1273
1364
|
});
|
|
1365
|
+
if (stopRequested || startVersion !== lifecycleVersion) {
|
|
1366
|
+
try {
|
|
1367
|
+
await nextTransport.stop();
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
setState("disconnected");
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1274
1373
|
transport = nextTransport;
|
|
1275
1374
|
await bindTransportListeners(nextTransport);
|
|
1276
1375
|
activeAttempt = 0;
|
|
@@ -1278,6 +1377,10 @@ function createStandaloneRealtimeService(options = {}) {
|
|
|
1278
1377
|
} catch {
|
|
1279
1378
|
transport = null;
|
|
1280
1379
|
unbindTransportListeners();
|
|
1380
|
+
if (stopRequested || startVersion !== lifecycleVersion) {
|
|
1381
|
+
setState("disconnected");
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1281
1384
|
scheduleReconnect();
|
|
1282
1385
|
}
|
|
1283
1386
|
})();
|
|
@@ -1288,6 +1391,7 @@ function createStandaloneRealtimeService(options = {}) {
|
|
|
1288
1391
|
}
|
|
1289
1392
|
}
|
|
1290
1393
|
async function stopInternal() {
|
|
1394
|
+
lifecycleVersion += 1;
|
|
1291
1395
|
stopRequested = true;
|
|
1292
1396
|
clearReconnectTimer();
|
|
1293
1397
|
const current = transport;
|
|
@@ -1460,23 +1564,13 @@ function createStandaloneSharedStateService() {
|
|
|
1460
1564
|
return;
|
|
1461
1565
|
}
|
|
1462
1566
|
const listeners = requestListenersByKey.get(request.key);
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
`No owner request handler for key "${request.key}". Owner is "${current.owner}".`
|
|
1466
|
-
);
|
|
1467
|
-
}
|
|
1468
|
-
let handled = false;
|
|
1469
|
-
for (const sub of listeners) {
|
|
1470
|
-
if (sub.owner === current.owner) {
|
|
1471
|
-
handled = true;
|
|
1472
|
-
sub.listener(request);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
if (!handled) {
|
|
1567
|
+
const ownerListener = listeners?.get(current.owner);
|
|
1568
|
+
if (!ownerListener) {
|
|
1476
1569
|
throw new Error(
|
|
1477
1570
|
`Owner "${current.owner}" has no active request handler for key "${request.key}".`
|
|
1478
1571
|
);
|
|
1479
1572
|
}
|
|
1573
|
+
ownerListener(request);
|
|
1480
1574
|
},
|
|
1481
1575
|
subscribeKey: (key, listener) => {
|
|
1482
1576
|
let listeners = listenersByKey.get(key);
|
|
@@ -1496,17 +1590,18 @@ function createStandaloneSharedStateService() {
|
|
|
1496
1590
|
subscribeRequests: (key, owner, listener) => {
|
|
1497
1591
|
let listeners = requestListenersByKey.get(key);
|
|
1498
1592
|
if (!listeners) {
|
|
1499
|
-
listeners = /* @__PURE__ */ new
|
|
1593
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1500
1594
|
requestListenersByKey.set(key, listeners);
|
|
1501
1595
|
}
|
|
1502
|
-
|
|
1503
|
-
owner,
|
|
1504
|
-
listener
|
|
1505
|
-
};
|
|
1506
|
-
listeners.add(sub);
|
|
1596
|
+
listeners.set(owner, listener);
|
|
1507
1597
|
return () => {
|
|
1508
1598
|
const current = requestListenersByKey.get(key);
|
|
1509
|
-
current
|
|
1599
|
+
if (!current) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (current.get(owner) === listener) {
|
|
1603
|
+
current.delete(owner);
|
|
1604
|
+
}
|
|
1510
1605
|
if (current && current.size === 0) {
|
|
1511
1606
|
requestListenersByKey.delete(key);
|
|
1512
1607
|
}
|
|
@@ -1515,6 +1610,37 @@ function createStandaloneSharedStateService() {
|
|
|
1515
1610
|
};
|
|
1516
1611
|
}
|
|
1517
1612
|
|
|
1613
|
+
// src/services/tenant.ts
|
|
1614
|
+
function createStandaloneTenantService(options = {}) {
|
|
1615
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1616
|
+
let snapshot = {
|
|
1617
|
+
tenantId: options.tenantId ?? "dev-tenant",
|
|
1618
|
+
tenantSlug: options.tenantSlug ?? "dev"
|
|
1619
|
+
};
|
|
1620
|
+
function emit() {
|
|
1621
|
+
for (const listener of listeners) {
|
|
1622
|
+
listener();
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
getSnapshot: () => ({ ...snapshot }),
|
|
1627
|
+
setTenant: (tenantId, tenantSlug) => {
|
|
1628
|
+
if (tenantId === snapshot.tenantId && tenantSlug === snapshot.tenantSlug) {
|
|
1629
|
+
return { ...snapshot };
|
|
1630
|
+
}
|
|
1631
|
+
snapshot = { tenantId, tenantSlug };
|
|
1632
|
+
emit();
|
|
1633
|
+
return { ...snapshot };
|
|
1634
|
+
},
|
|
1635
|
+
subscribe: (listener) => {
|
|
1636
|
+
listeners.add(listener);
|
|
1637
|
+
return () => {
|
|
1638
|
+
listeners.delete(listener);
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1518
1644
|
// src/services/user.ts
|
|
1519
1645
|
function createStandaloneUserService(seedUser) {
|
|
1520
1646
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -1579,7 +1705,7 @@ function createStandaloneUserService(seedUser) {
|
|
|
1579
1705
|
}
|
|
1580
1706
|
|
|
1581
1707
|
// src/initStandaloneServices.ts
|
|
1582
|
-
function
|
|
1708
|
+
function createTestServices(options = {}) {
|
|
1583
1709
|
const {
|
|
1584
1710
|
apiBase = "http://localhost:4000",
|
|
1585
1711
|
lang = "vi",
|
|
@@ -1587,25 +1713,31 @@ function initStandaloneServices(options = {}) {
|
|
|
1587
1713
|
observability: observabilityConfig = {},
|
|
1588
1714
|
observabilityOptions,
|
|
1589
1715
|
realtime: realtimeOptions,
|
|
1590
|
-
|
|
1591
|
-
|
|
1716
|
+
navigationPath = "/standalone",
|
|
1717
|
+
featureFlags = {},
|
|
1718
|
+
runtimeConfig = {},
|
|
1719
|
+
tenant,
|
|
1720
|
+
appSettings
|
|
1592
1721
|
} = options;
|
|
1593
1722
|
const userService = createStandaloneUserService(user ?? null);
|
|
1594
1723
|
const notificationService = createStandaloneNotificationService();
|
|
1595
1724
|
const requestContext = createRequestContextService();
|
|
1596
|
-
const
|
|
1725
|
+
const tenantService = createStandaloneTenantService(tenant);
|
|
1726
|
+
const appSettingsService = createStandaloneAppSettingsService(appSettings);
|
|
1727
|
+
return {
|
|
1597
1728
|
i18n: createStandaloneI18nService(lang),
|
|
1598
1729
|
userService,
|
|
1599
1730
|
apiClient: createStandaloneApiClient({
|
|
1600
1731
|
apiBase,
|
|
1601
|
-
userService,
|
|
1732
|
+
getUserService: () => getService(TOKENS.userService) ?? userService,
|
|
1602
1733
|
notificationService,
|
|
1603
|
-
requestContext
|
|
1734
|
+
requestContext,
|
|
1735
|
+
tenantService
|
|
1604
1736
|
}),
|
|
1605
|
-
navigation: createStandaloneNavigationService(
|
|
1737
|
+
navigation: createStandaloneNavigationService(navigationPath),
|
|
1606
1738
|
configService: createStandaloneConfigService(
|
|
1607
|
-
{ [FEATURE_FLAGS2.uiExperimental]: true },
|
|
1608
|
-
{ envName: "standalone", apiBase }
|
|
1739
|
+
{ [FEATURE_FLAGS2.uiExperimental]: true, ...featureFlags },
|
|
1740
|
+
{ envName: runtimeConfig.envName ?? "standalone", apiBase: runtimeConfig.apiBase ?? apiBase }
|
|
1609
1741
|
),
|
|
1610
1742
|
permissionService: createStandalonePermissionService(),
|
|
1611
1743
|
sharedState: createStandaloneSharedStateService(),
|
|
@@ -1620,8 +1752,14 @@ function initStandaloneServices(options = {}) {
|
|
|
1620
1752
|
getTraceContext: () => requestContext.getSnapshot()
|
|
1621
1753
|
}),
|
|
1622
1754
|
notification: notificationService,
|
|
1623
|
-
analytics: createStandaloneAnalyticsService()
|
|
1755
|
+
analytics: createStandaloneAnalyticsService(),
|
|
1756
|
+
tenantService,
|
|
1757
|
+
appSettingsService
|
|
1624
1758
|
};
|
|
1759
|
+
}
|
|
1760
|
+
function initStandaloneServices(options = {}) {
|
|
1761
|
+
const { customize, registerMode = "if-missing" } = options;
|
|
1762
|
+
const services = createTestServices(options);
|
|
1625
1763
|
customize?.(services);
|
|
1626
1764
|
services.sharedState.ensureKey(SHARED_STATE_KEYS.x, "host", 0);
|
|
1627
1765
|
services.sharedState.subscribeRequests(SHARED_STATE_KEYS.x, "host", (request) => {
|
|
@@ -1633,42 +1771,32 @@ function initStandaloneServices(options = {}) {
|
|
|
1633
1771
|
data: { nextValue: request.nextValue, reason: request.reason ?? "" }
|
|
1634
1772
|
});
|
|
1635
1773
|
});
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
if (registerMode === "always" || !getService(TOKENS.observability)) {
|
|
1664
|
-
registerService(TOKENS.observability, services.observability);
|
|
1665
|
-
}
|
|
1666
|
-
if (registerMode === "always" || !getService(TOKENS.notification)) {
|
|
1667
|
-
registerService(TOKENS.notification, services.notification);
|
|
1668
|
-
}
|
|
1669
|
-
if (registerMode === "always" || !getService(TOKENS.analytics)) {
|
|
1670
|
-
registerService(TOKENS.analytics, services.analytics);
|
|
1671
|
-
}
|
|
1774
|
+
const tokenServiceMap = [
|
|
1775
|
+
[TOKENS.i18n, services.i18n],
|
|
1776
|
+
[TOKENS.userService, services.userService],
|
|
1777
|
+
[TOKENS.apiClient, services.apiClient],
|
|
1778
|
+
[TOKENS.navigation, services.navigation],
|
|
1779
|
+
[TOKENS.configService, services.configService],
|
|
1780
|
+
[TOKENS.permissionService, services.permissionService],
|
|
1781
|
+
[TOKENS.sharedState, services.sharedState],
|
|
1782
|
+
[TOKENS.requestContext, services.requestContext],
|
|
1783
|
+
[TOKENS.realtime, services.realtime],
|
|
1784
|
+
[TOKENS.observability, services.observability],
|
|
1785
|
+
[TOKENS.notification, services.notification],
|
|
1786
|
+
[TOKENS.analytics, services.analytics],
|
|
1787
|
+
[TOKENS.tenantService, services.tenantService],
|
|
1788
|
+
[TOKENS.appSettingsService, services.appSettingsService]
|
|
1789
|
+
];
|
|
1790
|
+
for (const [token, service] of tokenServiceMap) {
|
|
1791
|
+
if (registerMode === "always" || !getService(token)) {
|
|
1792
|
+
registerService(token, service);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function createDevPreset(defaults) {
|
|
1797
|
+
return (overrides) => {
|
|
1798
|
+
initStandaloneServices({ ...defaults, ...overrides });
|
|
1799
|
+
};
|
|
1672
1800
|
}
|
|
1673
1801
|
|
|
1674
1802
|
// src/services/apiErrorMapper.ts
|
|
@@ -2074,6 +2202,8 @@ function createReactRouterNavigationAdapter(options = {}) {
|
|
|
2074
2202
|
syncFromRouter: (location, action, capabilities) => {
|
|
2075
2203
|
const normalizedLocation = normalizeLocation(location);
|
|
2076
2204
|
const runtimeIndex = capabilities?.historyIndex ?? resolveWindowHistoryIndex();
|
|
2205
|
+
let canGoBack = capabilities?.canGoBack;
|
|
2206
|
+
let canGoForward = capabilities?.canGoForward;
|
|
2077
2207
|
if (typeof runtimeIndex === "number" && Number.isInteger(runtimeIndex)) {
|
|
2078
2208
|
currentIndex = runtimeIndex;
|
|
2079
2209
|
if (runtimeIndex > maxIndex) {
|
|
@@ -2082,11 +2212,13 @@ function createReactRouterNavigationAdapter(options = {}) {
|
|
|
2082
2212
|
} else if (action === "PUSH") {
|
|
2083
2213
|
currentIndex += 1;
|
|
2084
2214
|
maxIndex = currentIndex;
|
|
2085
|
-
} else if (action === "POP"
|
|
2086
|
-
|
|
2215
|
+
} else if (action === "POP") {
|
|
2216
|
+
const hasHistoryContext = snapshot.canGoBack || snapshot.canGoForward;
|
|
2217
|
+
canGoBack ??= hasHistoryContext;
|
|
2218
|
+
canGoForward ??= hasHistoryContext;
|
|
2087
2219
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2220
|
+
canGoBack ??= currentIndex > 0;
|
|
2221
|
+
canGoForward ??= currentIndex < maxIndex;
|
|
2090
2222
|
setSnapshot({
|
|
2091
2223
|
action,
|
|
2092
2224
|
location: normalizedLocation,
|
|
@@ -2150,13 +2282,17 @@ function hasSnapshotChanged(previous, nextValue) {
|
|
|
2150
2282
|
}
|
|
2151
2283
|
export {
|
|
2152
2284
|
ApiError,
|
|
2285
|
+
createDevPreset,
|
|
2153
2286
|
createMsalUserService,
|
|
2154
2287
|
createReactRouterNavigationAdapter,
|
|
2155
2288
|
createRequestContextService,
|
|
2156
2289
|
createStandaloneApiClient,
|
|
2290
|
+
createStandaloneAppSettingsService,
|
|
2157
2291
|
createStandaloneI18nService,
|
|
2158
2292
|
createStandaloneRealtimeService,
|
|
2293
|
+
createStandaloneTenantService,
|
|
2159
2294
|
createStandaloneUserService,
|
|
2295
|
+
createTestServices,
|
|
2160
2296
|
initStandaloneServices,
|
|
2161
2297
|
isApiError,
|
|
2162
2298
|
mapApiErrorToUiError
|
package/package.json
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ptech-shell-dev",
|
|
3
|
-
|
|
4
|
-
"description": "Standalone/mock shell service implementations for Module Federation apps.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsup src/index.ts --format esm --dts --out-dir dist --clean",
|
|
21
|
-
"dev": "tsup src/index.ts --format esm --dts --out-dir dist --watch",
|
|
22
|
-
"test": "npm run build && node --test tests/*.test.mjs",
|
|
23
|
-
"prepublishOnly": "npm run build"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"ptech-shell-sdk": "^1.
|
|
27
|
-
},
|
|
28
|
-
"peerDependencies": {
|
|
29
|
-
"react": ">=18"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"@types/react": "^19.2.13
|
|
33
|
-
"react": "^19.2.4",
|
|
34
|
-
"tsup": "^8.5.1",
|
|
35
|
-
"typescript": "^5.9.3"
|
|
36
|
-
},
|
|
37
|
-
"keywords": [
|
|
38
|
-
"micro-frontend",
|
|
39
|
-
"module-federation",
|
|
40
|
-
"shell",
|
|
41
|
-
"standalone",
|
|
42
|
-
"mock"
|
|
43
|
-
],
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ptech-shell-dev",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "Standalone/mock shell service implementations for Module Federation apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm --dts --out-dir dist --watch",
|
|
22
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ptech-shell-sdk": "^1.8.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^19.2.13",
|
|
33
|
+
"react": "^19.2.4",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"typescript": "^5.9.3"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"micro-frontend",
|
|
39
|
+
"module-federation",
|
|
40
|
+
"shell",
|
|
41
|
+
"standalone",
|
|
42
|
+
"mock"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|