@trackunit/react-core-contexts 1.30.3 → 1.30.5-alpha-d37885bfce4.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/index.cjs.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var client = require('@apollo/client');
5
+ var reactCoreHooks = require('@trackunit/react-core-hooks');
6
+ var react = require('react');
5
7
  var context = require('@apollo/client/link/context');
6
8
  var removeTypename = require('@apollo/client/link/remove-typename');
7
9
  var utilities = require('@apollo/client/utilities');
8
- var reactCoreHooks = require('@trackunit/react-core-hooks');
9
10
  var graphql = require('graphql');
10
11
  var graphqlSse = require('graphql-sse');
11
- var react = require('react');
12
12
  var error = require('@apollo/client/link/error');
13
13
  var irisAppRuntimeCore = require('@trackunit/iris-app-runtime-core');
14
14
  var reactCoreContextsApi = require('@trackunit/react-core-contexts-api');
@@ -19,7 +19,7 @@ var irisAppRuntimeCoreApi = require('@trackunit/iris-app-runtime-core-api');
19
19
  /**
20
20
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
21
21
  */
22
- const createErrorLink = ({ errorHandler, token, }) => {
22
+ const createErrorLink = ({ errorHandler, getToken, }) => {
23
23
  return error.onError(({ graphQLErrors, networkError, operation, forward }) => {
24
24
  if (networkError) {
25
25
  // We skip the error logging if the error is an AbortError
@@ -67,7 +67,7 @@ const createErrorLink = ({ errorHandler, token, }) => {
67
67
  x.message.includes("Invalid token specified") ||
68
68
  x.message.includes("Access denied! You need to be authorized to perform this action!"));
69
69
  });
70
- if (invalidToken && token) {
70
+ if (invalidToken && getToken()) {
71
71
  errorHandler.captureException(new Error(JSON.stringify({
72
72
  category: "GraphQL",
73
73
  info: "GraphQL Error - invalidToken",
@@ -103,6 +103,69 @@ const createErrorLink = ({ errorHandler, token, }) => {
103
103
  });
104
104
  };
105
105
 
106
+ /**
107
+ * Wraps an SSE subscription link and provides the same error monitoring as the
108
+ * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
109
+ * and UNAUTHENTICATED codes — for long-lived subscription Observables.
110
+ */
111
+ const createSubscriptionErrorLink = ({ errorHandler, getToken, }) => {
112
+ return new client.ApolloLink((operation, forward) => {
113
+ return new client.Observable(observer => {
114
+ const subscription = forward(operation).subscribe({
115
+ next: response => {
116
+ const { errors } = response;
117
+ if (errors) {
118
+ const code = errors[0]?.extensions?.code;
119
+ if (code === "FORCE_RELOAD_BROWSER") {
120
+ window.location.reload();
121
+ }
122
+ const traceIds = [];
123
+ errors.forEach(error => {
124
+ if ("extensions" in error && error.extensions && typeof error.extensions.traceId === "string") {
125
+ traceIds.push(error.extensions.traceId);
126
+ }
127
+ });
128
+ if (traceIds.length) {
129
+ errorHandler.setTag("traceIds", traceIds.join(", "));
130
+ }
131
+ errorHandler.addBreadcrumb({
132
+ category: "GraphQL",
133
+ message: "GraphQL Subscription Error",
134
+ level: "error",
135
+ data: { log: JSON.stringify(errors) },
136
+ });
137
+ const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
138
+ x.message.includes("Invalid token specified") ||
139
+ x.message.includes("Access denied! You need to be authorized to perform this action!"));
140
+ if (invalidToken && getToken()) {
141
+ errorHandler.captureException(new Error(JSON.stringify({
142
+ category: "GraphQL",
143
+ info: "GraphQL Subscription Error - invalidToken",
144
+ level: "warning",
145
+ data: { log: JSON.stringify(errors) },
146
+ })), { level: "warning", fingerprint: ["GraphQL Subscription Error - invalidToken"] });
147
+ }
148
+ }
149
+ observer.next(response);
150
+ },
151
+ error: err => {
152
+ // eslint-disable-next-line no-console
153
+ console.error(err);
154
+ errorHandler.addBreadcrumb({
155
+ category: "GraphQL",
156
+ message: "GraphQL Subscription Network Error",
157
+ level: "error",
158
+ data: { log: String(err) },
159
+ });
160
+ observer.error(err);
161
+ },
162
+ complete: () => observer.complete(),
163
+ });
164
+ return () => subscription.unsubscribe();
165
+ });
166
+ });
167
+ };
168
+
106
169
  // Use a widened `string` key so TypeScript picks the `unknown` overload of
107
170
  // `Reflect.get` instead of the typed one — otherwise reads like `module`
108
171
  // resolve to `NodeModule` from @types/node.
@@ -170,11 +233,12 @@ const isInternalGqlContext = () => {
170
233
  return Reflect.get(globalThis, "gql") === "internal";
171
234
  };
172
235
 
173
- const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders, firstToken, errorHandler, }) => {
174
- let token;
175
- if (!token) {
176
- token = firstToken;
177
- }
236
+ /**
237
+ * @internal
238
+ */
239
+ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
240
+ let token = firstToken;
241
+ let tracingHeaders = initialTracingHeaders;
178
242
  const publicGraphQLLink = client.createHttpLink({
179
243
  uri: request => graphqlPublicUrl + "/" + request.operationName,
180
244
  });
@@ -184,7 +248,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
184
248
  const reportGraphQLLink = client.createHttpLink({
185
249
  uri: request => graphqlReportUrl + "/" + request.operationName,
186
250
  });
187
- const authLink = context.setContext(async (_, { headers: existingHeaders }) => {
251
+ const authLink = context.setContext((_, { headers: existingHeaders }) => {
188
252
  return {
189
253
  headers: {
190
254
  ...existingHeaders,
@@ -192,7 +256,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
192
256
  },
193
257
  };
194
258
  });
195
- const errorLink = createErrorLink({ errorHandler, token });
259
+ const errorLink = createErrorLink({ errorHandler, getToken: () => token });
196
260
  const defaultOptions = {
197
261
  watchQuery: {
198
262
  fetchPolicy: "no-cache",
@@ -209,10 +273,12 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
209
273
  };
210
274
  const removeTypenameLink = removeTypename.removeTypenameFromVariables();
211
275
  class SSELink extends client.ApolloLink {
276
+ /** @inheritdoc */
212
277
  constructor(options) {
213
278
  super();
214
279
  this.client = graphqlSse.createClient(options);
215
280
  }
281
+ /** @inheritdoc */
216
282
  request(operation) {
217
283
  return new utilities.Observable(sink => {
218
284
  return this.client.subscribe({ ...operation, query: graphql.print(operation.query) }, {
@@ -228,12 +294,13 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
228
294
  headers: () => generateHeaders(token, tracingHeaders),
229
295
  });
230
296
  // Split links based on operation type
297
+ const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
231
298
  const splitLink = client.from([
232
299
  authLink,
233
300
  client.split(({ query }) => {
234
301
  const definition = utilities.getMainDefinition(query);
235
302
  return definition.kind === "OperationDefinition" && definition.operation === "subscription";
236
- }, sseLink, client.from([
303
+ }, client.from([subscriptionErrorLink, sseLink]), client.from([
237
304
  errorLink,
238
305
  removeTypenameLink,
239
306
  client.split(operation => operation.getContext().clientName === "report", reportGraphQLLink, client.split(() => isInternalGqlContext(), internalGraphQLLink, publicGraphQLLink)),
@@ -261,8 +328,12 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
261
328
  getToken: () => {
262
329
  return token;
263
330
  },
331
+ setTracingHeaders: (newHeaders) => {
332
+ tracingHeaders = newHeaders;
333
+ },
264
334
  };
265
335
  };
336
+
266
337
  const useApolloClient = () => {
267
338
  const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = reactCoreHooks.useEnvironment();
268
339
  const { token: currentToken } = reactCoreHooks.useToken();
@@ -281,11 +352,17 @@ const useApolloClient = () => {
281
352
  errorHandler,
282
353
  });
283
354
  });
284
- react.useMemo(() => {
285
- if (client.getToken() !== currentToken) {
286
- client.setToken(currentToken);
287
- }
288
- }, [client, currentToken]);
355
+ // Synchronously propagate the token into the Apollo client closure during render
356
+ // so that child components' effects (useLayoutEffect / useEffect) always see the
357
+ // current token when they fire their first request in the same commit.
358
+ // React runs child effects before parent effects, so a parent useEffect here would
359
+ // be too late — child queries would go out with the previous token for one commit.
360
+ if (client.getToken() !== currentToken) {
361
+ client.setToken(currentToken);
362
+ }
363
+ react.useEffect(() => {
364
+ client.setTracingHeaders(tracingHeaders);
365
+ }, [client, tracingHeaders]);
289
366
  return client;
290
367
  };
291
368
  /**
@@ -352,16 +429,57 @@ const AnalyticsProviderIrisApp = ({ children }) => {
352
429
  };
353
430
 
354
431
  /**
355
- * Subscribe to methods initiated by changes in the host
432
+ * Subscribe to methods initiated by changes in the host.
433
+ *
434
+ * Handlers are registered on the singleton `host-runtime` penpal channel via
435
+ * {@link registerHostChangeHandler}, so push events from the host
436
+ * (`onTokenChanged`, `onTimeRangeChanged`, filter-bar updates, …) reach every
437
+ * mounted provider in the iris-app without forcing the host to open one
438
+ * penpal connection per subscription channel.
356
439
  *
357
- * @param methods STABLE object with the methods you'd want to subscribe to. Wrap in useMemo for instance to make it stable
440
+ * @param methods STABLE object with the methods you'd want to subscribe to.
441
+ * Wrap in `useMemo` (or build with stable callback references) so the
442
+ * effect doesn't re-register on every render.
443
+ * @param _channel Deprecated. Pre-v7 builds used a dedicated penpal channel
444
+ * per subscription provider (e.g. `token-subscription`); v7 collapses all
445
+ * subscriptions onto the singleton's `host-runtime` channel through a
446
+ * handler registry. The argument is accepted for source-compatibility with
447
+ * the v6 API and is ignored.
358
448
  */
359
- const useSubscribeToHostChanges = (methods) => {
449
+ const useSubscribeToHostChanges = (methods, _channel) => {
360
450
  react.useEffect(() => {
361
- const connection = irisAppRuntimeCore.setupHostConnector(methods);
362
- return () => {
363
- connection.destroy();
364
- };
451
+ const unregisters = [];
452
+ // Listed explicitly (rather than via `Object.entries`) so each call to
453
+ // `registerHostChangeHandler<K>` keeps the K → handler-type connection,
454
+ // which a runtime iteration over keys would erase.
455
+ if (methods.onTokenChanged) {
456
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onTokenChanged", methods.onTokenChanged));
457
+ }
458
+ if (methods.onTimeRangeChanged) {
459
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onTimeRangeChanged", methods.onTimeRangeChanged));
460
+ }
461
+ if (methods.onFilterBarValuesChanged) {
462
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onFilterBarValuesChanged", methods.onFilterBarValuesChanged));
463
+ }
464
+ if (methods.onAssetsFilterBarValuesChanged) {
465
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onAssetsFilterBarValuesChanged", methods.onAssetsFilterBarValuesChanged));
466
+ }
467
+ if (methods.onCustomersFilterBarValuesChanged) {
468
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onCustomersFilterBarValuesChanged", methods.onCustomersFilterBarValuesChanged));
469
+ }
470
+ if (methods.onSitesFilterBarValuesChanged) {
471
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onSitesFilterBarValuesChanged", methods.onSitesFilterBarValuesChanged));
472
+ }
473
+ if (methods.onTablePersistenceStateChanged) {
474
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onTablePersistenceStateChanged", methods.onTablePersistenceStateChanged));
475
+ }
476
+ if (methods.onAssetSortingStateChanged) {
477
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onAssetSortingStateChanged", methods.onAssetSortingStateChanged));
478
+ }
479
+ if (methods.onWidgetPollIntervalChanged) {
480
+ unregisters.push(irisAppRuntimeCore.registerHostChangeHandler("onWidgetPollIntervalChanged", methods.onWidgetPollIntervalChanged));
481
+ }
482
+ return () => unregisters.forEach(unregister => unregister());
365
483
  }, [methods]);
366
484
  };
367
485
 
@@ -378,7 +496,7 @@ const AssetSortingProviderIrisApp = ({ children }) => {
378
496
  const methods = react.useMemo(() => ({
379
497
  onAssetSortingStateChanged: setAssetSortingState,
380
498
  }), [setAssetSortingState]);
381
- useSubscribeToHostChanges(methods);
499
+ useSubscribeToHostChanges(methods, irisAppRuntimeCoreApi.Channels.AssetSortingSubscription);
382
500
  const contextValue = react.useMemo(() => ({
383
501
  sortingState: assetSortingState ?? {
384
502
  sortBy: irisAppRuntimeCoreApi.AssetSortByProperty.Criticality,
@@ -493,7 +611,7 @@ const FilterBarProviderIrisApp = ({ children }) => {
493
611
  setSitesFilterBarValues(values);
494
612
  },
495
613
  }), []);
496
- useSubscribeToHostChanges(methods);
614
+ useSubscribeToHostChanges(methods, irisAppRuntimeCoreApi.Channels.FilterBarSubscription);
497
615
  const isLoading = react.useMemo(() => assetsFilterBarValues === undefined ||
498
616
  customersFilterBarValues === undefined ||
499
617
  sitesFilterBarValues === undefined, [assetsFilterBarValues, customersFilterBarValues, sitesFilterBarValues]);
@@ -596,7 +714,7 @@ const TimeRangeProviderIrisApp = ({ children }) => {
596
714
  }));
597
715
  },
598
716
  }), []);
599
- useSubscribeToHostChanges(methods);
717
+ useSubscribeToHostChanges(methods, irisAppRuntimeCoreApi.Channels.TimeRangeSubscription);
600
718
  if (!timeRangeContext) {
601
719
  return null;
602
720
  }
@@ -634,7 +752,7 @@ const TokenProviderIrisApp = ({ children }) => {
634
752
  const methods = react.useMemo(() => ({
635
753
  onTokenChanged,
636
754
  }), [onTokenChanged]);
637
- useSubscribeToHostChanges(methods);
755
+ useSubscribeToHostChanges(methods, irisAppRuntimeCoreApi.Channels.TokenSubscription);
638
756
  if (state.status === "error") {
639
757
  throw new Error("Token context is invalid");
640
758
  }
@@ -762,7 +880,7 @@ const WidgetConfigProviderIrisApp = ({ children }) => {
762
880
  const methods = react.useMemo(() => ({
763
881
  onWidgetPollIntervalChanged: setPollInterval,
764
882
  }), [setPollInterval]);
765
- useSubscribeToHostChanges(methods);
883
+ useSubscribeToHostChanges(methods, irisAppRuntimeCoreApi.Channels.WidgetConfigSubscription);
766
884
  const widgetConfigContextValue = react.useMemo(() => ({
767
885
  getData: irisAppRuntimeCore.WidgetConfigRuntime.getWidgetData,
768
886
  setData: irisAppRuntimeCore.WidgetConfigRuntime.setWidgetData,
package/index.esm.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import { jsx, Fragment } from 'react/jsx-runtime';
2
- import { ApolloProvider, createHttpLink, from, split, ApolloClient, InMemoryCache, ApolloLink } from '@apollo/client';
2
+ import { ApolloLink, Observable, createHttpLink, from, split, ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
3
+ import { useEnvironment, useToken, useErrorHandler } from '@trackunit/react-core-hooks';
4
+ import { useState, useEffect, useMemo, useCallback, useReducer, Suspense } from 'react';
3
5
  import { setContext } from '@apollo/client/link/context';
4
6
  import { removeTypenameFromVariables } from '@apollo/client/link/remove-typename';
5
- import { getMainDefinition, Observable } from '@apollo/client/utilities';
6
- import { useEnvironment, useToken, useErrorHandler } from '@trackunit/react-core-hooks';
7
+ import { getMainDefinition, Observable as Observable$1 } from '@apollo/client/utilities';
7
8
  import { print } from 'graphql';
8
9
  import { createClient } from 'graphql-sse';
9
- import { useState, useMemo, useEffect, useCallback, useReducer, Suspense } from 'react';
10
10
  import { onError } from '@apollo/client/link/error';
11
- import { ToastRuntime, AnalyticsRuntime, setupHostConnector, AssetSortingRuntime, ConfirmationDialogRuntime, EnvironmentRuntime, ExportDataRuntime, AssetsFilterBarRuntime, CustomersFilterBarRuntime, SitesFilterBarRuntime, GeolocationRuntime, ModalDialogRuntime, NavigationRuntime, OemBrandingRuntime, ThemeCssRuntime, TimeRangeRuntime, TokenRuntime, CurrentUserRuntime, CurrentUserPreferenceRuntime, UserSubscriptionRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
11
+ import { ToastRuntime, AnalyticsRuntime, registerHostChangeHandler, AssetSortingRuntime, ConfirmationDialogRuntime, EnvironmentRuntime, ExportDataRuntime, AssetsFilterBarRuntime, CustomersFilterBarRuntime, SitesFilterBarRuntime, GeolocationRuntime, ModalDialogRuntime, NavigationRuntime, OemBrandingRuntime, ThemeCssRuntime, TimeRangeRuntime, TokenRuntime, CurrentUserRuntime, CurrentUserPreferenceRuntime, UserSubscriptionRuntime, WidgetConfigRuntime } from '@trackunit/iris-app-runtime-core';
12
12
  import { ToastProvider, AnalyticsContextProvider, AssetSortingProvider, ConfirmationDialogProvider, EnvironmentContextProvider, ErrorHandlingContextProvider, ExportDataContext, FilterBarProvider, GeolocationProvider, ModalDialogContextProvider, NavigationContextProvider, OemBrandingContextProvider, TimeRangeProvider, TokenProvider, CurrentUserProvider, CurrentUserPreferenceProvider, UserSubscriptionProvider, WidgetConfigProvider } from '@trackunit/react-core-contexts-api';
13
13
  import { registerTranslations, initializeTranslationsForApp } from '@trackunit/i18n-library-translation';
14
14
  import { Spinner } from '@trackunit/react-components';
15
- import { SortOrder, AssetSortByProperty } from '@trackunit/iris-app-runtime-core-api';
15
+ import { Channels, SortOrder, AssetSortByProperty } from '@trackunit/iris-app-runtime-core-api';
16
16
 
17
17
  /**
18
18
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
19
19
  */
20
- const createErrorLink = ({ errorHandler, token, }) => {
20
+ const createErrorLink = ({ errorHandler, getToken, }) => {
21
21
  return onError(({ graphQLErrors, networkError, operation, forward }) => {
22
22
  if (networkError) {
23
23
  // We skip the error logging if the error is an AbortError
@@ -65,7 +65,7 @@ const createErrorLink = ({ errorHandler, token, }) => {
65
65
  x.message.includes("Invalid token specified") ||
66
66
  x.message.includes("Access denied! You need to be authorized to perform this action!"));
67
67
  });
68
- if (invalidToken && token) {
68
+ if (invalidToken && getToken()) {
69
69
  errorHandler.captureException(new Error(JSON.stringify({
70
70
  category: "GraphQL",
71
71
  info: "GraphQL Error - invalidToken",
@@ -101,6 +101,69 @@ const createErrorLink = ({ errorHandler, token, }) => {
101
101
  });
102
102
  };
103
103
 
104
+ /**
105
+ * Wraps an SSE subscription link and provides the same error monitoring as the
106
+ * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
107
+ * and UNAUTHENTICATED codes — for long-lived subscription Observables.
108
+ */
109
+ const createSubscriptionErrorLink = ({ errorHandler, getToken, }) => {
110
+ return new ApolloLink((operation, forward) => {
111
+ return new Observable(observer => {
112
+ const subscription = forward(operation).subscribe({
113
+ next: response => {
114
+ const { errors } = response;
115
+ if (errors) {
116
+ const code = errors[0]?.extensions?.code;
117
+ if (code === "FORCE_RELOAD_BROWSER") {
118
+ window.location.reload();
119
+ }
120
+ const traceIds = [];
121
+ errors.forEach(error => {
122
+ if ("extensions" in error && error.extensions && typeof error.extensions.traceId === "string") {
123
+ traceIds.push(error.extensions.traceId);
124
+ }
125
+ });
126
+ if (traceIds.length) {
127
+ errorHandler.setTag("traceIds", traceIds.join(", "));
128
+ }
129
+ errorHandler.addBreadcrumb({
130
+ category: "GraphQL",
131
+ message: "GraphQL Subscription Error",
132
+ level: "error",
133
+ data: { log: JSON.stringify(errors) },
134
+ });
135
+ const invalidToken = errors.some(x => x.extensions?.code === "UNAUTHENTICATED" ||
136
+ x.message.includes("Invalid token specified") ||
137
+ x.message.includes("Access denied! You need to be authorized to perform this action!"));
138
+ if (invalidToken && getToken()) {
139
+ errorHandler.captureException(new Error(JSON.stringify({
140
+ category: "GraphQL",
141
+ info: "GraphQL Subscription Error - invalidToken",
142
+ level: "warning",
143
+ data: { log: JSON.stringify(errors) },
144
+ })), { level: "warning", fingerprint: ["GraphQL Subscription Error - invalidToken"] });
145
+ }
146
+ }
147
+ observer.next(response);
148
+ },
149
+ error: err => {
150
+ // eslint-disable-next-line no-console
151
+ console.error(err);
152
+ errorHandler.addBreadcrumb({
153
+ category: "GraphQL",
154
+ message: "GraphQL Subscription Network Error",
155
+ level: "error",
156
+ data: { log: String(err) },
157
+ });
158
+ observer.error(err);
159
+ },
160
+ complete: () => observer.complete(),
161
+ });
162
+ return () => subscription.unsubscribe();
163
+ });
164
+ });
165
+ };
166
+
104
167
  // Use a widened `string` key so TypeScript picks the `unknown` overload of
105
168
  // `Reflect.get` instead of the typed one — otherwise reads like `module`
106
169
  // resolve to `NodeModule` from @types/node.
@@ -168,11 +231,12 @@ const isInternalGqlContext = () => {
168
231
  return Reflect.get(globalThis, "gql") === "internal";
169
232
  };
170
233
 
171
- const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders, firstToken, errorHandler, }) => {
172
- let token;
173
- if (!token) {
174
- token = firstToken;
175
- }
234
+ /**
235
+ * @internal
236
+ */
237
+ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }) => {
238
+ let token = firstToken;
239
+ let tracingHeaders = initialTracingHeaders;
176
240
  const publicGraphQLLink = createHttpLink({
177
241
  uri: request => graphqlPublicUrl + "/" + request.operationName,
178
242
  });
@@ -182,7 +246,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
182
246
  const reportGraphQLLink = createHttpLink({
183
247
  uri: request => graphqlReportUrl + "/" + request.operationName,
184
248
  });
185
- const authLink = setContext(async (_, { headers: existingHeaders }) => {
249
+ const authLink = setContext((_, { headers: existingHeaders }) => {
186
250
  return {
187
251
  headers: {
188
252
  ...existingHeaders,
@@ -190,7 +254,7 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
190
254
  },
191
255
  };
192
256
  });
193
- const errorLink = createErrorLink({ errorHandler, token });
257
+ const errorLink = createErrorLink({ errorHandler, getToken: () => token });
194
258
  const defaultOptions = {
195
259
  watchQuery: {
196
260
  fetchPolicy: "no-cache",
@@ -207,12 +271,14 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
207
271
  };
208
272
  const removeTypenameLink = removeTypenameFromVariables();
209
273
  class SSELink extends ApolloLink {
274
+ /** @inheritdoc */
210
275
  constructor(options) {
211
276
  super();
212
277
  this.client = createClient(options);
213
278
  }
279
+ /** @inheritdoc */
214
280
  request(operation) {
215
- return new Observable(sink => {
281
+ return new Observable$1(sink => {
216
282
  return this.client.subscribe({ ...operation, query: print(operation.query) }, {
217
283
  next: value => sink.next(value),
218
284
  complete: sink.complete.bind(sink),
@@ -226,12 +292,13 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
226
292
  headers: () => generateHeaders(token, tracingHeaders),
227
293
  });
228
294
  // Split links based on operation type
295
+ const subscriptionErrorLink = createSubscriptionErrorLink({ errorHandler, getToken: () => token });
229
296
  const splitLink = from([
230
297
  authLink,
231
298
  split(({ query }) => {
232
299
  const definition = getMainDefinition(query);
233
300
  return definition.kind === "OperationDefinition" && definition.operation === "subscription";
234
- }, sseLink, from([
301
+ }, from([subscriptionErrorLink, sseLink]), from([
235
302
  errorLink,
236
303
  removeTypenameLink,
237
304
  split(operation => operation.getContext().clientName === "report", reportGraphQLLink, split(() => isInternalGqlContext(), internalGraphQLLink, publicGraphQLLink)),
@@ -259,8 +326,12 @@ const createApolloClient = ({ graphqlPublicUrl, graphqlInternalUrl, graphqlRepor
259
326
  getToken: () => {
260
327
  return token;
261
328
  },
329
+ setTracingHeaders: (newHeaders) => {
330
+ tracingHeaders = newHeaders;
331
+ },
262
332
  };
263
333
  };
334
+
264
335
  const useApolloClient = () => {
265
336
  const { graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, environment, tracingHeaders } = useEnvironment();
266
337
  const { token: currentToken } = useToken();
@@ -279,11 +350,17 @@ const useApolloClient = () => {
279
350
  errorHandler,
280
351
  });
281
352
  });
282
- useMemo(() => {
283
- if (client.getToken() !== currentToken) {
284
- client.setToken(currentToken);
285
- }
286
- }, [client, currentToken]);
353
+ // Synchronously propagate the token into the Apollo client closure during render
354
+ // so that child components' effects (useLayoutEffect / useEffect) always see the
355
+ // current token when they fire their first request in the same commit.
356
+ // React runs child effects before parent effects, so a parent useEffect here would
357
+ // be too late — child queries would go out with the previous token for one commit.
358
+ if (client.getToken() !== currentToken) {
359
+ client.setToken(currentToken);
360
+ }
361
+ useEffect(() => {
362
+ client.setTracingHeaders(tracingHeaders);
363
+ }, [client, tracingHeaders]);
287
364
  return client;
288
365
  };
289
366
  /**
@@ -350,16 +427,57 @@ const AnalyticsProviderIrisApp = ({ children }) => {
350
427
  };
351
428
 
352
429
  /**
353
- * Subscribe to methods initiated by changes in the host
430
+ * Subscribe to methods initiated by changes in the host.
431
+ *
432
+ * Handlers are registered on the singleton `host-runtime` penpal channel via
433
+ * {@link registerHostChangeHandler}, so push events from the host
434
+ * (`onTokenChanged`, `onTimeRangeChanged`, filter-bar updates, …) reach every
435
+ * mounted provider in the iris-app without forcing the host to open one
436
+ * penpal connection per subscription channel.
354
437
  *
355
- * @param methods STABLE object with the methods you'd want to subscribe to. Wrap in useMemo for instance to make it stable
438
+ * @param methods STABLE object with the methods you'd want to subscribe to.
439
+ * Wrap in `useMemo` (or build with stable callback references) so the
440
+ * effect doesn't re-register on every render.
441
+ * @param _channel Deprecated. Pre-v7 builds used a dedicated penpal channel
442
+ * per subscription provider (e.g. `token-subscription`); v7 collapses all
443
+ * subscriptions onto the singleton's `host-runtime` channel through a
444
+ * handler registry. The argument is accepted for source-compatibility with
445
+ * the v6 API and is ignored.
356
446
  */
357
- const useSubscribeToHostChanges = (methods) => {
447
+ const useSubscribeToHostChanges = (methods, _channel) => {
358
448
  useEffect(() => {
359
- const connection = setupHostConnector(methods);
360
- return () => {
361
- connection.destroy();
362
- };
449
+ const unregisters = [];
450
+ // Listed explicitly (rather than via `Object.entries`) so each call to
451
+ // `registerHostChangeHandler<K>` keeps the K → handler-type connection,
452
+ // which a runtime iteration over keys would erase.
453
+ if (methods.onTokenChanged) {
454
+ unregisters.push(registerHostChangeHandler("onTokenChanged", methods.onTokenChanged));
455
+ }
456
+ if (methods.onTimeRangeChanged) {
457
+ unregisters.push(registerHostChangeHandler("onTimeRangeChanged", methods.onTimeRangeChanged));
458
+ }
459
+ if (methods.onFilterBarValuesChanged) {
460
+ unregisters.push(registerHostChangeHandler("onFilterBarValuesChanged", methods.onFilterBarValuesChanged));
461
+ }
462
+ if (methods.onAssetsFilterBarValuesChanged) {
463
+ unregisters.push(registerHostChangeHandler("onAssetsFilterBarValuesChanged", methods.onAssetsFilterBarValuesChanged));
464
+ }
465
+ if (methods.onCustomersFilterBarValuesChanged) {
466
+ unregisters.push(registerHostChangeHandler("onCustomersFilterBarValuesChanged", methods.onCustomersFilterBarValuesChanged));
467
+ }
468
+ if (methods.onSitesFilterBarValuesChanged) {
469
+ unregisters.push(registerHostChangeHandler("onSitesFilterBarValuesChanged", methods.onSitesFilterBarValuesChanged));
470
+ }
471
+ if (methods.onTablePersistenceStateChanged) {
472
+ unregisters.push(registerHostChangeHandler("onTablePersistenceStateChanged", methods.onTablePersistenceStateChanged));
473
+ }
474
+ if (methods.onAssetSortingStateChanged) {
475
+ unregisters.push(registerHostChangeHandler("onAssetSortingStateChanged", methods.onAssetSortingStateChanged));
476
+ }
477
+ if (methods.onWidgetPollIntervalChanged) {
478
+ unregisters.push(registerHostChangeHandler("onWidgetPollIntervalChanged", methods.onWidgetPollIntervalChanged));
479
+ }
480
+ return () => unregisters.forEach(unregister => unregister());
363
481
  }, [methods]);
364
482
  };
365
483
 
@@ -376,7 +494,7 @@ const AssetSortingProviderIrisApp = ({ children }) => {
376
494
  const methods = useMemo(() => ({
377
495
  onAssetSortingStateChanged: setAssetSortingState,
378
496
  }), [setAssetSortingState]);
379
- useSubscribeToHostChanges(methods);
497
+ useSubscribeToHostChanges(methods, Channels.AssetSortingSubscription);
380
498
  const contextValue = useMemo(() => ({
381
499
  sortingState: assetSortingState ?? {
382
500
  sortBy: AssetSortByProperty.Criticality,
@@ -491,7 +609,7 @@ const FilterBarProviderIrisApp = ({ children }) => {
491
609
  setSitesFilterBarValues(values);
492
610
  },
493
611
  }), []);
494
- useSubscribeToHostChanges(methods);
612
+ useSubscribeToHostChanges(methods, Channels.FilterBarSubscription);
495
613
  const isLoading = useMemo(() => assetsFilterBarValues === undefined ||
496
614
  customersFilterBarValues === undefined ||
497
615
  sitesFilterBarValues === undefined, [assetsFilterBarValues, customersFilterBarValues, sitesFilterBarValues]);
@@ -594,7 +712,7 @@ const TimeRangeProviderIrisApp = ({ children }) => {
594
712
  }));
595
713
  },
596
714
  }), []);
597
- useSubscribeToHostChanges(methods);
715
+ useSubscribeToHostChanges(methods, Channels.TimeRangeSubscription);
598
716
  if (!timeRangeContext) {
599
717
  return null;
600
718
  }
@@ -632,7 +750,7 @@ const TokenProviderIrisApp = ({ children }) => {
632
750
  const methods = useMemo(() => ({
633
751
  onTokenChanged,
634
752
  }), [onTokenChanged]);
635
- useSubscribeToHostChanges(methods);
753
+ useSubscribeToHostChanges(methods, Channels.TokenSubscription);
636
754
  if (state.status === "error") {
637
755
  throw new Error("Token context is invalid");
638
756
  }
@@ -760,7 +878,7 @@ const WidgetConfigProviderIrisApp = ({ children }) => {
760
878
  const methods = useMemo(() => ({
761
879
  onWidgetPollIntervalChanged: setPollInterval,
762
880
  }), [setPollInterval]);
763
- useSubscribeToHostChanges(methods);
881
+ useSubscribeToHostChanges(methods, Channels.WidgetConfigSubscription);
764
882
  const widgetConfigContextValue = useMemo(() => ({
765
883
  getData: WidgetConfigRuntime.getWidgetData,
766
884
  setData: WidgetConfigRuntime.setWidgetData,
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@trackunit/react-core-contexts",
3
- "version": "1.30.3",
3
+ "version": "1.30.5-alpha-d37885bfce4.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/iris-app-api": "1.20.7",
11
- "@trackunit/iris-app-runtime-core-api": "1.16.6",
12
- "@trackunit/react-core-hooks": "1.17.10",
13
- "@trackunit/i18n-library-translation": "1.22.1",
14
- "@trackunit/react-components": "1.26.3",
15
- "@trackunit/iris-app-runtime-core": "1.17.6",
10
+ "@trackunit/iris-app-api": "1.20.9-alpha-d37885bfce4.0",
11
+ "@trackunit/iris-app-runtime-core-api": "1.16.8-alpha-d37885bfce4.0",
12
+ "@trackunit/react-core-hooks": "1.17.12-alpha-d37885bfce4.0",
13
+ "@trackunit/i18n-library-translation": "1.22.3-alpha-d37885bfce4.0",
14
+ "@trackunit/react-components": "1.26.5-alpha-d37885bfce4.0",
15
+ "@trackunit/iris-app-runtime-core": "1.17.8-alpha-d37885bfce4.0",
16
16
  "graphql-sse": "^2.5.4",
17
- "@trackunit/react-core-contexts-api": "1.17.6"
17
+ "@trackunit/react-core-contexts-api": "1.17.8-alpha-d37885bfce4.0"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@apollo/client": "^3.13.8",
@@ -0,0 +1,19 @@
1
+ import { ApolloClient } from "@apollo/client";
2
+ import { ErrorHandlingContextValue, TracingHeaders } from "@trackunit/iris-app-runtime-core-api";
3
+ /**
4
+ * @internal
5
+ */
6
+ export declare const createApolloClient: ({ graphqlPublicUrl, graphqlInternalUrl, graphqlReportUrl, isDev, tracingHeaders: initialTracingHeaders, firstToken, errorHandler, }: {
7
+ graphqlPublicUrl: string;
8
+ graphqlInternalUrl: string;
9
+ graphqlReportUrl: string;
10
+ tracingHeaders: TracingHeaders;
11
+ isDev: boolean;
12
+ firstToken?: string;
13
+ errorHandler: ErrorHandlingContextValue;
14
+ }) => {
15
+ client: ApolloClient<import("@apollo/client").NormalizedCacheObject>;
16
+ setToken: (newToken: string | undefined) => void;
17
+ getToken: () => string | undefined;
18
+ setTracingHeaders: (newHeaders: TracingHeaders) => void;
19
+ };
@@ -3,7 +3,7 @@ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api"
3
3
  /**
4
4
  * This error link is used to capture error information, i. e. traceId, graphQL errors, network errors, etc.
5
5
  */
6
- export declare const createErrorLink: ({ errorHandler, token, }: {
6
+ export declare const createErrorLink: ({ errorHandler, getToken, }: {
7
7
  errorHandler: ErrorHandlingContextValue;
8
- token: string | undefined;
8
+ getToken: () => string | undefined;
9
9
  }) => ApolloLink;
@@ -0,0 +1,11 @@
1
+ import { ApolloLink } from "@apollo/client";
2
+ import { ErrorHandlingContextValue } from "@trackunit/iris-app-runtime-core-api";
3
+ /**
4
+ * Wraps an SSE subscription link and provides the same error monitoring as the
5
+ * HTTP error link — capturing GraphQL errors, traceIds, FORCE_RELOAD_BROWSER,
6
+ * and UNAUTHENTICATED codes — for long-lived subscription Observables.
7
+ */
8
+ export declare const createSubscriptionErrorLink: ({ errorHandler, getToken, }: {
9
+ errorHandler: ErrorHandlingContextValue;
10
+ getToken: () => string | undefined;
11
+ }) => ApolloLink;
@@ -1,9 +1,20 @@
1
- import { setupHostConnector } from "@trackunit/iris-app-runtime-core";
2
- type ChildConnectorApi = Parameters<typeof setupHostConnector>[0];
1
+ import { ChildConnectorApi } from "@trackunit/iris-app-runtime-core-api";
3
2
  /**
4
- * Subscribe to methods initiated by changes in the host
3
+ * Subscribe to methods initiated by changes in the host.
5
4
  *
6
- * @param methods STABLE object with the methods you'd want to subscribe to. Wrap in useMemo for instance to make it stable
5
+ * Handlers are registered on the singleton `host-runtime` penpal channel via
6
+ * {@link registerHostChangeHandler}, so push events from the host
7
+ * (`onTokenChanged`, `onTimeRangeChanged`, filter-bar updates, …) reach every
8
+ * mounted provider in the iris-app without forcing the host to open one
9
+ * penpal connection per subscription channel.
10
+ *
11
+ * @param methods STABLE object with the methods you'd want to subscribe to.
12
+ * Wrap in `useMemo` (or build with stable callback references) so the
13
+ * effect doesn't re-register on every render.
14
+ * @param _channel Deprecated. Pre-v7 builds used a dedicated penpal channel
15
+ * per subscription provider (e.g. `token-subscription`); v7 collapses all
16
+ * subscriptions onto the singleton's `host-runtime` channel through a
17
+ * handler registry. The argument is accepted for source-compatibility with
18
+ * the v6 API and is ignored.
7
19
  */
8
- export declare const useSubscribeToHostChanges: (methods: ChildConnectorApi) => void;
9
- export {};
20
+ export declare const useSubscribeToHostChanges: (methods: Partial<ChildConnectorApi>, _channel?: string) => void;