ptech-shell-dev 1.6.7 → 1.7.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 CHANGED
@@ -79,15 +79,17 @@ type ServiceRegisterMode = 'if-missing' | 'always';
79
79
  type StandaloneInitOptions = {
80
80
  /**
81
81
  * API base URL when running standalone.
82
- * Example: http://localhost:4000
82
+ * @default 'http://localhost:4000'
83
83
  */
84
84
  apiBase?: string;
85
85
  /**
86
86
  * Default language for standalone.
87
+ * @default 'vi'
87
88
  */
88
89
  lang?: Lang;
89
90
  /**
90
91
  * Seed user for standalone. (Optional)
92
+ * @default { id: 'dev', name: 'Dev User' }
91
93
  */
92
94
  user?: User | null;
93
95
  /**
@@ -116,8 +118,30 @@ type StandaloneInitOptions = {
116
118
  * - always: replace any existing service for the token.
117
119
  */
118
120
  registerMode?: ServiceRegisterMode;
121
+ /**
122
+ * Default path for standalone navigation service.
123
+ * @default '/standalone'
124
+ */
125
+ navigationPath?: string;
126
+ /**
127
+ * Feature flags seed for standalone config service.
128
+ * Merged with built-in defaults.
129
+ */
130
+ featureFlags?: Record<string, boolean>;
131
+ /**
132
+ * Runtime config overrides for standalone config service.
133
+ */
134
+ runtimeConfig?: {
135
+ envName?: string;
136
+ apiBase?: string;
137
+ };
119
138
  };
120
139
 
140
+ /**
141
+ * Creates all standalone service instances without registering them.
142
+ * Useful for testing or when you need direct access to services.
143
+ */
144
+ declare function createTestServices(options?: StandaloneInitOptions): StandaloneServices;
121
145
  /**
122
146
  * Initialize "safe defaults" so a remote app can run standalone without the host.
123
147
  *
@@ -125,6 +149,21 @@ type StandaloneInitOptions = {
125
149
  * But if host doesn't (or remote is standalone), this provides mocks.
126
150
  */
127
151
  declare function initStandaloneServices(options?: StandaloneInitOptions): void;
152
+ /**
153
+ * Create a preset factory with default options baked in.
154
+ * Useful for projects that want consistent defaults across remotes.
155
+ *
156
+ * @example
157
+ * // In a shared config file:
158
+ * export const initHrmServices = createDevPreset({
159
+ * apiBase: 'http://localhost:5000',
160
+ * lang: 'en',
161
+ * });
162
+ *
163
+ * // In each remote's bootstrap:
164
+ * initHrmServices({ user: { id: 'admin', name: 'Admin' } });
165
+ */
166
+ declare function createDevPreset(defaults: StandaloneInitOptions): (overrides?: StandaloneInitOptions) => void;
128
167
 
129
168
  type CreateStandaloneApiClientOptions = {
130
169
  apiBase: string;
@@ -376,4 +415,4 @@ declare function createRequestContextService(options?: CreateRequestContextServi
376
415
  */
377
416
  declare function createStandaloneUserService(seedUser: User | null): UserService;
378
417
 
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 };
418
+ 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, createStandaloneI18nService, createStandaloneRealtimeService, createStandaloneUserService, createTestServices, initStandaloneServices, isApiError, mapApiErrorToUiError };
package/dist/index.js CHANGED
@@ -354,6 +354,15 @@ function normalizeRetryOptions(input, defaultRetryOptions) {
354
354
  function shouldRetryResponse(response, retry) {
355
355
  return retry.retryOnStatuses.has(response.status);
356
356
  }
357
+ async function disposeRetryResponse(response) {
358
+ if (!response.body || typeof response.body.cancel !== "function") {
359
+ return;
360
+ }
361
+ try {
362
+ await response.body.cancel();
363
+ } catch {
364
+ }
365
+ }
357
366
  function shouldRetryError(error) {
358
367
  return error.isNetworkError && error.code !== "aborted";
359
368
  }
@@ -541,6 +550,7 @@ async function executeRaw(requestOptions, options) {
541
550
  );
542
551
  syncResponseContext(response, options);
543
552
  if (retry && shouldUseRetry && attempt < maxAttempts && shouldRetryResponse(response, retry)) {
553
+ await disposeRetryResponse(response);
544
554
  const delayMs = computeRetryDelayMs(attempt, retry);
545
555
  await sleep(delayMs, requestOptions.signal);
546
556
  continue;
@@ -1079,14 +1089,13 @@ import {
1079
1089
  } from "ptech-shell-sdk";
1080
1090
  var DEFAULT_PERMISSIONS = listPermissionsByRole(USER_ROLES.authenticated);
1081
1091
  function createStandalonePermissionService(initialPermissions = DEFAULT_PERMISSIONS) {
1082
- const listeners = /* @__PURE__ */ new Set();
1083
1092
  const permissions = new Set(initialPermissions);
1084
1093
  return {
1085
1094
  can: (permission) => permissions.has(permission),
1086
1095
  list: () => Array.from(permissions.values()),
1087
- subscribe: (listener) => {
1088
- listeners.add(listener);
1089
- return () => listeners.delete(listener);
1096
+ subscribe: () => {
1097
+ return () => {
1098
+ };
1090
1099
  }
1091
1100
  };
1092
1101
  }
@@ -1121,8 +1130,8 @@ function createNoopTransport() {
1121
1130
  let disconnectListener;
1122
1131
  return {
1123
1132
  start: async () => {
1124
- messageListener = messageListener;
1125
- disconnectListener = disconnectListener;
1133
+ void messageListener;
1134
+ void disconnectListener;
1126
1135
  },
1127
1136
  stop: async () => void 0,
1128
1137
  onMessage: (listener) => {
@@ -1169,6 +1178,7 @@ function createStandaloneRealtimeService(options = {}) {
1169
1178
  let unbindMessage;
1170
1179
  let unbindDisconnect;
1171
1180
  let inFlightStart = null;
1181
+ let lifecycleVersion = 0;
1172
1182
  function emitStateChanged() {
1173
1183
  for (const listener of listeners) {
1174
1184
  listener();
@@ -1259,6 +1269,7 @@ function createStandaloneRealtimeService(options = {}) {
1259
1269
  return inFlightStart;
1260
1270
  }
1261
1271
  inFlightStart = (async () => {
1272
+ const startVersion = lifecycleVersion;
1262
1273
  stopRequested = false;
1263
1274
  clearReconnectTimer();
1264
1275
  setState(activeAttempt > 0 ? "reconnecting" : "connecting");
@@ -1271,6 +1282,14 @@ function createStandaloneRealtimeService(options = {}) {
1271
1282
  traceId: context.traceId,
1272
1283
  correlationId: context.correlationId
1273
1284
  });
1285
+ if (stopRequested || startVersion !== lifecycleVersion) {
1286
+ try {
1287
+ await nextTransport.stop();
1288
+ } catch {
1289
+ }
1290
+ setState("disconnected");
1291
+ return;
1292
+ }
1274
1293
  transport = nextTransport;
1275
1294
  await bindTransportListeners(nextTransport);
1276
1295
  activeAttempt = 0;
@@ -1278,6 +1297,10 @@ function createStandaloneRealtimeService(options = {}) {
1278
1297
  } catch {
1279
1298
  transport = null;
1280
1299
  unbindTransportListeners();
1300
+ if (stopRequested || startVersion !== lifecycleVersion) {
1301
+ setState("disconnected");
1302
+ return;
1303
+ }
1281
1304
  scheduleReconnect();
1282
1305
  }
1283
1306
  })();
@@ -1288,6 +1311,7 @@ function createStandaloneRealtimeService(options = {}) {
1288
1311
  }
1289
1312
  }
1290
1313
  async function stopInternal() {
1314
+ lifecycleVersion += 1;
1291
1315
  stopRequested = true;
1292
1316
  clearReconnectTimer();
1293
1317
  const current = transport;
@@ -1460,23 +1484,13 @@ function createStandaloneSharedStateService() {
1460
1484
  return;
1461
1485
  }
1462
1486
  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) {
1487
+ const ownerListener = listeners?.get(current.owner);
1488
+ if (!ownerListener) {
1476
1489
  throw new Error(
1477
1490
  `Owner "${current.owner}" has no active request handler for key "${request.key}".`
1478
1491
  );
1479
1492
  }
1493
+ ownerListener(request);
1480
1494
  },
1481
1495
  subscribeKey: (key, listener) => {
1482
1496
  let listeners = listenersByKey.get(key);
@@ -1496,17 +1510,18 @@ function createStandaloneSharedStateService() {
1496
1510
  subscribeRequests: (key, owner, listener) => {
1497
1511
  let listeners = requestListenersByKey.get(key);
1498
1512
  if (!listeners) {
1499
- listeners = /* @__PURE__ */ new Set();
1513
+ listeners = /* @__PURE__ */ new Map();
1500
1514
  requestListenersByKey.set(key, listeners);
1501
1515
  }
1502
- const sub = {
1503
- owner,
1504
- listener
1505
- };
1506
- listeners.add(sub);
1516
+ listeners.set(owner, listener);
1507
1517
  return () => {
1508
1518
  const current = requestListenersByKey.get(key);
1509
- current?.delete(sub);
1519
+ if (!current) {
1520
+ return;
1521
+ }
1522
+ if (current.get(owner) === listener) {
1523
+ current.delete(owner);
1524
+ }
1510
1525
  if (current && current.size === 0) {
1511
1526
  requestListenersByKey.delete(key);
1512
1527
  }
@@ -1579,7 +1594,7 @@ function createStandaloneUserService(seedUser) {
1579
1594
  }
1580
1595
 
1581
1596
  // src/initStandaloneServices.ts
1582
- function initStandaloneServices(options = {}) {
1597
+ function createTestServices(options = {}) {
1583
1598
  const {
1584
1599
  apiBase = "http://localhost:4000",
1585
1600
  lang = "vi",
@@ -1587,25 +1602,26 @@ function initStandaloneServices(options = {}) {
1587
1602
  observability: observabilityConfig = {},
1588
1603
  observabilityOptions,
1589
1604
  realtime: realtimeOptions,
1590
- customize,
1591
- registerMode = "if-missing"
1605
+ navigationPath = "/standalone",
1606
+ featureFlags = {},
1607
+ runtimeConfig = {}
1592
1608
  } = options;
1593
1609
  const userService = createStandaloneUserService(user ?? null);
1594
1610
  const notificationService = createStandaloneNotificationService();
1595
1611
  const requestContext = createRequestContextService();
1596
- const services = {
1612
+ return {
1597
1613
  i18n: createStandaloneI18nService(lang),
1598
1614
  userService,
1599
1615
  apiClient: createStandaloneApiClient({
1600
1616
  apiBase,
1601
- userService,
1617
+ getUserService: () => getService(TOKENS.userService) ?? userService,
1602
1618
  notificationService,
1603
1619
  requestContext
1604
1620
  }),
1605
- navigation: createStandaloneNavigationService("/standalone"),
1621
+ navigation: createStandaloneNavigationService(navigationPath),
1606
1622
  configService: createStandaloneConfigService(
1607
- { [FEATURE_FLAGS2.uiExperimental]: true },
1608
- { envName: "standalone", apiBase }
1623
+ { [FEATURE_FLAGS2.uiExperimental]: true, ...featureFlags },
1624
+ { envName: runtimeConfig.envName ?? "standalone", apiBase: runtimeConfig.apiBase ?? apiBase }
1609
1625
  ),
1610
1626
  permissionService: createStandalonePermissionService(),
1611
1627
  sharedState: createStandaloneSharedStateService(),
@@ -1622,6 +1638,10 @@ function initStandaloneServices(options = {}) {
1622
1638
  notification: notificationService,
1623
1639
  analytics: createStandaloneAnalyticsService()
1624
1640
  };
1641
+ }
1642
+ function initStandaloneServices(options = {}) {
1643
+ const { customize, registerMode = "if-missing" } = options;
1644
+ const services = createTestServices(options);
1625
1645
  customize?.(services);
1626
1646
  services.sharedState.ensureKey(SHARED_STATE_KEYS.x, "host", 0);
1627
1647
  services.sharedState.subscribeRequests(SHARED_STATE_KEYS.x, "host", (request) => {
@@ -1633,42 +1653,30 @@ function initStandaloneServices(options = {}) {
1633
1653
  data: { nextValue: request.nextValue, reason: request.reason ?? "" }
1634
1654
  });
1635
1655
  });
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
- }
1656
+ const tokenServiceMap = [
1657
+ [TOKENS.i18n, services.i18n],
1658
+ [TOKENS.userService, services.userService],
1659
+ [TOKENS.apiClient, services.apiClient],
1660
+ [TOKENS.navigation, services.navigation],
1661
+ [TOKENS.configService, services.configService],
1662
+ [TOKENS.permissionService, services.permissionService],
1663
+ [TOKENS.sharedState, services.sharedState],
1664
+ [TOKENS.requestContext, services.requestContext],
1665
+ [TOKENS.realtime, services.realtime],
1666
+ [TOKENS.observability, services.observability],
1667
+ [TOKENS.notification, services.notification],
1668
+ [TOKENS.analytics, services.analytics]
1669
+ ];
1670
+ for (const [token, service] of tokenServiceMap) {
1671
+ if (registerMode === "always" || !getService(token)) {
1672
+ registerService(token, service);
1673
+ }
1674
+ }
1675
+ }
1676
+ function createDevPreset(defaults) {
1677
+ return (overrides) => {
1678
+ initStandaloneServices({ ...defaults, ...overrides });
1679
+ };
1672
1680
  }
1673
1681
 
1674
1682
  // src/services/apiErrorMapper.ts
@@ -2074,6 +2082,8 @@ function createReactRouterNavigationAdapter(options = {}) {
2074
2082
  syncFromRouter: (location, action, capabilities) => {
2075
2083
  const normalizedLocation = normalizeLocation(location);
2076
2084
  const runtimeIndex = capabilities?.historyIndex ?? resolveWindowHistoryIndex();
2085
+ let canGoBack = capabilities?.canGoBack;
2086
+ let canGoForward = capabilities?.canGoForward;
2077
2087
  if (typeof runtimeIndex === "number" && Number.isInteger(runtimeIndex)) {
2078
2088
  currentIndex = runtimeIndex;
2079
2089
  if (runtimeIndex > maxIndex) {
@@ -2082,11 +2092,13 @@ function createReactRouterNavigationAdapter(options = {}) {
2082
2092
  } else if (action === "PUSH") {
2083
2093
  currentIndex += 1;
2084
2094
  maxIndex = currentIndex;
2085
- } else if (action === "POP" && currentIndex > 0) {
2086
- currentIndex -= 1;
2095
+ } else if (action === "POP") {
2096
+ const hasHistoryContext = snapshot.canGoBack || snapshot.canGoForward;
2097
+ canGoBack ??= hasHistoryContext;
2098
+ canGoForward ??= hasHistoryContext;
2087
2099
  }
2088
- const canGoBack = capabilities?.canGoBack ?? currentIndex > 0;
2089
- const canGoForward = capabilities?.canGoForward ?? currentIndex < maxIndex;
2100
+ canGoBack ??= currentIndex > 0;
2101
+ canGoForward ??= currentIndex < maxIndex;
2090
2102
  setSnapshot({
2091
2103
  action,
2092
2104
  location: normalizedLocation,
@@ -2150,6 +2162,7 @@ function hasSnapshotChanged(previous, nextValue) {
2150
2162
  }
2151
2163
  export {
2152
2164
  ApiError,
2165
+ createDevPreset,
2153
2166
  createMsalUserService,
2154
2167
  createReactRouterNavigationAdapter,
2155
2168
  createRequestContextService,
@@ -2157,6 +2170,7 @@ export {
2157
2170
  createStandaloneI18nService,
2158
2171
  createStandaloneRealtimeService,
2159
2172
  createStandaloneUserService,
2173
+ createTestServices,
2160
2174
  initStandaloneServices,
2161
2175
  isApiError,
2162
2176
  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
- },
1
+ {
2
+ "name": "ptech-shell-dev",
3
+ "version": "1.7.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.7.0"
27
+ },
28
28
  "peerDependencies": {
29
29
  "react": ">=18"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/react": "^19.2.13 ",
32
+ "@types/react": "^19.2.13",
33
33
  "react": "^19.2.4",
34
34
  "tsup": "^8.5.1",
35
35
  "typescript": "^5.9.3"
36
36
  },
37
- "keywords": [
38
- "micro-frontend",
39
- "module-federation",
40
- "shell",
41
- "standalone",
42
- "mock"
43
- ],
44
- "license": "MIT"
45
- }
37
+ "keywords": [
38
+ "micro-frontend",
39
+ "module-federation",
40
+ "shell",
41
+ "standalone",
42
+ "mock"
43
+ ],
44
+ "license": "MIT"
45
+ }