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.
Files changed (3) hide show
  1. package/dist/index.d.ts +110 -3
  2. package/dist/index.js +212 -76
  3. 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
- * Example: http://localhost:4000
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: (listener) => {
1088
- listeners.add(listener);
1089
- return () => listeners.delete(listener);
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
- messageListener = messageListener;
1125
- disconnectListener = disconnectListener;
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
- if (!listeners || listeners.size === 0) {
1464
- throw new Error(
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 Set();
1593
+ listeners = /* @__PURE__ */ new Map();
1500
1594
  requestListenersByKey.set(key, listeners);
1501
1595
  }
1502
- const sub = {
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?.delete(sub);
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 initStandaloneServices(options = {}) {
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
- customize,
1591
- registerMode = "if-missing"
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 services = {
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("/standalone"),
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
- if (registerMode === "always" || !getService(TOKENS.i18n)) {
1637
- registerService(TOKENS.i18n, services.i18n);
1638
- }
1639
- if (registerMode === "always" || !getService(TOKENS.userService)) {
1640
- registerService(TOKENS.userService, services.userService);
1641
- }
1642
- if (registerMode === "always" || !getService(TOKENS.apiClient)) {
1643
- registerService(TOKENS.apiClient, services.apiClient);
1644
- }
1645
- if (registerMode === "always" || !getService(TOKENS.navigation)) {
1646
- registerService(TOKENS.navigation, services.navigation);
1647
- }
1648
- if (registerMode === "always" || !getService(TOKENS.configService)) {
1649
- registerService(TOKENS.configService, services.configService);
1650
- }
1651
- if (registerMode === "always" || !getService(TOKENS.permissionService)) {
1652
- registerService(TOKENS.permissionService, services.permissionService);
1653
- }
1654
- if (registerMode === "always" || !getService(TOKENS.sharedState)) {
1655
- registerService(TOKENS.sharedState, services.sharedState);
1656
- }
1657
- if (registerMode === "always" || !getService(TOKENS.requestContext)) {
1658
- registerService(TOKENS.requestContext, services.requestContext);
1659
- }
1660
- if (registerMode === "always" || !getService(TOKENS.realtime)) {
1661
- registerService(TOKENS.realtime, services.realtime);
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" && currentIndex > 0) {
2086
- currentIndex -= 1;
2215
+ } else if (action === "POP") {
2216
+ const hasHistoryContext = snapshot.canGoBack || snapshot.canGoForward;
2217
+ canGoBack ??= hasHistoryContext;
2218
+ canGoForward ??= hasHistoryContext;
2087
2219
  }
2088
- const canGoBack = capabilities?.canGoBack ?? currentIndex > 0;
2089
- const canGoForward = capabilities?.canGoForward ?? currentIndex < maxIndex;
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
- "version": "1.6.7",
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.6.5"
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
+ }