@gears-frontx/react 0.2.0-alpha.0 → 0.2.0-alpha.3

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/testing.cjs CHANGED
@@ -38,19 +38,22 @@ __export(testing_exports, {
38
38
  module.exports = __toCommonJS(testing_exports);
39
39
 
40
40
  // src/queryClient.tsx
41
- var import_react = __toESM(require("react"), 1);
41
+ var import_react2 = __toESM(require("react"), 1);
42
42
  var import_react_query = require("@tanstack/react-query");
43
43
  var import_framework2 = require("@gears-frontx/framework");
44
44
 
45
45
  // src/hooks/QueryCache.ts
46
46
  var import_framework = require("@gears-frontx/framework");
47
47
 
48
+ // src/hooks/useLatest.ts
49
+ var import_react = require("react");
50
+
48
51
  // src/queryClient.tsx
49
52
  var import_jsx_runtime = require("react/jsx-runtime");
50
53
  var APP_QUERY_CLIENT_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client");
51
54
  var APP_QUERY_CLIENT_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-resolver");
52
- var FrontXQueryClientContext = (0, import_react.createContext)(void 0);
53
- var useCommitEffect = typeof globalThis.window === "undefined" ? import_react.useEffect : import_react.useLayoutEffect;
55
+ var FrontXQueryClientContext = (0, import_react2.createContext)(void 0);
56
+ var useCommitEffect = typeof globalThis.window === "undefined" ? import_react2.useEffect : import_react2.useLayoutEffect;
54
57
  function resolveFrontXQueryClient(app) {
55
58
  const clientApp = app;
56
59
  return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();
@@ -59,7 +62,7 @@ function bootstrapFrontXQueryClient(app) {
59
62
  return resolveFrontXQueryClient(app);
60
63
  }
61
64
  function useOptionalFrontXQueryClient() {
62
- return (0, import_react.useContext)(FrontXQueryClientContext);
65
+ return (0, import_react2.useContext)(FrontXQueryClientContext);
63
66
  }
64
67
 
65
68
  // src/testing.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts","../src/queryClient.tsx","../src/hooks/QueryCache.ts"],"sourcesContent":["/**\n * Test-only entry for @gears-frontx/react.\n *\n * Utilities here are intended for Vitest (or similar) suites that need access\n * to query-client wiring without importing package internals directly.\n */\n\n// @cpt-dod:cpt-frontx-dod-react-bindings-provider:p1\n\n// @cpt-begin:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\nexport {\n bootstrapFrontXQueryClient,\n resolveFrontXQueryClient,\n useOptionalFrontXQueryClient,\n} from './queryClient';\n\n/** App-layer tests must import this from `@gears-frontx/react/testing`, not `@gears-frontx/framework/testing`. */\nexport { describeBootstrapMfeContract } from '@gears-frontx/framework/testing';\nexport type {\n BootstrapMfeResolveArgs,\n BootstrapMfeTestSpecOptions,\n} from '@gears-frontx/framework/testing';\n// @cpt-end:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\n","import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n QueryClientContext,\n QueryClientProvider,\n type QueryClient,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseInfiniteQuery,\n useSuspenseQuery,\n type MutationFunctionContext,\n} from '@tanstack/react-query';\nimport {\n subscribeQueryCacheRuntimeChanged,\n type EndpointDescriptor,\n type FrontXApp,\n} from '@gears-frontx/framework';\nimport type { ApiQueryOverrides } from './hooks/useApiQuery';\nimport type {\n ApiInfiniteQueryOptions,\n ApiInfiniteQueryPageContext,\n} from './hooks/useApiInfiniteQuery';\nimport type { UseApiMutationOptions } from './hooks/useApiMutation';\nimport { createQueryCache, type MutationCallbackContext, type QueryCacheKey } from './hooks/QueryCache';\n\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-query-provider:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-query:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\nconst APP_QUERY_CLIENT_SYMBOL = Symbol.for('frontx:query-cache:app-client');\nconst APP_QUERY_CLIENT_RESOLVER_SYMBOL = Symbol.for('frontx:query-cache:app-client-resolver');\nconst APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = Symbol.for('frontx:query-cache:app-client-activator');\n\ntype QueryClientApp = FrontXApp & {\n [APP_QUERY_CLIENT_SYMBOL]?: QueryClient;\n [APP_QUERY_CLIENT_RESOLVER_SYMBOL]?: () => QueryClient | undefined;\n [APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?: () => QueryClient | undefined;\n};\n\nconst FrontXQueryClientContext = createContext<QueryClient | undefined>(undefined);\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\ntype RuntimeAwareEndpointDescriptor<TData> = EndpointDescriptor<TData> & {\n fetch(options?: { signal?: AbortSignal; staleTime?: number }): Promise<TData>;\n};\n\ntype AppResolvedQueryClient = {\n app: FrontXApp;\n queryClient: QueryClient;\n};\n\nconst useCommitEffect = typeof globalThis.window === 'undefined' ? useEffect : useLayoutEffect;\n\nexport function resolveFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();\n}\n\nexport function hasFrontXQueryClientActivator(app: FrontXApp): boolean {\n return typeof (app as QueryClientApp)[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL] === 'function';\n}\n\nexport function activateFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?.();\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n/**\n * Imperatively invalidates cache entries for `target` on the QueryClient bound to `app`.\n * Use from non-React code (e.g. MFE actions); in components prefer `useQueryCache().invalidate`.\n */\nexport async function invalidateQueryCacheForApp(\n app: FrontXApp,\n target: EndpointDescriptor<unknown> | QueryCacheKey\n): Promise<void> {\n const queryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (!queryClient) {\n throw new Error(\n '[invalidateQueryCacheForApp] No QueryClient on app. Ensure queryCache() or queryCacheShared() is in the plugin chain.'\n );\n }\n await createQueryCache(queryClient).invalidate(target);\n}\n\n/** Resolves the app-bound QueryClient without triggering plugin activation during render. */\nexport function bootstrapFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n return resolveFrontXQueryClient(app);\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\nexport function useBootstrappedFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const fromApp = useMemo(() => bootstrapFrontXQueryClient(app), [app]);\n const [eventResolvedClient, setEventResolvedClient] = useState<AppResolvedQueryClient | undefined>(\n undefined\n );\n const queryClient =\n fromApp ?? (eventResolvedClient?.app === app ? eventResolvedClient.queryClient : undefined);\n\n useCommitEffect(() => {\n if (fromApp || !hasFrontXQueryClientActivator(app)) {\n return;\n }\n\n const tryResolveQueryClient = () => {\n const nextQueryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (nextQueryClient) {\n setEventResolvedClient((current) => {\n if (current?.app === app && current.queryClient === nextQueryClient) {\n return current;\n }\n\n return {\n app,\n queryClient: nextQueryClient,\n };\n });\n }\n };\n\n // Only activate after commit so abandoned renders cannot leak retainers/listeners.\n tryResolveQueryClient();\n\n const subscription = subscribeQueryCacheRuntimeChanged(() => {\n tryResolveQueryClient();\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, [app, fromApp]);\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\nexport function FrontXQueryClientProvider({\n queryClient,\n children,\n}: Readonly<{\n queryClient: QueryClient | undefined;\n children: React.ReactNode;\n}>) {\n if (!queryClient) {\n return (\n <FrontXQueryClientContext.Provider value={undefined}>\n <QueryClientContext.Provider value={undefined}>{children}</QueryClientContext.Provider>\n </FrontXQueryClientContext.Provider>\n );\n }\n\n return (\n <FrontXQueryClientContext.Provider value={queryClient}>\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n </FrontXQueryClientContext.Provider>\n );\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\nexport function useOptionalFrontXQueryClient(): QueryClient | undefined {\n return useContext(FrontXQueryClientContext);\n}\n\nexport function useRequiredFrontXQueryClient(): QueryClient {\n const queryClient = useOptionalFrontXQueryClient();\n if (!queryClient) {\n throw new Error(\n '[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition.'\n );\n }\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\nfunction buildPageContext<TPage>(\n page: TPage,\n pages: readonly TPage[],\n descriptor: EndpointDescriptor<TPage>,\n descriptors: readonly EndpointDescriptor<TPage>[]\n): ApiInfiniteQueryPageContext<TPage> {\n return {\n page,\n pages,\n descriptor,\n descriptors,\n };\n}\n\nfunction normalizeFetchStaleTime(staleTime: unknown): number | undefined {\n if (staleTime === 'static') {\n return Number.POSITIVE_INFINITY;\n }\n\n return typeof staleTime === 'number' ? staleTime : undefined;\n}\n\nfunction resolveFetchStaleTime<TData>(\n queryClient: QueryClient,\n descriptor: EndpointDescriptor<TData>,\n overrideStaleTime?: number\n): number | undefined {\n return normalizeFetchStaleTime(\n overrideStaleTime ??\n descriptor.staleTime ??\n queryClient.getDefaultOptions().queries?.staleTime\n );\n}\n\nfunction fetchDescriptor<TData>(\n descriptor: EndpointDescriptor<TData>,\n options?: { signal?: AbortSignal; staleTime?: number }\n): Promise<TData> {\n return (descriptor as RuntimeAwareEndpointDescriptor<TData>).fetch(options);\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\nexport function useFrontXQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n refetch: result.refetch,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\nexport function useFrontXSuspenseQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useSuspenseQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\nexport function useFrontXInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\nexport function useFrontXSuspenseInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useSuspenseInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\nexport function useFrontXMutation<\n TData = unknown,\n TError = Error,\n TVariables = void,\n TContext = unknown,\n>(\n options: UseApiMutationOptions<TData, TError, TVariables, TContext>,\n callbackCtx: MutationCallbackContext\n) {\n useRequiredFrontXQueryClient();\n\n const latestRef = React.useRef({ options, callbackCtx });\n latestRef.current = { options, callbackCtx };\n const fetchAbortControllersRef = React.useRef<Set<AbortController>>(new Set());\n\n React.useEffect(() => {\n const fetchAbortControllers = fetchAbortControllersRef.current;\n return () => {\n if (latestRef.current.options.abortOnUnmount) {\n for (const controller of fetchAbortControllers) {\n controller.abort();\n }\n fetchAbortControllers.clear();\n }\n };\n }, []);\n\n const mutationFn = React.useCallback((variables: TVariables, context: MutationFunctionContext) => {\n const cancelOnSupersede = latestRef.current.options.cancelOnSupersede;\n if (cancelOnSupersede) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n\n const controller = new AbortController();\n fetchAbortControllersRef.current.add(controller);\n\n const librarySignal = (context as MutationFunctionContext & { signal?: AbortSignal }).signal;\n if (cancelOnSupersede && librarySignal) {\n if (librarySignal.aborted) {\n controller.abort();\n } else {\n librarySignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return latestRef.current.options.endpoint\n .fetch(variables, { signal: controller.signal })\n .finally(() => {\n fetchAbortControllersRef.current.delete(controller);\n });\n }, []);\n\n const onMutate = React.useCallback((variables: TVariables, _context: MutationFunctionContext) => {\n const current = latestRef.current;\n if (!current.options.onMutate) {\n return undefined as TContext;\n }\n\n return current.options.onMutate(variables, current.callbackCtx);\n }, []);\n\n const onSuccess = React.useCallback((data: TData, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onSuccess?.(data, variables, context, current.callbackCtx);\n }, []);\n\n const onError = React.useCallback((error: TError, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onError?.(error, variables, context, current.callbackCtx);\n }, []);\n\n const onSettled = React.useCallback((\n data: TData | undefined,\n error: TError | null,\n variables: TVariables,\n context: TContext | undefined\n ) => {\n const current = latestRef.current;\n return current.options.onSettled?.(\n data,\n error,\n variables,\n context,\n current.callbackCtx\n );\n }, []);\n\n const mutation = useMutation<TData, TError, TVariables, TContext>({\n mutationFn,\n onMutate: options.onMutate ? onMutate : undefined,\n onSuccess: options.onSuccess ? onSuccess : undefined,\n onError: options.onError ? onError : undefined,\n onSettled: options.onSettled ? onSettled : undefined,\n });\n\n return {\n mutate: mutation.mutate as (variables: TVariables) => void,\n mutateAsync: mutation.mutateAsync as (variables: TVariables) => Promise<TData>,\n isPending: mutation.isPending,\n error: mutation.error,\n data: mutation.data,\n reset: mutation.reset,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\n","/**\n * QueryCache — restricted interface for interacting with the shared QueryClient.\n *\n * MFEs and screen-set components access the cache only through this sanctioned\n * public interface. It is injected as { queryCache } into useApiMutation\n * callbacks and also returned by useQueryCache() for controlled imperative\n * cache access.\n */\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-create-query-cache\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n\nimport type { EndpointDescriptor } from '@gears-frontx/framework';\nimport { eventBus } from '@gears-frontx/framework';\nimport { type QueryClient, type QueryState } from '@tanstack/react-query';\n\nexport type QueryCacheKey = readonly unknown[];\nexport type QueryCacheRefetchMode = 'active' | 'inactive' | 'all' | 'none';\nexport type QueryCacheInvalidateTarget =\n | EndpointDescriptor<unknown>\n | QueryCacheKey\n | null\n | undefined;\n\nexport interface QueryCacheState<TData = unknown, TError = Error> {\n data: TData | undefined;\n dataUpdatedAt: number;\n error: TError | null;\n errorUpdatedAt: number;\n fetchFailureCount: number;\n fetchFailureReason: TError | null;\n fetchStatus: 'fetching' | 'paused' | 'idle';\n isInvalidated: boolean;\n status: 'pending' | 'error' | 'success';\n}\n\nexport type QueryCacheInvalidateFilters = {\n queryKey?: QueryCacheInvalidateTarget;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n};\n\ntype QueryCacheUpdater<T> = T | ((old: T | undefined) => T | undefined);\n\ntype CacheInvalidateEvent = {\n queryKey: QueryCacheKey;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n source?: string;\n};\n\ntype CacheSetEvent = {\n queryKey: QueryCacheKey;\n dataOrUpdater: QueryCacheUpdater<unknown>;\n source?: string;\n};\n\ntype CacheRemoveEvent = {\n queryKey: QueryCacheKey;\n source?: string;\n};\n\nconst QUERY_CLIENT_BROADCAST_TARGET = Symbol.for('frontx:query-cache:broadcast-target');\nconst QUERY_CLIENT_BROADCAST_COUNTER = Symbol.for('frontx:query-cache:broadcast-counter');\n\ntype QueryClientWithMetadata = QueryClient & {\n [QUERY_CLIENT_BROADCAST_TARGET]?: string;\n};\n\ntype QueryClientCounterHost = typeof globalThis & {\n [QUERY_CLIENT_BROADCAST_COUNTER]?: number;\n};\n\nfunction emitCacheEvent(event: string, payload: unknown): void {\n (eventBus.emit as (eventType: string, payload: unknown) => void)(event, payload);\n}\n\nfunction ensureBroadcastSource(queryClient: QueryClient): string {\n const clientWithMetadata = queryClient as QueryClientWithMetadata;\n const existingSource = clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET];\n if (typeof existingSource === 'string' && existingSource.length > 0) {\n return existingSource;\n }\n\n const host = globalThis as QueryClientCounterHost;\n const nextCounter = (host[QUERY_CLIENT_BROADCAST_COUNTER] ?? 0) + 1;\n host[QUERY_CLIENT_BROADCAST_COUNTER] = nextCounter;\n\n const source = `query-cache-${nextCounter}`;\n clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET] = source;\n return source;\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-resolve-key\nexport function resolveKey(target: EndpointDescriptor<unknown> | QueryCacheKey): readonly unknown[] {\n if (\n target !== null &&\n typeof target === 'object' &&\n !Array.isArray(target) &&\n 'key' in target &&\n 'fetch' in target &&\n Array.isArray(target.key) &&\n typeof target.fetch === 'function'\n ) {\n return target.key;\n }\n\n return target as readonly unknown[];\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-resolve-key\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\nexport interface QueryCache {\n get<T>(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined;\n getState<TData = unknown, TError = Error>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey\n ): QueryCacheState<TData, TError> | undefined;\n set<T>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey,\n dataOrUpdater: T | ((old: T | undefined) => T | undefined)\n ): void;\n cancel(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidate(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidateMany(filters?: QueryCacheInvalidateFilters | null): Promise<void>;\n remove(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): void;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\nexport interface MutationCallbackContext {\n queryCache: QueryCache;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\n\nfunction toQueryUpdater<T>(\n dataOrUpdater: QueryCacheUpdater<T>\n): (old: T | undefined) => T | undefined {\n if (typeof dataOrUpdater === 'function') {\n return dataOrUpdater as (old: T | undefined) => T | undefined;\n }\n\n return () => dataOrUpdater;\n}\n\nfunction mapQueryState<TData, TError = Error>(\n state: QueryState<TData, TError>\n): QueryCacheState<TData, TError> {\n return {\n data: state.data,\n dataUpdatedAt: state.dataUpdatedAt,\n error: state.error,\n errorUpdatedAt: state.errorUpdatedAt,\n fetchFailureCount: state.fetchFailureCount,\n fetchFailureReason: state.fetchFailureReason,\n fetchStatus: state.fetchStatus,\n isInvalidated: state.isInvalidated,\n status: state.status,\n };\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-create-query-cache\nexport function createQueryCache(queryClient: QueryClient): QueryCache {\n return {\n get: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined => {\n return queryClient.getQueryData<T>(resolveKey(key));\n },\n getState: <TData = unknown, TError = Error>(key: EndpointDescriptor<unknown> | QueryCacheKey) => {\n const state = queryClient.getQueryState<TData, TError>(resolveKey(key));\n return state ? mapQueryState(state) : undefined;\n },\n set: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey, dataOrUpdater: QueryCacheUpdater<T>): void => {\n const resolvedKey = resolveKey(key);\n queryClient.setQueryData<T>(resolvedKey, toQueryUpdater(dataOrUpdater));\n emitCacheEvent('cache/set', {\n queryKey: resolvedKey,\n dataOrUpdater: dataOrUpdater as QueryCacheUpdater<unknown>,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheSetEvent);\n },\n cancel: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n return queryClient.cancelQueries({ queryKey: resolveKey(key) });\n },\n invalidate: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n const resolvedKey = resolveKey(key);\n const result = queryClient.invalidateQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/invalidate', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n invalidateMany: (filters): Promise<void> => {\n if (filters?.queryKey == null) {\n return Promise.resolve();\n }\n\n const resolvedKey = resolveKey(filters.queryKey);\n if (!Array.isArray(resolvedKey) || resolvedKey.length === 0) {\n return Promise.resolve();\n }\n\n const payload = {\n ...filters,\n queryKey: resolvedKey,\n };\n const result = queryClient.invalidateQueries(payload);\n emitCacheEvent('cache/invalidate', {\n ...payload,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n remove: (key: EndpointDescriptor<unknown> | QueryCacheKey): void => {\n const resolvedKey = resolveKey(key);\n queryClient.removeQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/remove', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheRemoveEvent);\n },\n };\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-create-query-cache\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAOO;AACP,yBAUO;AACP,IAAAA,oBAIO;;;ACVP,uBAAyB;;;ADmJjB;AAzHR,IAAM,0BAA0B,uBAAO,IAAI,+BAA+B;AAC1E,IAAM,mCAAmC,uBAAO,IAAI,wCAAwC;AAS5F,IAAM,+BAA2B,4BAAuC,MAAS;AAajF,IAAM,kBAAkB,OAAO,WAAW,WAAW,cAAc,yBAAY;AAExE,SAAS,yBAAyB,KAAyC;AAChF,QAAM,YAAY;AAClB,SAAO,UAAU,uBAAuB,KAAK,UAAU,gCAAgC,IAAI;AAC7F;AA+BO,SAAS,2BAA2B,KAAyC;AAClF,SAAO,yBAAyB,GAAG;AACrC;AA0EO,SAAS,+BAAwD;AACtE,aAAO,yBAAW,wBAAwB;AAC5C;;;AD/JA,qBAA6C;","names":["import_framework"]}
1
+ {"version":3,"sources":["../src/testing.ts","../src/queryClient.tsx","../src/hooks/QueryCache.ts","../src/hooks/useLatest.ts"],"sourcesContent":["/**\n * Test-only entry for @gears-frontx/react.\n *\n * Utilities here are intended for Vitest (or similar) suites that need access\n * to query-client wiring without importing package internals directly.\n */\n\n// @cpt-dod:cpt-frontx-dod-react-bindings-provider:p1\n\n// @cpt-begin:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\nexport {\n bootstrapFrontXQueryClient,\n resolveFrontXQueryClient,\n useOptionalFrontXQueryClient,\n} from './queryClient';\n\n/** App-layer tests must import this from `@gears-frontx/react/testing`, not `@gears-frontx/framework/testing`. */\nexport { describeBootstrapMfeContract } from '@gears-frontx/framework/testing';\nexport type {\n BootstrapMfeResolveArgs,\n BootstrapMfeTestSpecOptions,\n} from '@gears-frontx/framework/testing';\n// @cpt-end:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\n","import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n QueryClientContext,\n QueryClientProvider,\n type QueryClient,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseInfiniteQuery,\n useSuspenseQuery,\n type MutationFunctionContext,\n} from '@tanstack/react-query';\nimport {\n subscribeQueryCacheRuntimeChanged,\n type EndpointDescriptor,\n type FrontXApp,\n} from '@gears-frontx/framework';\nimport type { ApiQueryOverrides } from './hooks/useApiQuery';\nimport type {\n ApiInfiniteQueryOptions,\n ApiInfiniteQueryPageContext,\n} from './hooks/useApiInfiniteQuery';\nimport type { UseApiMutationOptions } from './hooks/useApiMutation';\nimport { createQueryCache, type MutationCallbackContext, type QueryCacheKey } from './hooks/QueryCache';\nimport { useLatest } from './hooks/useLatest';\n\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-query-provider:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-query:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\nconst APP_QUERY_CLIENT_SYMBOL = Symbol.for('frontx:query-cache:app-client');\nconst APP_QUERY_CLIENT_RESOLVER_SYMBOL = Symbol.for('frontx:query-cache:app-client-resolver');\nconst APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = Symbol.for('frontx:query-cache:app-client-activator');\n\ntype QueryClientApp = FrontXApp & {\n [APP_QUERY_CLIENT_SYMBOL]?: QueryClient;\n [APP_QUERY_CLIENT_RESOLVER_SYMBOL]?: () => QueryClient | undefined;\n [APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?: () => QueryClient | undefined;\n};\n\nconst FrontXQueryClientContext = createContext<QueryClient | undefined>(undefined);\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\ntype RuntimeAwareEndpointDescriptor<TData> = EndpointDescriptor<TData> & {\n fetch(options?: { signal?: AbortSignal; staleTime?: number }): Promise<TData>;\n};\n\ntype AppResolvedQueryClient = {\n app: FrontXApp;\n queryClient: QueryClient;\n};\n\nconst useCommitEffect = typeof globalThis.window === 'undefined' ? useEffect : useLayoutEffect;\n\nexport function resolveFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();\n}\n\nexport function hasFrontXQueryClientActivator(app: FrontXApp): boolean {\n return typeof (app as QueryClientApp)[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL] === 'function';\n}\n\nexport function activateFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?.();\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n/**\n * Imperatively invalidates cache entries for `target` on the QueryClient bound to `app`.\n * Use from non-React code (e.g. MFE actions); in components prefer `useQueryCache().invalidate`.\n */\nexport async function invalidateQueryCacheForApp(\n app: FrontXApp,\n target: EndpointDescriptor<unknown> | QueryCacheKey\n): Promise<void> {\n const queryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (!queryClient) {\n throw new Error(\n '[invalidateQueryCacheForApp] No QueryClient on app. Ensure queryCache() or queryCacheShared() is in the plugin chain.'\n );\n }\n await createQueryCache(queryClient).invalidate(target);\n}\n\n/** Resolves the app-bound QueryClient without triggering plugin activation during render. */\nexport function bootstrapFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n return resolveFrontXQueryClient(app);\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\nexport function useBootstrappedFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const fromApp = useMemo(() => bootstrapFrontXQueryClient(app), [app]);\n const [eventResolvedClient, setEventResolvedClient] = useState<AppResolvedQueryClient | undefined>(\n undefined\n );\n const queryClient =\n fromApp ?? (eventResolvedClient?.app === app ? eventResolvedClient.queryClient : undefined);\n\n useCommitEffect(() => {\n if (fromApp || !hasFrontXQueryClientActivator(app)) {\n return;\n }\n\n const tryResolveQueryClient = () => {\n const nextQueryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (nextQueryClient) {\n setEventResolvedClient((current) => {\n if (current?.app === app && current.queryClient === nextQueryClient) {\n return current;\n }\n\n return {\n app,\n queryClient: nextQueryClient,\n };\n });\n }\n };\n\n // Only activate after commit so abandoned renders cannot leak retainers/listeners.\n tryResolveQueryClient();\n\n const subscription = subscribeQueryCacheRuntimeChanged(() => {\n tryResolveQueryClient();\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, [app, fromApp]);\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\nexport function FrontXQueryClientProvider({\n queryClient,\n children,\n}: Readonly<{\n queryClient: QueryClient | undefined;\n children: React.ReactNode;\n}>) {\n if (!queryClient) {\n return (\n <FrontXQueryClientContext.Provider value={undefined}>\n <QueryClientContext.Provider value={undefined}>{children}</QueryClientContext.Provider>\n </FrontXQueryClientContext.Provider>\n );\n }\n\n return (\n <FrontXQueryClientContext.Provider value={queryClient}>\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n </FrontXQueryClientContext.Provider>\n );\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\nexport function useOptionalFrontXQueryClient(): QueryClient | undefined {\n return useContext(FrontXQueryClientContext);\n}\n\nexport function useRequiredFrontXQueryClient(): QueryClient {\n const queryClient = useOptionalFrontXQueryClient();\n if (!queryClient) {\n throw new Error(\n '[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition.'\n );\n }\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\nfunction buildPageContext<TPage>(\n page: TPage,\n pages: readonly TPage[],\n descriptor: EndpointDescriptor<TPage>,\n descriptors: readonly EndpointDescriptor<TPage>[]\n): ApiInfiniteQueryPageContext<TPage> {\n return {\n page,\n pages,\n descriptor,\n descriptors,\n };\n}\n\nfunction normalizeFetchStaleTime(staleTime: unknown): number | undefined {\n if (staleTime === 'static') {\n return Number.POSITIVE_INFINITY;\n }\n\n return typeof staleTime === 'number' ? staleTime : undefined;\n}\n\nfunction resolveFetchStaleTime<TData>(\n queryClient: QueryClient,\n descriptor: EndpointDescriptor<TData>,\n overrideStaleTime?: number\n): number | undefined {\n return normalizeFetchStaleTime(\n overrideStaleTime ??\n descriptor.staleTime ??\n queryClient.getDefaultOptions().queries?.staleTime\n );\n}\n\nfunction fetchDescriptor<TData>(\n descriptor: EndpointDescriptor<TData>,\n options?: { signal?: AbortSignal; staleTime?: number }\n): Promise<TData> {\n return (descriptor as RuntimeAwareEndpointDescriptor<TData>).fetch(options);\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\nexport function useFrontXQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n refetch: result.refetch,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\nexport function useFrontXSuspenseQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useSuspenseQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\nexport function useFrontXInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\nexport function useFrontXSuspenseInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useSuspenseInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\nexport function useFrontXMutation<\n TData = unknown,\n TError = Error,\n TVariables = void,\n TContext = unknown,\n>(\n options: UseApiMutationOptions<TData, TError, TVariables, TContext>,\n callbackCtx: MutationCallbackContext\n) {\n useRequiredFrontXQueryClient();\n\n const latestRef = useLatest({ options, callbackCtx });\n const fetchAbortControllersRef = React.useRef<Set<AbortController>>(new Set());\n\n // Effect event: reads abortOnUnmount from the most recent render, not\n // whatever option value was current when the unmount effect was set up.\n const abortFetchesOnUnmount = React.useEffectEvent(() => {\n if (options.abortOnUnmount) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n });\n\n React.useEffect(() => {\n return () => {\n abortFetchesOnUnmount();\n };\n }, []);\n\n const mutationFn = React.useCallback((variables: TVariables, context: MutationFunctionContext) => {\n const cancelOnSupersede = latestRef.current.options.cancelOnSupersede;\n if (cancelOnSupersede) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n\n const controller = new AbortController();\n fetchAbortControllersRef.current.add(controller);\n\n const librarySignal = (context as MutationFunctionContext & { signal?: AbortSignal }).signal;\n if (cancelOnSupersede && librarySignal) {\n if (librarySignal.aborted) {\n controller.abort();\n } else {\n librarySignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return latestRef.current.options.endpoint\n .fetch(variables, { signal: controller.signal })\n .finally(() => {\n fetchAbortControllersRef.current.delete(controller);\n });\n // latestRef's identity never changes (useLatest returns a stable ref\n // object) — listing it satisfies exhaustive-deps without recreating this\n // callback on every render.\n }, [latestRef]);\n\n const onMutate = React.useCallback((variables: TVariables, _context: MutationFunctionContext) => {\n const current = latestRef.current;\n if (!current.options.onMutate) {\n return undefined as TContext;\n }\n\n return current.options.onMutate(variables, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onSuccess = React.useCallback((data: TData, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onSuccess?.(data, variables, context, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onError = React.useCallback((error: TError, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onError?.(error, variables, context, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onSettled = React.useCallback((\n data: TData | undefined,\n error: TError | null,\n variables: TVariables,\n context: TContext | undefined\n ) => {\n const current = latestRef.current;\n return current.options.onSettled?.(\n data,\n error,\n variables,\n context,\n current.callbackCtx\n );\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const mutation = useMutation<TData, TError, TVariables, TContext>({\n mutationFn,\n onMutate: options.onMutate ? onMutate : undefined,\n onSuccess: options.onSuccess ? onSuccess : undefined,\n onError: options.onError ? onError : undefined,\n onSettled: options.onSettled ? onSettled : undefined,\n });\n\n return {\n mutate: mutation.mutate as (variables: TVariables) => void,\n mutateAsync: mutation.mutateAsync as (variables: TVariables) => Promise<TData>,\n isPending: mutation.isPending,\n error: mutation.error,\n data: mutation.data,\n reset: mutation.reset,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\n","/**\n * QueryCache — restricted interface for interacting with the shared QueryClient.\n *\n * MFEs and screen-set components access the cache only through this sanctioned\n * public interface. It is injected as { queryCache } into useApiMutation\n * callbacks and also returned by useQueryCache() for controlled imperative\n * cache access.\n */\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-create-query-cache\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n\nimport type { EndpointDescriptor } from '@gears-frontx/framework';\nimport { eventBus } from '@gears-frontx/framework';\nimport { type QueryClient, type QueryState } from '@tanstack/react-query';\n\nexport type QueryCacheKey = readonly unknown[];\nexport type QueryCacheRefetchMode = 'active' | 'inactive' | 'all' | 'none';\nexport type QueryCacheInvalidateTarget =\n | EndpointDescriptor<unknown>\n | QueryCacheKey\n | null\n | undefined;\n\nexport interface QueryCacheState<TData = unknown, TError = Error> {\n data: TData | undefined;\n dataUpdatedAt: number;\n error: TError | null;\n errorUpdatedAt: number;\n fetchFailureCount: number;\n fetchFailureReason: TError | null;\n fetchStatus: 'fetching' | 'paused' | 'idle';\n isInvalidated: boolean;\n status: 'pending' | 'error' | 'success';\n}\n\nexport type QueryCacheInvalidateFilters = {\n queryKey?: QueryCacheInvalidateTarget;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n};\n\ntype QueryCacheUpdater<T> = T | ((old: T | undefined) => T | undefined);\n\ntype CacheInvalidateEvent = {\n queryKey: QueryCacheKey;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n source?: string;\n};\n\ntype CacheSetEvent = {\n queryKey: QueryCacheKey;\n dataOrUpdater: QueryCacheUpdater<unknown>;\n source?: string;\n};\n\ntype CacheRemoveEvent = {\n queryKey: QueryCacheKey;\n source?: string;\n};\n\nconst QUERY_CLIENT_BROADCAST_TARGET = Symbol.for('frontx:query-cache:broadcast-target');\nconst QUERY_CLIENT_BROADCAST_COUNTER = Symbol.for('frontx:query-cache:broadcast-counter');\n\ntype QueryClientWithMetadata = QueryClient & {\n [QUERY_CLIENT_BROADCAST_TARGET]?: string;\n};\n\ntype QueryClientCounterHost = typeof globalThis & {\n [QUERY_CLIENT_BROADCAST_COUNTER]?: number;\n};\n\nfunction emitCacheEvent(event: string, payload: unknown): void {\n (eventBus.emit as (eventType: string, payload: unknown) => void)(event, payload);\n}\n\nfunction ensureBroadcastSource(queryClient: QueryClient): string {\n const clientWithMetadata = queryClient as QueryClientWithMetadata;\n const existingSource = clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET];\n if (typeof existingSource === 'string' && existingSource.length > 0) {\n return existingSource;\n }\n\n const host = globalThis as QueryClientCounterHost;\n const nextCounter = (host[QUERY_CLIENT_BROADCAST_COUNTER] ?? 0) + 1;\n host[QUERY_CLIENT_BROADCAST_COUNTER] = nextCounter;\n\n const source = `query-cache-${nextCounter}`;\n clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET] = source;\n return source;\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-resolve-key\nexport function resolveKey(target: EndpointDescriptor<unknown> | QueryCacheKey): readonly unknown[] {\n if (\n target !== null &&\n typeof target === 'object' &&\n !Array.isArray(target) &&\n 'key' in target &&\n 'fetch' in target &&\n Array.isArray(target.key) &&\n typeof target.fetch === 'function'\n ) {\n return target.key;\n }\n\n return target as readonly unknown[];\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-resolve-key\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\nexport interface QueryCache {\n get<T>(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined;\n getState<TData = unknown, TError = Error>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey\n ): QueryCacheState<TData, TError> | undefined;\n set<T>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey,\n dataOrUpdater: T | ((old: T | undefined) => T | undefined)\n ): void;\n cancel(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidate(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidateMany(filters?: QueryCacheInvalidateFilters | null): Promise<void>;\n remove(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): void;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\nexport interface MutationCallbackContext {\n queryCache: QueryCache;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\n\nfunction toQueryUpdater<T>(\n dataOrUpdater: QueryCacheUpdater<T>\n): (old: T | undefined) => T | undefined {\n if (typeof dataOrUpdater === 'function') {\n return dataOrUpdater as (old: T | undefined) => T | undefined;\n }\n\n return () => dataOrUpdater;\n}\n\nfunction mapQueryState<TData, TError = Error>(\n state: QueryState<TData, TError>\n): QueryCacheState<TData, TError> {\n return {\n data: state.data,\n dataUpdatedAt: state.dataUpdatedAt,\n error: state.error,\n errorUpdatedAt: state.errorUpdatedAt,\n fetchFailureCount: state.fetchFailureCount,\n fetchFailureReason: state.fetchFailureReason,\n fetchStatus: state.fetchStatus,\n isInvalidated: state.isInvalidated,\n status: state.status,\n };\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-create-query-cache\nexport function createQueryCache(queryClient: QueryClient): QueryCache {\n return {\n get: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined => {\n return queryClient.getQueryData<T>(resolveKey(key));\n },\n getState: <TData = unknown, TError = Error>(key: EndpointDescriptor<unknown> | QueryCacheKey) => {\n const state = queryClient.getQueryState<TData, TError>(resolveKey(key));\n return state ? mapQueryState(state) : undefined;\n },\n set: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey, dataOrUpdater: QueryCacheUpdater<T>): void => {\n const resolvedKey = resolveKey(key);\n queryClient.setQueryData<T>(resolvedKey, toQueryUpdater(dataOrUpdater));\n emitCacheEvent('cache/set', {\n queryKey: resolvedKey,\n dataOrUpdater: dataOrUpdater as QueryCacheUpdater<unknown>,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheSetEvent);\n },\n cancel: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n return queryClient.cancelQueries({ queryKey: resolveKey(key) });\n },\n invalidate: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n const resolvedKey = resolveKey(key);\n const result = queryClient.invalidateQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/invalidate', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n invalidateMany: (filters): Promise<void> => {\n if (filters?.queryKey == null) {\n return Promise.resolve();\n }\n\n const resolvedKey = resolveKey(filters.queryKey);\n if (!Array.isArray(resolvedKey) || resolvedKey.length === 0) {\n return Promise.resolve();\n }\n\n const payload = {\n ...filters,\n queryKey: resolvedKey,\n };\n const result = queryClient.invalidateQueries(payload);\n emitCacheEvent('cache/invalidate', {\n ...payload,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n remove: (key: EndpointDescriptor<unknown> | QueryCacheKey): void => {\n const resolvedKey = resolveKey(key);\n queryClient.removeQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/remove', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheRemoveEvent);\n },\n };\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-create-query-cache\n","/**\n * useLatest - keeps a ref pinned to the most recently rendered value\n *\n * A handful of hooks/components in this package need to read the current\n * value of a prop or option from inside a callback or effect-cleanup closure\n * that must NOT re-run every time that value changes (e.g. a stable\n * useCallback with `[]` deps, or a mount/unmount effect keyed on identity\n * rather than on every option). Stashing the value in a ref sidesteps the\n * closure staleness without pulling the value into the dependency array.\n *\n * The write happens inside a deps-less effect (runs after every commit)\n * rather than during render, because refs must not be mutated while\n * rendering: render can be invoked more than once per commit (React\n * StrictMode's double-invoke, and future concurrent-rendering paths that\n * may discard a render), so a render-time write can leave the ref out of\n * sync with what was actually committed to the DOM. A deps-less effect\n * runs exactly once per real commit, so the ref always reflects the value\n * that was last rendered.\n */\n\nimport { useEffect, useRef, type RefObject } from 'react';\n\n/**\n * Returns a ref that always holds the most recently rendered `value`.\n *\n * The ref identity is stable across renders (same object returned every\n * time), so it is safe to add to a `useCallback`/`useEffect` dependency\n * array without causing the callback/effect to be recreated.\n */\nexport function useLatest<T>(value: T): RefObject<T> {\n const ref = useRef(value);\n\n // Deps-less: must run after every render so `ref.current` never lags\n // behind the value that was just committed. See module doc for why this\n // write cannot happen during render itself.\n useEffect(() => {\n ref.current = value;\n });\n\n return ref;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,gBAOO;AACP,yBAUO;AACP,IAAAC,oBAIO;;;ACVP,uBAAyB;;;ACOzB,mBAAkD;;;AF6I1C;AAzHR,IAAM,0BAA0B,uBAAO,IAAI,+BAA+B;AAC1E,IAAM,mCAAmC,uBAAO,IAAI,wCAAwC;AAS5F,IAAM,+BAA2B,6BAAuC,MAAS;AAajF,IAAM,kBAAkB,OAAO,WAAW,WAAW,cAAc,0BAAY;AAExE,SAAS,yBAAyB,KAAyC;AAChF,QAAM,YAAY;AAClB,SAAO,UAAU,uBAAuB,KAAK,UAAU,gCAAgC,IAAI;AAC7F;AA+BO,SAAS,2BAA2B,KAAyC;AAClF,SAAO,yBAAyB,GAAG;AACrC;AA0EO,SAAS,+BAAwD;AACtE,aAAO,0BAAW,wBAAwB;AAC5C;;;ADhKA,qBAA6C;","names":["import_react","import_framework"]}
package/dist/testing.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import React, {
3
3
  createContext,
4
4
  useContext,
5
- useEffect,
5
+ useEffect as useEffect2,
6
6
  useLayoutEffect,
7
7
  useMemo,
8
8
  useState
@@ -23,12 +23,15 @@ import {
23
23
  // src/hooks/QueryCache.ts
24
24
  import { eventBus } from "@gears-frontx/framework";
25
25
 
26
+ // src/hooks/useLatest.ts
27
+ import { useEffect, useRef } from "react";
28
+
26
29
  // src/queryClient.tsx
27
30
  import { jsx } from "react/jsx-runtime";
28
31
  var APP_QUERY_CLIENT_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client");
29
32
  var APP_QUERY_CLIENT_RESOLVER_SYMBOL = /* @__PURE__ */ Symbol.for("frontx:query-cache:app-client-resolver");
30
33
  var FrontXQueryClientContext = createContext(void 0);
31
- var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect : useLayoutEffect;
34
+ var useCommitEffect = typeof globalThis.window === "undefined" ? useEffect2 : useLayoutEffect;
32
35
  function resolveFrontXQueryClient(app) {
33
36
  const clientApp = app;
34
37
  return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/queryClient.tsx","../src/hooks/QueryCache.ts","../src/testing.ts"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n QueryClientContext,\n QueryClientProvider,\n type QueryClient,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseInfiniteQuery,\n useSuspenseQuery,\n type MutationFunctionContext,\n} from '@tanstack/react-query';\nimport {\n subscribeQueryCacheRuntimeChanged,\n type EndpointDescriptor,\n type FrontXApp,\n} from '@gears-frontx/framework';\nimport type { ApiQueryOverrides } from './hooks/useApiQuery';\nimport type {\n ApiInfiniteQueryOptions,\n ApiInfiniteQueryPageContext,\n} from './hooks/useApiInfiniteQuery';\nimport type { UseApiMutationOptions } from './hooks/useApiMutation';\nimport { createQueryCache, type MutationCallbackContext, type QueryCacheKey } from './hooks/QueryCache';\n\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-query-provider:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-query:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\nconst APP_QUERY_CLIENT_SYMBOL = Symbol.for('frontx:query-cache:app-client');\nconst APP_QUERY_CLIENT_RESOLVER_SYMBOL = Symbol.for('frontx:query-cache:app-client-resolver');\nconst APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = Symbol.for('frontx:query-cache:app-client-activator');\n\ntype QueryClientApp = FrontXApp & {\n [APP_QUERY_CLIENT_SYMBOL]?: QueryClient;\n [APP_QUERY_CLIENT_RESOLVER_SYMBOL]?: () => QueryClient | undefined;\n [APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?: () => QueryClient | undefined;\n};\n\nconst FrontXQueryClientContext = createContext<QueryClient | undefined>(undefined);\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\ntype RuntimeAwareEndpointDescriptor<TData> = EndpointDescriptor<TData> & {\n fetch(options?: { signal?: AbortSignal; staleTime?: number }): Promise<TData>;\n};\n\ntype AppResolvedQueryClient = {\n app: FrontXApp;\n queryClient: QueryClient;\n};\n\nconst useCommitEffect = typeof globalThis.window === 'undefined' ? useEffect : useLayoutEffect;\n\nexport function resolveFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();\n}\n\nexport function hasFrontXQueryClientActivator(app: FrontXApp): boolean {\n return typeof (app as QueryClientApp)[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL] === 'function';\n}\n\nexport function activateFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?.();\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n/**\n * Imperatively invalidates cache entries for `target` on the QueryClient bound to `app`.\n * Use from non-React code (e.g. MFE actions); in components prefer `useQueryCache().invalidate`.\n */\nexport async function invalidateQueryCacheForApp(\n app: FrontXApp,\n target: EndpointDescriptor<unknown> | QueryCacheKey\n): Promise<void> {\n const queryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (!queryClient) {\n throw new Error(\n '[invalidateQueryCacheForApp] No QueryClient on app. Ensure queryCache() or queryCacheShared() is in the plugin chain.'\n );\n }\n await createQueryCache(queryClient).invalidate(target);\n}\n\n/** Resolves the app-bound QueryClient without triggering plugin activation during render. */\nexport function bootstrapFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n return resolveFrontXQueryClient(app);\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\nexport function useBootstrappedFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const fromApp = useMemo(() => bootstrapFrontXQueryClient(app), [app]);\n const [eventResolvedClient, setEventResolvedClient] = useState<AppResolvedQueryClient | undefined>(\n undefined\n );\n const queryClient =\n fromApp ?? (eventResolvedClient?.app === app ? eventResolvedClient.queryClient : undefined);\n\n useCommitEffect(() => {\n if (fromApp || !hasFrontXQueryClientActivator(app)) {\n return;\n }\n\n const tryResolveQueryClient = () => {\n const nextQueryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (nextQueryClient) {\n setEventResolvedClient((current) => {\n if (current?.app === app && current.queryClient === nextQueryClient) {\n return current;\n }\n\n return {\n app,\n queryClient: nextQueryClient,\n };\n });\n }\n };\n\n // Only activate after commit so abandoned renders cannot leak retainers/listeners.\n tryResolveQueryClient();\n\n const subscription = subscribeQueryCacheRuntimeChanged(() => {\n tryResolveQueryClient();\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, [app, fromApp]);\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\nexport function FrontXQueryClientProvider({\n queryClient,\n children,\n}: Readonly<{\n queryClient: QueryClient | undefined;\n children: React.ReactNode;\n}>) {\n if (!queryClient) {\n return (\n <FrontXQueryClientContext.Provider value={undefined}>\n <QueryClientContext.Provider value={undefined}>{children}</QueryClientContext.Provider>\n </FrontXQueryClientContext.Provider>\n );\n }\n\n return (\n <FrontXQueryClientContext.Provider value={queryClient}>\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n </FrontXQueryClientContext.Provider>\n );\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\nexport function useOptionalFrontXQueryClient(): QueryClient | undefined {\n return useContext(FrontXQueryClientContext);\n}\n\nexport function useRequiredFrontXQueryClient(): QueryClient {\n const queryClient = useOptionalFrontXQueryClient();\n if (!queryClient) {\n throw new Error(\n '[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition.'\n );\n }\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\nfunction buildPageContext<TPage>(\n page: TPage,\n pages: readonly TPage[],\n descriptor: EndpointDescriptor<TPage>,\n descriptors: readonly EndpointDescriptor<TPage>[]\n): ApiInfiniteQueryPageContext<TPage> {\n return {\n page,\n pages,\n descriptor,\n descriptors,\n };\n}\n\nfunction normalizeFetchStaleTime(staleTime: unknown): number | undefined {\n if (staleTime === 'static') {\n return Number.POSITIVE_INFINITY;\n }\n\n return typeof staleTime === 'number' ? staleTime : undefined;\n}\n\nfunction resolveFetchStaleTime<TData>(\n queryClient: QueryClient,\n descriptor: EndpointDescriptor<TData>,\n overrideStaleTime?: number\n): number | undefined {\n return normalizeFetchStaleTime(\n overrideStaleTime ??\n descriptor.staleTime ??\n queryClient.getDefaultOptions().queries?.staleTime\n );\n}\n\nfunction fetchDescriptor<TData>(\n descriptor: EndpointDescriptor<TData>,\n options?: { signal?: AbortSignal; staleTime?: number }\n): Promise<TData> {\n return (descriptor as RuntimeAwareEndpointDescriptor<TData>).fetch(options);\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\nexport function useFrontXQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n refetch: result.refetch,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\nexport function useFrontXSuspenseQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useSuspenseQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\nexport function useFrontXInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\nexport function useFrontXSuspenseInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useSuspenseInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\nexport function useFrontXMutation<\n TData = unknown,\n TError = Error,\n TVariables = void,\n TContext = unknown,\n>(\n options: UseApiMutationOptions<TData, TError, TVariables, TContext>,\n callbackCtx: MutationCallbackContext\n) {\n useRequiredFrontXQueryClient();\n\n const latestRef = React.useRef({ options, callbackCtx });\n latestRef.current = { options, callbackCtx };\n const fetchAbortControllersRef = React.useRef<Set<AbortController>>(new Set());\n\n React.useEffect(() => {\n const fetchAbortControllers = fetchAbortControllersRef.current;\n return () => {\n if (latestRef.current.options.abortOnUnmount) {\n for (const controller of fetchAbortControllers) {\n controller.abort();\n }\n fetchAbortControllers.clear();\n }\n };\n }, []);\n\n const mutationFn = React.useCallback((variables: TVariables, context: MutationFunctionContext) => {\n const cancelOnSupersede = latestRef.current.options.cancelOnSupersede;\n if (cancelOnSupersede) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n\n const controller = new AbortController();\n fetchAbortControllersRef.current.add(controller);\n\n const librarySignal = (context as MutationFunctionContext & { signal?: AbortSignal }).signal;\n if (cancelOnSupersede && librarySignal) {\n if (librarySignal.aborted) {\n controller.abort();\n } else {\n librarySignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return latestRef.current.options.endpoint\n .fetch(variables, { signal: controller.signal })\n .finally(() => {\n fetchAbortControllersRef.current.delete(controller);\n });\n }, []);\n\n const onMutate = React.useCallback((variables: TVariables, _context: MutationFunctionContext) => {\n const current = latestRef.current;\n if (!current.options.onMutate) {\n return undefined as TContext;\n }\n\n return current.options.onMutate(variables, current.callbackCtx);\n }, []);\n\n const onSuccess = React.useCallback((data: TData, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onSuccess?.(data, variables, context, current.callbackCtx);\n }, []);\n\n const onError = React.useCallback((error: TError, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onError?.(error, variables, context, current.callbackCtx);\n }, []);\n\n const onSettled = React.useCallback((\n data: TData | undefined,\n error: TError | null,\n variables: TVariables,\n context: TContext | undefined\n ) => {\n const current = latestRef.current;\n return current.options.onSettled?.(\n data,\n error,\n variables,\n context,\n current.callbackCtx\n );\n }, []);\n\n const mutation = useMutation<TData, TError, TVariables, TContext>({\n mutationFn,\n onMutate: options.onMutate ? onMutate : undefined,\n onSuccess: options.onSuccess ? onSuccess : undefined,\n onError: options.onError ? onError : undefined,\n onSettled: options.onSettled ? onSettled : undefined,\n });\n\n return {\n mutate: mutation.mutate as (variables: TVariables) => void,\n mutateAsync: mutation.mutateAsync as (variables: TVariables) => Promise<TData>,\n isPending: mutation.isPending,\n error: mutation.error,\n data: mutation.data,\n reset: mutation.reset,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\n","/**\n * QueryCache — restricted interface for interacting with the shared QueryClient.\n *\n * MFEs and screen-set components access the cache only through this sanctioned\n * public interface. It is injected as { queryCache } into useApiMutation\n * callbacks and also returned by useQueryCache() for controlled imperative\n * cache access.\n */\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-create-query-cache\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n\nimport type { EndpointDescriptor } from '@gears-frontx/framework';\nimport { eventBus } from '@gears-frontx/framework';\nimport { type QueryClient, type QueryState } from '@tanstack/react-query';\n\nexport type QueryCacheKey = readonly unknown[];\nexport type QueryCacheRefetchMode = 'active' | 'inactive' | 'all' | 'none';\nexport type QueryCacheInvalidateTarget =\n | EndpointDescriptor<unknown>\n | QueryCacheKey\n | null\n | undefined;\n\nexport interface QueryCacheState<TData = unknown, TError = Error> {\n data: TData | undefined;\n dataUpdatedAt: number;\n error: TError | null;\n errorUpdatedAt: number;\n fetchFailureCount: number;\n fetchFailureReason: TError | null;\n fetchStatus: 'fetching' | 'paused' | 'idle';\n isInvalidated: boolean;\n status: 'pending' | 'error' | 'success';\n}\n\nexport type QueryCacheInvalidateFilters = {\n queryKey?: QueryCacheInvalidateTarget;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n};\n\ntype QueryCacheUpdater<T> = T | ((old: T | undefined) => T | undefined);\n\ntype CacheInvalidateEvent = {\n queryKey: QueryCacheKey;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n source?: string;\n};\n\ntype CacheSetEvent = {\n queryKey: QueryCacheKey;\n dataOrUpdater: QueryCacheUpdater<unknown>;\n source?: string;\n};\n\ntype CacheRemoveEvent = {\n queryKey: QueryCacheKey;\n source?: string;\n};\n\nconst QUERY_CLIENT_BROADCAST_TARGET = Symbol.for('frontx:query-cache:broadcast-target');\nconst QUERY_CLIENT_BROADCAST_COUNTER = Symbol.for('frontx:query-cache:broadcast-counter');\n\ntype QueryClientWithMetadata = QueryClient & {\n [QUERY_CLIENT_BROADCAST_TARGET]?: string;\n};\n\ntype QueryClientCounterHost = typeof globalThis & {\n [QUERY_CLIENT_BROADCAST_COUNTER]?: number;\n};\n\nfunction emitCacheEvent(event: string, payload: unknown): void {\n (eventBus.emit as (eventType: string, payload: unknown) => void)(event, payload);\n}\n\nfunction ensureBroadcastSource(queryClient: QueryClient): string {\n const clientWithMetadata = queryClient as QueryClientWithMetadata;\n const existingSource = clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET];\n if (typeof existingSource === 'string' && existingSource.length > 0) {\n return existingSource;\n }\n\n const host = globalThis as QueryClientCounterHost;\n const nextCounter = (host[QUERY_CLIENT_BROADCAST_COUNTER] ?? 0) + 1;\n host[QUERY_CLIENT_BROADCAST_COUNTER] = nextCounter;\n\n const source = `query-cache-${nextCounter}`;\n clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET] = source;\n return source;\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-resolve-key\nexport function resolveKey(target: EndpointDescriptor<unknown> | QueryCacheKey): readonly unknown[] {\n if (\n target !== null &&\n typeof target === 'object' &&\n !Array.isArray(target) &&\n 'key' in target &&\n 'fetch' in target &&\n Array.isArray(target.key) &&\n typeof target.fetch === 'function'\n ) {\n return target.key;\n }\n\n return target as readonly unknown[];\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-resolve-key\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\nexport interface QueryCache {\n get<T>(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined;\n getState<TData = unknown, TError = Error>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey\n ): QueryCacheState<TData, TError> | undefined;\n set<T>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey,\n dataOrUpdater: T | ((old: T | undefined) => T | undefined)\n ): void;\n cancel(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidate(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidateMany(filters?: QueryCacheInvalidateFilters | null): Promise<void>;\n remove(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): void;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\nexport interface MutationCallbackContext {\n queryCache: QueryCache;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\n\nfunction toQueryUpdater<T>(\n dataOrUpdater: QueryCacheUpdater<T>\n): (old: T | undefined) => T | undefined {\n if (typeof dataOrUpdater === 'function') {\n return dataOrUpdater as (old: T | undefined) => T | undefined;\n }\n\n return () => dataOrUpdater;\n}\n\nfunction mapQueryState<TData, TError = Error>(\n state: QueryState<TData, TError>\n): QueryCacheState<TData, TError> {\n return {\n data: state.data,\n dataUpdatedAt: state.dataUpdatedAt,\n error: state.error,\n errorUpdatedAt: state.errorUpdatedAt,\n fetchFailureCount: state.fetchFailureCount,\n fetchFailureReason: state.fetchFailureReason,\n fetchStatus: state.fetchStatus,\n isInvalidated: state.isInvalidated,\n status: state.status,\n };\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-create-query-cache\nexport function createQueryCache(queryClient: QueryClient): QueryCache {\n return {\n get: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined => {\n return queryClient.getQueryData<T>(resolveKey(key));\n },\n getState: <TData = unknown, TError = Error>(key: EndpointDescriptor<unknown> | QueryCacheKey) => {\n const state = queryClient.getQueryState<TData, TError>(resolveKey(key));\n return state ? mapQueryState(state) : undefined;\n },\n set: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey, dataOrUpdater: QueryCacheUpdater<T>): void => {\n const resolvedKey = resolveKey(key);\n queryClient.setQueryData<T>(resolvedKey, toQueryUpdater(dataOrUpdater));\n emitCacheEvent('cache/set', {\n queryKey: resolvedKey,\n dataOrUpdater: dataOrUpdater as QueryCacheUpdater<unknown>,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheSetEvent);\n },\n cancel: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n return queryClient.cancelQueries({ queryKey: resolveKey(key) });\n },\n invalidate: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n const resolvedKey = resolveKey(key);\n const result = queryClient.invalidateQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/invalidate', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n invalidateMany: (filters): Promise<void> => {\n if (filters?.queryKey == null) {\n return Promise.resolve();\n }\n\n const resolvedKey = resolveKey(filters.queryKey);\n if (!Array.isArray(resolvedKey) || resolvedKey.length === 0) {\n return Promise.resolve();\n }\n\n const payload = {\n ...filters,\n queryKey: resolvedKey,\n };\n const result = queryClient.invalidateQueries(payload);\n emitCacheEvent('cache/invalidate', {\n ...payload,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n remove: (key: EndpointDescriptor<unknown> | QueryCacheKey): void => {\n const resolvedKey = resolveKey(key);\n queryClient.removeQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/remove', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheRemoveEvent);\n },\n };\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-create-query-cache\n","/**\n * Test-only entry for @gears-frontx/react.\n *\n * Utilities here are intended for Vitest (or similar) suites that need access\n * to query-client wiring without importing package internals directly.\n */\n\n// @cpt-dod:cpt-frontx-dod-react-bindings-provider:p1\n\n// @cpt-begin:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\nexport {\n bootstrapFrontXQueryClient,\n resolveFrontXQueryClient,\n useOptionalFrontXQueryClient,\n} from './queryClient';\n\n/** App-layer tests must import this from `@gears-frontx/react/testing`, not `@gears-frontx/framework/testing`. */\nexport { describeBootstrapMfeContract } from '@gears-frontx/framework/testing';\nexport type {\n BootstrapMfeResolveArgs,\n BootstrapMfeTestSpecOptions,\n} from '@gears-frontx/framework/testing';\n// @cpt-end:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\n"],"mappings":";AAAA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAGK;;;ACVP,SAAS,gBAAgB;;;ADmJjB;AAzHR,IAAM,0BAA0B,uBAAO,IAAI,+BAA+B;AAC1E,IAAM,mCAAmC,uBAAO,IAAI,wCAAwC;AAS5F,IAAM,2BAA2B,cAAuC,MAAS;AAajF,IAAM,kBAAkB,OAAO,WAAW,WAAW,cAAc,YAAY;AAExE,SAAS,yBAAyB,KAAyC;AAChF,QAAM,YAAY;AAClB,SAAO,UAAU,uBAAuB,KAAK,UAAU,gCAAgC,IAAI;AAC7F;AA+BO,SAAS,2BAA2B,KAAyC;AAClF,SAAO,yBAAyB,GAAG;AACrC;AA0EO,SAAS,+BAAwD;AACtE,SAAO,WAAW,wBAAwB;AAC5C;;;AE/JA,SAAS,oCAAoC;","names":[]}
1
+ {"version":3,"sources":["../src/queryClient.tsx","../src/hooks/QueryCache.ts","../src/hooks/useLatest.ts","../src/testing.ts"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useEffect,\n useLayoutEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n QueryClientContext,\n QueryClientProvider,\n type QueryClient,\n useInfiniteQuery,\n useMutation,\n useQuery,\n useSuspenseInfiniteQuery,\n useSuspenseQuery,\n type MutationFunctionContext,\n} from '@tanstack/react-query';\nimport {\n subscribeQueryCacheRuntimeChanged,\n type EndpointDescriptor,\n type FrontXApp,\n} from '@gears-frontx/framework';\nimport type { ApiQueryOverrides } from './hooks/useApiQuery';\nimport type {\n ApiInfiniteQueryOptions,\n ApiInfiniteQueryPageContext,\n} from './hooks/useApiInfiniteQuery';\nimport type { UseApiMutationOptions } from './hooks/useApiMutation';\nimport { createQueryCache, type MutationCallbackContext, type QueryCacheKey } from './hooks/QueryCache';\nimport { useLatest } from './hooks/useLatest';\n\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-query-provider:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-query:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\nconst APP_QUERY_CLIENT_SYMBOL = Symbol.for('frontx:query-cache:app-client');\nconst APP_QUERY_CLIENT_RESOLVER_SYMBOL = Symbol.for('frontx:query-cache:app-client-resolver');\nconst APP_QUERY_CLIENT_ACTIVATOR_SYMBOL = Symbol.for('frontx:query-cache:app-client-activator');\n\ntype QueryClientApp = FrontXApp & {\n [APP_QUERY_CLIENT_SYMBOL]?: QueryClient;\n [APP_QUERY_CLIENT_RESOLVER_SYMBOL]?: () => QueryClient | undefined;\n [APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?: () => QueryClient | undefined;\n};\n\nconst FrontXQueryClientContext = createContext<QueryClient | undefined>(undefined);\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-react-query-client-symbols\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\ntype RuntimeAwareEndpointDescriptor<TData> = EndpointDescriptor<TData> & {\n fetch(options?: { signal?: AbortSignal; staleTime?: number }): Promise<TData>;\n};\n\ntype AppResolvedQueryClient = {\n app: FrontXApp;\n queryClient: QueryClient;\n};\n\nconst useCommitEffect = typeof globalThis.window === 'undefined' ? useEffect : useLayoutEffect;\n\nexport function resolveFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_RESOLVER_SYMBOL]?.();\n}\n\nexport function hasFrontXQueryClientActivator(app: FrontXApp): boolean {\n return typeof (app as QueryClientApp)[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL] === 'function';\n}\n\nexport function activateFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const clientApp = app as QueryClientApp;\n return clientApp[APP_QUERY_CLIENT_SYMBOL] ?? clientApp[APP_QUERY_CLIENT_ACTIVATOR_SYMBOL]?.();\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-resolve-app-query-client\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n/**\n * Imperatively invalidates cache entries for `target` on the QueryClient bound to `app`.\n * Use from non-React code (e.g. MFE actions); in components prefer `useQueryCache().invalidate`.\n */\nexport async function invalidateQueryCacheForApp(\n app: FrontXApp,\n target: EndpointDescriptor<unknown> | QueryCacheKey\n): Promise<void> {\n const queryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (!queryClient) {\n throw new Error(\n '[invalidateQueryCacheForApp] No QueryClient on app. Ensure queryCache() or queryCacheShared() is in the plugin chain.'\n );\n }\n await createQueryCache(queryClient).invalidate(target);\n}\n\n/** Resolves the app-bound QueryClient without triggering plugin activation during render. */\nexport function bootstrapFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n return resolveFrontXQueryClient(app);\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-imperative-cache-bootstrap\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\nexport function useBootstrappedFrontXQueryClient(app: FrontXApp): QueryClient | undefined {\n const fromApp = useMemo(() => bootstrapFrontXQueryClient(app), [app]);\n const [eventResolvedClient, setEventResolvedClient] = useState<AppResolvedQueryClient | undefined>(\n undefined\n );\n const queryClient =\n fromApp ?? (eventResolvedClient?.app === app ? eventResolvedClient.queryClient : undefined);\n\n useCommitEffect(() => {\n if (fromApp || !hasFrontXQueryClientActivator(app)) {\n return;\n }\n\n const tryResolveQueryClient = () => {\n const nextQueryClient = resolveFrontXQueryClient(app) ?? activateFrontXQueryClient(app);\n if (nextQueryClient) {\n setEventResolvedClient((current) => {\n if (current?.app === app && current.queryClient === nextQueryClient) {\n return current;\n }\n\n return {\n app,\n queryClient: nextQueryClient,\n };\n });\n }\n };\n\n // Only activate after commit so abandoned renders cannot leak retainers/listeners.\n tryResolveQueryClient();\n\n const subscription = subscribeQueryCacheRuntimeChanged(() => {\n tryResolveQueryClient();\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, [app, fromApp]);\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-query-client-lifecycle:p2:inst-use-bootstrapped-query-client\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\nexport function FrontXQueryClientProvider({\n queryClient,\n children,\n}: Readonly<{\n queryClient: QueryClient | undefined;\n children: React.ReactNode;\n}>) {\n if (!queryClient) {\n return (\n <FrontXQueryClientContext.Provider value={undefined}>\n <QueryClientContext.Provider value={undefined}>{children}</QueryClientContext.Provider>\n </FrontXQueryClientContext.Provider>\n );\n }\n\n return (\n <FrontXQueryClientContext.Provider value={queryClient}>\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n </FrontXQueryClientContext.Provider>\n );\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-frontx-query-client-provider\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\nexport function useOptionalFrontXQueryClient(): QueryClient | undefined {\n return useContext(FrontXQueryClientContext);\n}\n\nexport function useRequiredFrontXQueryClient(): QueryClient {\n const queryClient = useOptionalFrontXQueryClient();\n if (!queryClient) {\n throw new Error(\n '[FrontXProvider] No query cache available. Add queryCache() or queryCacheShared() to your plugin composition.'\n );\n }\n\n return queryClient;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-query-provider:p2:inst-query-client-context-hooks\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\nfunction buildPageContext<TPage>(\n page: TPage,\n pages: readonly TPage[],\n descriptor: EndpointDescriptor<TPage>,\n descriptors: readonly EndpointDescriptor<TPage>[]\n): ApiInfiniteQueryPageContext<TPage> {\n return {\n page,\n pages,\n descriptor,\n descriptors,\n };\n}\n\nfunction normalizeFetchStaleTime(staleTime: unknown): number | undefined {\n if (staleTime === 'static') {\n return Number.POSITIVE_INFINITY;\n }\n\n return typeof staleTime === 'number' ? staleTime : undefined;\n}\n\nfunction resolveFetchStaleTime<TData>(\n queryClient: QueryClient,\n descriptor: EndpointDescriptor<TData>,\n overrideStaleTime?: number\n): number | undefined {\n return normalizeFetchStaleTime(\n overrideStaleTime ??\n descriptor.staleTime ??\n queryClient.getDefaultOptions().queries?.staleTime\n );\n}\n\nfunction fetchDescriptor<TData>(\n descriptor: EndpointDescriptor<TData>,\n options?: { signal?: AbortSignal; staleTime?: number }\n): Promise<TData> {\n return (descriptor as RuntimeAwareEndpointDescriptor<TData>).fetch(options);\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-descriptor-fetch-stale-helpers\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\nexport function useFrontXQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n refetch: result.refetch,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\nexport function useFrontXSuspenseQuery<TData = unknown, TError = Error>(\n descriptor: EndpointDescriptor<TData>,\n overrides?: ApiQueryOverrides\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const staleTime = resolveFetchStaleTime(queryClient, descriptor, overrides?.staleTime);\n const result = useSuspenseQuery<TData, TError>({\n queryKey: descriptor.key as unknown[],\n queryFn: ({ signal }) => fetchDescriptor(descriptor, { signal, staleTime }),\n staleTime: overrides?.staleTime ?? descriptor.staleTime,\n gcTime: overrides?.gcTime ?? descriptor.gcTime,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\nexport function useFrontXInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isLoading: result.isLoading,\n isFetching: result.isFetching,\n isError: result.isError,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\nexport function useFrontXSuspenseInfiniteQuery<TPage = unknown, TError = Error>(\n options: ApiInfiniteQueryOptions<TPage>\n) {\n const queryClient = useRequiredFrontXQueryClient();\n const result = useSuspenseInfiniteQuery<\n TPage,\n TError,\n readonly TPage[],\n readonly unknown[],\n EndpointDescriptor<TPage>\n >({\n queryKey: options.initialPage.key,\n initialPageParam: options.initialPage,\n queryFn: ({ pageParam, signal }) =>\n fetchDescriptor(pageParam, {\n signal,\n staleTime: resolveFetchStaleTime(queryClient, pageParam, options.staleTime),\n }),\n getNextPageParam: (\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n ) =>\n options.getNextPage(\n buildPageContext(\n lastPage,\n allPages,\n lastPageDescriptor,\n allPageDescriptors\n )\n ),\n getPreviousPageParam: options.getPreviousPage\n ? (firstPage, allPages, firstPageDescriptor, allPageDescriptors) =>\n options.getPreviousPage?.(\n buildPageContext(\n firstPage,\n allPages,\n firstPageDescriptor,\n allPageDescriptors\n )\n )\n : undefined,\n select: (data) => data.pages,\n staleTime: options.staleTime ?? options.initialPage.staleTime,\n gcTime: options.gcTime ?? options.initialPage.gcTime,\n maxPages: options.maxPages,\n });\n\n return {\n data: result.data,\n error: result.error,\n isFetching: result.isFetching,\n hasNextPage: result.hasNextPage ?? false,\n hasPreviousPage: result.hasPreviousPage ?? false,\n isFetchingNextPage: result.isFetchingNextPage,\n isFetchingPreviousPage: result.isFetchingPreviousPage,\n fetchNextPage: async () => {\n await result.fetchNextPage();\n },\n fetchPreviousPage: async () => {\n await result.fetchPreviousPage();\n },\n refetch: async () => {\n await result.refetch();\n },\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-query:p2:inst-delegate-frontx-use-suspense-infinite-query\n\n// @cpt-begin:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\nexport function useFrontXMutation<\n TData = unknown,\n TError = Error,\n TVariables = void,\n TContext = unknown,\n>(\n options: UseApiMutationOptions<TData, TError, TVariables, TContext>,\n callbackCtx: MutationCallbackContext\n) {\n useRequiredFrontXQueryClient();\n\n const latestRef = useLatest({ options, callbackCtx });\n const fetchAbortControllersRef = React.useRef<Set<AbortController>>(new Set());\n\n // Effect event: reads abortOnUnmount from the most recent render, not\n // whatever option value was current when the unmount effect was set up.\n const abortFetchesOnUnmount = React.useEffectEvent(() => {\n if (options.abortOnUnmount) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n });\n\n React.useEffect(() => {\n return () => {\n abortFetchesOnUnmount();\n };\n }, []);\n\n const mutationFn = React.useCallback((variables: TVariables, context: MutationFunctionContext) => {\n const cancelOnSupersede = latestRef.current.options.cancelOnSupersede;\n if (cancelOnSupersede) {\n for (const controller of fetchAbortControllersRef.current) {\n controller.abort();\n }\n fetchAbortControllersRef.current.clear();\n }\n\n const controller = new AbortController();\n fetchAbortControllersRef.current.add(controller);\n\n const librarySignal = (context as MutationFunctionContext & { signal?: AbortSignal }).signal;\n if (cancelOnSupersede && librarySignal) {\n if (librarySignal.aborted) {\n controller.abort();\n } else {\n librarySignal.addEventListener('abort', () => controller.abort(), { once: true });\n }\n }\n\n return latestRef.current.options.endpoint\n .fetch(variables, { signal: controller.signal })\n .finally(() => {\n fetchAbortControllersRef.current.delete(controller);\n });\n // latestRef's identity never changes (useLatest returns a stable ref\n // object) — listing it satisfies exhaustive-deps without recreating this\n // callback on every render.\n }, [latestRef]);\n\n const onMutate = React.useCallback((variables: TVariables, _context: MutationFunctionContext) => {\n const current = latestRef.current;\n if (!current.options.onMutate) {\n return undefined as TContext;\n }\n\n return current.options.onMutate(variables, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onSuccess = React.useCallback((data: TData, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onSuccess?.(data, variables, context, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onError = React.useCallback((error: TError, variables: TVariables, context: TContext | undefined) => {\n const current = latestRef.current;\n return current.options.onError?.(error, variables, context, current.callbackCtx);\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const onSettled = React.useCallback((\n data: TData | undefined,\n error: TError | null,\n variables: TVariables,\n context: TContext | undefined\n ) => {\n const current = latestRef.current;\n return current.options.onSettled?.(\n data,\n error,\n variables,\n context,\n current.callbackCtx\n );\n // latestRef's identity never changes — see mutationFn above.\n }, [latestRef]);\n\n const mutation = useMutation<TData, TError, TVariables, TContext>({\n mutationFn,\n onMutate: options.onMutate ? onMutate : undefined,\n onSuccess: options.onSuccess ? onSuccess : undefined,\n onError: options.onError ? onError : undefined,\n onSettled: options.onSettled ? onSettled : undefined,\n });\n\n return {\n mutate: mutation.mutate as (variables: TVariables) => void,\n mutateAsync: mutation.mutateAsync as (variables: TVariables) => Promise<TData>,\n isPending: mutation.isPending,\n error: mutation.error,\n data: mutation.data,\n reset: mutation.reset,\n };\n}\n// @cpt-end:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-delegate-frontx-use-mutation\n","/**\n * QueryCache — restricted interface for interacting with the shared QueryClient.\n *\n * MFEs and screen-set components access the cache only through this sanctioned\n * public interface. It is injected as { queryCache } into useApiMutation\n * callbacks and also returned by useQueryCache() for controlled imperative\n * cache access.\n */\n// @cpt-dod:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2\n// @cpt-flow:cpt-frontx-flow-request-lifecycle-use-api-mutation:p2:inst-create-query-cache\n// @cpt-FEATURE:implement-endpoint-descriptors:p3\n\nimport type { EndpointDescriptor } from '@gears-frontx/framework';\nimport { eventBus } from '@gears-frontx/framework';\nimport { type QueryClient, type QueryState } from '@tanstack/react-query';\n\nexport type QueryCacheKey = readonly unknown[];\nexport type QueryCacheRefetchMode = 'active' | 'inactive' | 'all' | 'none';\nexport type QueryCacheInvalidateTarget =\n | EndpointDescriptor<unknown>\n | QueryCacheKey\n | null\n | undefined;\n\nexport interface QueryCacheState<TData = unknown, TError = Error> {\n data: TData | undefined;\n dataUpdatedAt: number;\n error: TError | null;\n errorUpdatedAt: number;\n fetchFailureCount: number;\n fetchFailureReason: TError | null;\n fetchStatus: 'fetching' | 'paused' | 'idle';\n isInvalidated: boolean;\n status: 'pending' | 'error' | 'success';\n}\n\nexport type QueryCacheInvalidateFilters = {\n queryKey?: QueryCacheInvalidateTarget;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n};\n\ntype QueryCacheUpdater<T> = T | ((old: T | undefined) => T | undefined);\n\ntype CacheInvalidateEvent = {\n queryKey: QueryCacheKey;\n exact?: boolean;\n refetchType?: QueryCacheRefetchMode;\n source?: string;\n};\n\ntype CacheSetEvent = {\n queryKey: QueryCacheKey;\n dataOrUpdater: QueryCacheUpdater<unknown>;\n source?: string;\n};\n\ntype CacheRemoveEvent = {\n queryKey: QueryCacheKey;\n source?: string;\n};\n\nconst QUERY_CLIENT_BROADCAST_TARGET = Symbol.for('frontx:query-cache:broadcast-target');\nconst QUERY_CLIENT_BROADCAST_COUNTER = Symbol.for('frontx:query-cache:broadcast-counter');\n\ntype QueryClientWithMetadata = QueryClient & {\n [QUERY_CLIENT_BROADCAST_TARGET]?: string;\n};\n\ntype QueryClientCounterHost = typeof globalThis & {\n [QUERY_CLIENT_BROADCAST_COUNTER]?: number;\n};\n\nfunction emitCacheEvent(event: string, payload: unknown): void {\n (eventBus.emit as (eventType: string, payload: unknown) => void)(event, payload);\n}\n\nfunction ensureBroadcastSource(queryClient: QueryClient): string {\n const clientWithMetadata = queryClient as QueryClientWithMetadata;\n const existingSource = clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET];\n if (typeof existingSource === 'string' && existingSource.length > 0) {\n return existingSource;\n }\n\n const host = globalThis as QueryClientCounterHost;\n const nextCounter = (host[QUERY_CLIENT_BROADCAST_COUNTER] ?? 0) + 1;\n host[QUERY_CLIENT_BROADCAST_COUNTER] = nextCounter;\n\n const source = `query-cache-${nextCounter}`;\n clientWithMetadata[QUERY_CLIENT_BROADCAST_TARGET] = source;\n return source;\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-resolve-key\nexport function resolveKey(target: EndpointDescriptor<unknown> | QueryCacheKey): readonly unknown[] {\n if (\n target !== null &&\n typeof target === 'object' &&\n !Array.isArray(target) &&\n 'key' in target &&\n 'fetch' in target &&\n Array.isArray(target.key) &&\n typeof target.fetch === 'function'\n ) {\n return target.key;\n }\n\n return target as readonly unknown[];\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-resolve-key\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\nexport interface QueryCache {\n get<T>(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined;\n getState<TData = unknown, TError = Error>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey\n ): QueryCacheState<TData, TError> | undefined;\n set<T>(\n queryKey: EndpointDescriptor<unknown> | QueryCacheKey,\n dataOrUpdater: T | ((old: T | undefined) => T | undefined)\n ): void;\n cancel(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidate(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void>;\n invalidateMany(filters?: QueryCacheInvalidateFilters | null): Promise<void>;\n remove(queryKey: EndpointDescriptor<unknown> | QueryCacheKey): void;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-query-cache-interface\n\n// @cpt-begin:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\nexport interface MutationCallbackContext {\n queryCache: QueryCache;\n}\n// @cpt-end:cpt-frontx-dod-request-lifecycle-use-api-mutation:p2:inst-mutation-callback-context\n\nfunction toQueryUpdater<T>(\n dataOrUpdater: QueryCacheUpdater<T>\n): (old: T | undefined) => T | undefined {\n if (typeof dataOrUpdater === 'function') {\n return dataOrUpdater as (old: T | undefined) => T | undefined;\n }\n\n return () => dataOrUpdater;\n}\n\nfunction mapQueryState<TData, TError = Error>(\n state: QueryState<TData, TError>\n): QueryCacheState<TData, TError> {\n return {\n data: state.data,\n dataUpdatedAt: state.dataUpdatedAt,\n error: state.error,\n errorUpdatedAt: state.errorUpdatedAt,\n fetchFailureCount: state.fetchFailureCount,\n fetchFailureReason: state.fetchFailureReason,\n fetchStatus: state.fetchStatus,\n isInvalidated: state.isInvalidated,\n status: state.status,\n };\n}\n\n// @cpt-begin:implement-endpoint-descriptors:p3:inst-create-query-cache\nexport function createQueryCache(queryClient: QueryClient): QueryCache {\n return {\n get: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey): T | undefined => {\n return queryClient.getQueryData<T>(resolveKey(key));\n },\n getState: <TData = unknown, TError = Error>(key: EndpointDescriptor<unknown> | QueryCacheKey) => {\n const state = queryClient.getQueryState<TData, TError>(resolveKey(key));\n return state ? mapQueryState(state) : undefined;\n },\n set: <T,>(key: EndpointDescriptor<unknown> | QueryCacheKey, dataOrUpdater: QueryCacheUpdater<T>): void => {\n const resolvedKey = resolveKey(key);\n queryClient.setQueryData<T>(resolvedKey, toQueryUpdater(dataOrUpdater));\n emitCacheEvent('cache/set', {\n queryKey: resolvedKey,\n dataOrUpdater: dataOrUpdater as QueryCacheUpdater<unknown>,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheSetEvent);\n },\n cancel: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n return queryClient.cancelQueries({ queryKey: resolveKey(key) });\n },\n invalidate: (key: EndpointDescriptor<unknown> | QueryCacheKey): Promise<void> => {\n const resolvedKey = resolveKey(key);\n const result = queryClient.invalidateQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/invalidate', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n invalidateMany: (filters): Promise<void> => {\n if (filters?.queryKey == null) {\n return Promise.resolve();\n }\n\n const resolvedKey = resolveKey(filters.queryKey);\n if (!Array.isArray(resolvedKey) || resolvedKey.length === 0) {\n return Promise.resolve();\n }\n\n const payload = {\n ...filters,\n queryKey: resolvedKey,\n };\n const result = queryClient.invalidateQueries(payload);\n emitCacheEvent('cache/invalidate', {\n ...payload,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheInvalidateEvent);\n return result;\n },\n remove: (key: EndpointDescriptor<unknown> | QueryCacheKey): void => {\n const resolvedKey = resolveKey(key);\n queryClient.removeQueries({ queryKey: resolvedKey });\n emitCacheEvent('cache/remove', {\n queryKey: resolvedKey,\n source: ensureBroadcastSource(queryClient),\n } satisfies CacheRemoveEvent);\n },\n };\n}\n// @cpt-end:implement-endpoint-descriptors:p3:inst-create-query-cache\n","/**\n * useLatest - keeps a ref pinned to the most recently rendered value\n *\n * A handful of hooks/components in this package need to read the current\n * value of a prop or option from inside a callback or effect-cleanup closure\n * that must NOT re-run every time that value changes (e.g. a stable\n * useCallback with `[]` deps, or a mount/unmount effect keyed on identity\n * rather than on every option). Stashing the value in a ref sidesteps the\n * closure staleness without pulling the value into the dependency array.\n *\n * The write happens inside a deps-less effect (runs after every commit)\n * rather than during render, because refs must not be mutated while\n * rendering: render can be invoked more than once per commit (React\n * StrictMode's double-invoke, and future concurrent-rendering paths that\n * may discard a render), so a render-time write can leave the ref out of\n * sync with what was actually committed to the DOM. A deps-less effect\n * runs exactly once per real commit, so the ref always reflects the value\n * that was last rendered.\n */\n\nimport { useEffect, useRef, type RefObject } from 'react';\n\n/**\n * Returns a ref that always holds the most recently rendered `value`.\n *\n * The ref identity is stable across renders (same object returned every\n * time), so it is safe to add to a `useCallback`/`useEffect` dependency\n * array without causing the callback/effect to be recreated.\n */\nexport function useLatest<T>(value: T): RefObject<T> {\n const ref = useRef(value);\n\n // Deps-less: must run after every render so `ref.current` never lags\n // behind the value that was just committed. See module doc for why this\n // write cannot happen during render itself.\n useEffect(() => {\n ref.current = value;\n });\n\n return ref;\n}\n","/**\n * Test-only entry for @gears-frontx/react.\n *\n * Utilities here are intended for Vitest (or similar) suites that need access\n * to query-client wiring without importing package internals directly.\n */\n\n// @cpt-dod:cpt-frontx-dod-react-bindings-provider:p1\n\n// @cpt-begin:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\nexport {\n bootstrapFrontXQueryClient,\n resolveFrontXQueryClient,\n useOptionalFrontXQueryClient,\n} from './queryClient';\n\n/** App-layer tests must import this from `@gears-frontx/react/testing`, not `@gears-frontx/framework/testing`. */\nexport { describeBootstrapMfeContract } from '@gears-frontx/framework/testing';\nexport type {\n BootstrapMfeResolveArgs,\n BootstrapMfeTestSpecOptions,\n} from '@gears-frontx/framework/testing';\n// @cpt-end:cpt-frontx-dod-react-bindings-provider:p1:inst-react-testing-reexports\n"],"mappings":";AAAA,OAAO;AAAA,EACL;AAAA,EACA;AAAA,EACA,aAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAGK;;;ACVP,SAAS,gBAAgB;;;ACOzB,SAAS,WAAW,cAA8B;;;AF6I1C;AAzHR,IAAM,0BAA0B,uBAAO,IAAI,+BAA+B;AAC1E,IAAM,mCAAmC,uBAAO,IAAI,wCAAwC;AAS5F,IAAM,2BAA2B,cAAuC,MAAS;AAajF,IAAM,kBAAkB,OAAO,WAAW,WAAW,cAAcC,aAAY;AAExE,SAAS,yBAAyB,KAAyC;AAChF,QAAM,YAAY;AAClB,SAAO,UAAU,uBAAuB,KAAK,UAAU,gCAAgC,IAAI;AAC7F;AA+BO,SAAS,2BAA2B,KAAyC;AAClF,SAAO,yBAAyB,GAAG;AACrC;AA0EO,SAAS,+BAAwD;AACtE,SAAO,WAAW,wBAAwB;AAC5C;;;AGhKA,SAAS,oCAAoC;","names":["useEffect","useEffect"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gears-frontx/react",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-alpha.3",
4
4
  "description": "React bindings and hooks for FrontX framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -26,8 +26,7 @@
26
26
  "files": [
27
27
  "dist",
28
28
  "CLAUDE.md",
29
- "llms.txt",
30
- "commands"
29
+ "llms.txt"
31
30
  ],
32
31
  "scripts": {
33
32
  "build": "tsup",
@@ -41,7 +40,7 @@
41
40
  "@reduxjs/toolkit": "^2.5.0",
42
41
  "@tanstack/react-query": "^5.90.0",
43
42
  "@testing-library/react": "16.3.2",
44
- "@types/react": "^19.0.8",
43
+ "@types/react": "^19.2.18",
45
44
  "@types/react-redux": "^7.1.33",
46
45
  "@vitejs/plugin-react": "4.3.4",
47
46
  "@vitest/coverage-v8": "4.1.4",
@@ -1,58 +0,0 @@
1
- <!-- @standalone -->
2
- # frontx:duplicate-screenset - Duplicate Screenset
3
-
4
- ## AI WORKFLOW (REQUIRED)
5
- 1) Read .ai/targets/SCREENSETS.md before starting.
6
- 2) Gather requirements from user.
7
- 3) Follow steps below.
8
-
9
- REQUIRED: Copy folder and update ids.ts only (2 steps, 96% reduction).
10
- REQUIRED: Pass arch:check with zero errors.
11
- REQUIRED: Test via Chrome DevTools MCP (never skip).
12
-
13
- ## GATHER REQUIREMENTS
14
- Ask user for:
15
- - SOURCE screenset name (existing screenset to copy).
16
- - TARGET screenset name (camelCase, single word preferred).
17
- - TARGET category: Drafts | Mockups | Production.
18
-
19
- ## STEP 1: Copy Screenset via CLI
20
- ```bash
21
- frontx screenset copy SOURCE TARGET --category={category}
22
- ```
23
- The CLI automatically:
24
- - Copies all files (screens, slices, actions, events, effects, API, icons, i18n).
25
- - Updates ids.ts with TARGET_SCREENSET_ID and all SCREEN_IDs.
26
- - Everything auto-updates via template literals:
27
- - Event enums: ${TARGET_SCREENSET_ID}/${DOMAIN_ID}/eventName.
28
- - Icon IDs: ${TARGET_SCREENSET_ID}:iconName.
29
- - API domain: ${TARGET_SCREENSET_ID}:serviceName.
30
- - Redux state key.
31
- - Screenset auto-discovered (no manual registration).
32
-
33
- ## STEP 2: Validate
34
- ```bash
35
- npm run type-check
36
- npm run arch:check
37
- npm run lint
38
- grep -rn "OLD_SCREENSET_ID" src/screensets/TARGET/ # Must return 0 matches
39
- ```
40
-
41
- ## STEP 3: Test via Chrome DevTools MCP
42
- STOP: If MCP connection is broken, fix it first. NEVER skip testing.
43
- - npm run dev.
44
- - Verify TARGET in screenset selector.
45
- - Click TARGET screenset in dev panel.
46
- - Verify URL changes to target screenset.
47
- - Check 0 console errors.
48
- - Test all primary features.
49
- - Verify auto-discovery worked (screenset appears without manual registration).
50
-
51
- ## CHECKLIST
52
- - [ ] Gather requirements (source, target name, category).
53
- - [ ] Run `frontx screenset copy SOURCE TARGET --category={category}`.
54
- - [ ] Run type-check (MUST pass).
55
- - [ ] Run arch:check (MUST pass).
56
- - [ ] Run lint (MUST pass).
57
- - [ ] Verify zero occurrences of old screenset ID.
58
- - [ ] Test via Chrome DevTools MCP (NEVER skip).
@@ -1,57 +0,0 @@
1
- <!-- @standalone -->
2
- # frontx:new-component - Add New UI Component
3
-
4
- ## AI WORKFLOW (REQUIRED)
5
- 1) Read `.ai/project/GUIDELINES.md` and `.ai/project/targets/UNIT_TESTING.md` when they exist **before** any unit-test, validation, or testing steps in this command (project-owned overrides take precedence over defaults here).
6
- 2) Check for existing equivalent in project (e.g. components/ui/) and configured UI library first.
7
- 3) Gather requirements from user.
8
- 4) Implement.
9
-
10
- ## CHECK EXISTING COMPONENTS FIRST
11
- - REQUIRED: Read `frontx.config.json` to find the `uikit` value.
12
- - If uikit is a third-party package (not `shadcn` or `none`): read its exports from `node_modules/<package>/` to check for existing components.
13
- - REQUIRED: Before creating a new component, scan the project AND the configured UI library for existing equivalents.
14
- - REQUIRED: Reuse existing components if equivalent exists.
15
-
16
- ## GATHER REQUIREMENTS
17
- Ask user for:
18
- - Component name (e.g., "DataTable", "ColorPicker").
19
- - Component description and props.
20
-
21
- ## IF SCREENSET COMPONENT
22
-
23
- ### STEP 0: Determine Subfolder
24
- - components/ui/: Base UI primitives (shadcn components or custom).
25
- - screens/{screen}/components/: Screen-specific components.
26
- - components/: Shared composites used across screens.
27
-
28
- ### STEP 1: Implementation
29
-
30
- #### 1.1 Create Component
31
- File: src/components/ui/{ComponentName}.tsx (base primitives)
32
- or: src/screens/{screen}/components/{ComponentName}.tsx (screen-specific)
33
- or: src/components/{ComponentName}.tsx (shared composites)
34
- - Must be reusable within the MFE.
35
- - NO Redux or state management.
36
- - Accept value/onChange pattern for state.
37
-
38
- #### 1.2 Export
39
- Export from local index if needed.
40
-
41
- #### 1.3 Validation
42
- <!-- @cpt-begin:cpt-frontx-flow-unit-test-generation-and-agent-verification-agent-verify:p1:inst-agent-implement-and-test -->
43
- If creating a shared composite in `src/components/`, add or update a component test for its stable behavior.
44
- Run `npm run test:unit`, `npm run type-check`, `npm run arch:check`, and `npm run dev` (or the equivalent commands for this project's configured package manager) and apply any stricter triggers or conventions from `.ai/project/targets/UNIT_TESTING.md`. Apply constraints from workflow step 1.
45
- <!-- @cpt-end:cpt-frontx-flow-unit-test-generation-and-agent-verification-agent-verify:p1:inst-agent-implement-and-test -->
46
- Test component in UI.
47
-
48
- ## TESTING
49
- - REQUIRED: Load `.ai/project/GUIDELINES.md` and `.ai/project/targets/UNIT_TESTING.md` when they exist **before** interpreting unit-test requirements elsewhere in this command.
50
- - REQUIRED: Follow `.ai/project/targets/UNIT_TESTING.md` for `__test-utils__/` placement and durable, non-placeholder assertions when adding or updating tests.
51
-
52
- ## RULES
53
- - REQUIRED: Check existing project UI components first; create new only if missing.
54
- - FORBIDDEN: Redux, business logic, side effects in components.
55
- - FORBIDDEN: Inline styles outside components/ui/.
56
- - REQUIRED: Accept value/onChange pattern for state.
57
- - REQUIRED: New shared composite components in `src/components/` include a colocated or nearby unit test when the behavior is stable.