@mui/internal-docs-infra 0.11.1-canary.9 → 0.11.1
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/ChunkProvider/ChunkContext.d.mts +10 -0
- package/ChunkProvider/ChunkContext.mjs +15 -0
- package/ChunkProvider/ChunkProvider.d.mts +14 -0
- package/ChunkProvider/ChunkProvider.mjs +38 -0
- package/ChunkProvider/PreloadContext.d.mts +14 -0
- package/ChunkProvider/PreloadContext.mjs +18 -0
- package/ChunkProvider/PreloadProvider.d.mts +13 -0
- package/ChunkProvider/PreloadProvider.mjs +33 -0
- package/ChunkProvider/index.d.mts +7 -0
- package/ChunkProvider/index.mjs +7 -0
- package/ChunkProvider/types.d.mts +23 -0
- package/ChunkProvider/types.mjs +1 -0
- package/ChunkProvider/usePreload.d.mts +8 -0
- package/ChunkProvider/usePreload.mjs +21 -0
- package/CodeControllerContext/CodeControllerContext.d.mts +11 -0
- package/CodeControllerContext/CodeControllerContext.mjs +2 -1
- package/CodeHighlighter/CodeHighlighter.d.mts +15 -1
- package/CodeHighlighter/CodeHighlighter.mjs +97 -319
- package/CodeHighlighter/CodeHighlighterChunk.d.mts +42 -0
- package/CodeHighlighter/CodeHighlighterChunk.mjs +77 -0
- package/CodeHighlighter/CodeHighlighterClient.mjs +597 -128
- package/CodeHighlighter/CodeHighlighterContext.d.mts +57 -1
- package/CodeHighlighter/CodeHighlighterFallbackContext.d.mts +14 -2
- package/CodeHighlighter/CodeHighlighterFallbackContext.mjs +1 -3
- package/CodeHighlighter/CodeInitialSourceLoader.d.mts +10 -0
- package/CodeHighlighter/CodeInitialSourceLoader.mjs +108 -0
- package/CodeHighlighter/CodeSourceLoader.d.mts +11 -0
- package/CodeHighlighter/CodeSourceLoader.mjs +128 -0
- package/CodeHighlighter/buildCodeHighlighterChunkProps.d.mts +47 -0
- package/CodeHighlighter/buildCodeHighlighterChunkProps.mjs +61 -0
- package/CodeHighlighter/buildStringFallback.d.mts +29 -0
- package/CodeHighlighter/buildStringFallback.mjs +42 -0
- package/CodeHighlighter/codeToFallbackProps.d.mts +31 -2
- package/CodeHighlighter/codeToFallbackProps.mjs +347 -42
- package/CodeHighlighter/createClientProps.d.mts +17 -0
- package/CodeHighlighter/createClientProps.mjs +78 -0
- package/CodeHighlighter/errors.d.mts +6 -0
- package/CodeHighlighter/errors.mjs +10 -0
- package/CodeHighlighter/fallbackCompression.d.mts +96 -0
- package/CodeHighlighter/fallbackCompression.mjs +253 -0
- package/CodeHighlighter/fallbackFormat.d.mts +137 -0
- package/CodeHighlighter/fallbackFormat.mjs +422 -0
- package/CodeHighlighter/index.d.mts +4 -1
- package/CodeHighlighter/index.mjs +3 -1
- package/CodeHighlighter/mergeComments.d.mts +38 -0
- package/CodeHighlighter/mergeComments.mjs +80 -0
- package/CodeHighlighter/prepareInitialSource.d.mts +42 -0
- package/CodeHighlighter/prepareInitialSource.mjs +292 -0
- package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
- package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
- package/CodeHighlighter/types.d.mts +272 -8
- package/CodeHighlighter/useCodeFallback.d.mts +94 -0
- package/CodeHighlighter/useCodeFallback.mjs +204 -0
- package/CodeHighlighter/useGrammarsReady.d.mts +18 -0
- package/CodeHighlighter/useGrammarsReady.mjs +45 -0
- package/CodeHighlighter/useSpeculativeCodePreload.d.mts +26 -0
- package/CodeHighlighter/useSpeculativeCodePreload.mjs +40 -0
- package/CodeHighlighter/useSpeculativeEditingPreload.d.mts +33 -0
- package/CodeHighlighter/useSpeculativeEditingPreload.mjs +58 -0
- package/CodeHighlighter/useSpeculativeGrammarPreload.d.mts +23 -0
- package/CodeHighlighter/useSpeculativeGrammarPreload.mjs +31 -0
- package/CodeHighlighter/useSpeculativeUseCodePreload.d.mts +22 -0
- package/CodeHighlighter/useSpeculativeUseCodePreload.mjs +41 -0
- package/CodeProvider/CodeContext.d.mts +47 -12
- package/CodeProvider/CodeContext.mjs +7 -0
- package/CodeProvider/CodeProvider.d.mts +4 -2
- package/CodeProvider/CodeProvider.mjs +40 -102
- package/CodeProvider/CodeProviderLazy.d.mts +40 -0
- package/CodeProvider/CodeProviderLazy.mjs +96 -0
- package/CodeProvider/constants.d.mts +26 -0
- package/CodeProvider/constants.mjs +24 -0
- package/CodeProvider/createParseSourceWorkerClient.d.mts +6 -0
- package/CodeProvider/createParseSourceWorkerClient.mjs +22 -2
- package/CodeProvider/index.d.mts +2 -1
- package/CodeProvider/index.mjs +9 -1
- package/CodeProvider/parseSourceWorker.mjs +33 -0
- package/CodeProvider/useCodeProviderValue.d.mts +54 -0
- package/CodeProvider/useCodeProviderValue.mjs +188 -0
- package/CoordinatedLazy/ChunkServerLoader.d.mts +25 -0
- package/CoordinatedLazy/ChunkServerLoader.mjs +97 -0
- package/CoordinatedLazy/CoordinatedContentContext.d.mts +15 -0
- package/CoordinatedLazy/CoordinatedContentContext.mjs +22 -0
- package/CoordinatedLazy/CoordinatedFallbackContext.d.mts +11 -0
- package/CoordinatedLazy/CoordinatedFallbackContext.mjs +13 -0
- package/CoordinatedLazy/CoordinatedGateContext.d.mts +14 -0
- package/CoordinatedLazy/CoordinatedGateContext.mjs +19 -0
- package/CoordinatedLazy/CoordinatedLazy.d.mts +14 -0
- package/CoordinatedLazy/CoordinatedLazy.mjs +86 -0
- package/CoordinatedLazy/CoordinatedLazyClient.d.mts +24 -0
- package/CoordinatedLazy/CoordinatedLazyClient.mjs +65 -0
- package/CoordinatedLazy/LazyContent.d.mts +26 -0
- package/CoordinatedLazy/LazyContent.mjs +80 -0
- package/CoordinatedLazy/LazyContentServer.d.mts +18 -0
- package/CoordinatedLazy/LazyContentServer.mjs +25 -0
- package/CoordinatedLazy/buildChunkRenderInputs.d.mts +8 -0
- package/CoordinatedLazy/buildChunkRenderInputs.mjs +35 -0
- package/CoordinatedLazy/createCoordinatedLazy.d.mts +32 -0
- package/CoordinatedLazy/createCoordinatedLazy.mjs +127 -0
- package/CoordinatedLazy/index.d.mts +14 -0
- package/CoordinatedLazy/index.mjs +18 -0
- package/CoordinatedLazy/resolveChunkRender.d.mts +26 -0
- package/CoordinatedLazy/resolveChunkRender.mjs +73 -0
- package/CoordinatedLazy/types.d.mts +408 -0
- package/CoordinatedLazy/types.mjs +1 -0
- package/CoordinatedLazy/useChunk.d.mts +30 -0
- package/CoordinatedLazy/useChunk.mjs +135 -0
- package/CoordinatedLazy/useCoordinatedFallback.d.mts +12 -0
- package/CoordinatedLazy/useCoordinatedFallback.mjs +40 -0
- package/CoordinatedLazy/useCoordinatedSwap.d.mts +16 -0
- package/CoordinatedLazy/useCoordinatedSwap.mjs +124 -0
- package/LICENSE +1 -1
- package/abstractCreateDemo/abstractCreateDemo.d.mts +54 -3
- package/abstractCreateDemo/abstractCreateDemo.mjs +47 -7
- package/abstractCreateDemo/resolveDemoFlag.d.mts +20 -0
- package/abstractCreateDemo/resolveDemoFlag.mjs +25 -0
- package/abstractCreateStream/abstractCreateStream.d.mts +18 -0
- package/abstractCreateStream/abstractCreateStream.mjs +45 -0
- package/abstractCreateStream/index.d.mts +2 -0
- package/abstractCreateStream/index.mjs +1 -0
- package/abstractCreateStream/types.d.mts +34 -0
- package/abstractCreateStream/types.mjs +1 -0
- package/abstractCreateTypes/TypeCode.mjs +12 -11
- package/abstractCreateTypes/typesToJsx.mjs +30 -9
- package/cli/ensureDemoClients.mjs +4 -148
- package/cli/ensureDemoPages.d.mts +45 -0
- package/cli/ensureDemoPages.mjs +99 -0
- package/cli/fileUtils/index.d.mts +11 -0
- package/cli/fileUtils/index.mjs +48 -0
- package/cli/findDemoIndexFiles.d.mts +15 -0
- package/cli/findDemoIndexFiles.mjs +121 -0
- package/cli/index.mjs +1 -1
- package/cli/loadNextConfig.d.mts +25 -0
- package/cli/loadNextConfig.mjs +60 -1
- package/cli/runBrowser.mjs +1 -1
- package/cli/runValidate.mjs +44 -1
- package/package.json +84 -4
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +30 -0
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.d.mts +17 -0
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.mjs +52 -0
- package/pipeline/hastUtils/frameFallbackFromSpans.d.mts +18 -0
- package/pipeline/hastUtils/frameFallbackFromSpans.mjs +24 -0
- package/pipeline/hastUtils/hast.d.mts +27 -0
- package/pipeline/hastUtils/hastCompression.d.mts +3 -1
- package/pipeline/hastUtils/hastCompression.mjs +9 -1
- package/pipeline/hastUtils/hastDecompress.mjs +10 -4
- package/pipeline/hastUtils/hastDictionary.mjs +9 -0
- package/pipeline/hastUtils/hastUtils.d.mts +4 -3
- package/pipeline/hastUtils/hastUtils.mjs +24 -12
- package/pipeline/hastUtils/index.d.mts +2 -1
- package/pipeline/hastUtils/index.mjs +2 -1
- package/pipeline/hastUtils/stripHighlightingSpans.d.mts +6 -2
- package/pipeline/hastUtils/stripHighlightingSpans.mjs +22 -10
- package/pipeline/lintJavascriptDemoFocus/lintJavascriptDemoFocus.mjs +10 -7
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.d.mts +31 -13
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.mjs +50 -55
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.d.mts +78 -0
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.mjs +405 -0
- package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.d.mts +5 -5
- package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.mjs +36 -66
- package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.d.mts +23 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs +92 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSource.d.mts +19 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSource.mjs +25 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.d.mts +17 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.mjs +26 -0
- package/pipeline/loadIsomorphicCodeVariant/diffHast.d.mts +26 -2
- package/pipeline/loadIsomorphicCodeVariant/diffHast.mjs +563 -19
- package/pipeline/loadIsomorphicCodeVariant/embedTransforms.d.mts +49 -0
- package/pipeline/loadIsomorphicCodeVariant/embedTransforms.mjs +152 -0
- package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.d.mts +51 -0
- package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.mjs +161 -0
- package/pipeline/loadIsomorphicCodeVariant/flattenCodeVariant.mjs +6 -3
- package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.d.mts +12 -0
- package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs +44 -0
- package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.d.mts +16 -0
- package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +74 -0
- package/pipeline/loadIsomorphicCodeVariant/loadCodeFallback.mjs +17 -5
- package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +229 -15
- package/pipeline/loadIsomorphicCodeVariant/transformSource.d.mts +2 -2
- package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +56 -22
- package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.d.mts +18 -0
- package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.mjs +11 -7
- package/pipeline/loadServerTypes/hastTypeUtils.d.mts +2 -2
- package/pipeline/loadServerTypes/hastTypeUtils.mjs +4 -4
- package/pipeline/loadServerTypes/loadServerTypes.mjs +1 -1
- package/pipeline/loadServerTypesMeta/extractJSDocText.d.mts +14 -0
- package/pipeline/loadServerTypesMeta/extractJSDocText.mjs +60 -0
- package/pipeline/loadServerTypesMeta/processTypes.mjs +43 -46
- package/pipeline/loadServerTypesText/order.mjs +1 -1
- package/pipeline/loadServerTypesText/parseTypesMarkdown.mjs +3 -1
- package/pipeline/loaderUtils/index.d.mts +0 -1
- package/pipeline/loaderUtils/index.mjs +0 -1
- package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -1
- package/pipeline/loaderUtils/parseImportsAndComments.mjs +19 -9
- package/pipeline/loaderUtils/resolveModulePath.mjs +23 -1
- package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.d.mts +12 -0
- package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs +17 -13
- package/pipeline/parseSource/addLineGutters.mjs +45 -11
- package/pipeline/parseSource/calculateFrameRanges.d.mts +22 -0
- package/pipeline/parseSource/calculateFrameRanges.mjs +69 -25
- package/pipeline/parseSource/detectGrammarScopes.d.mts +13 -0
- package/pipeline/parseSource/detectGrammarScopes.mjs +35 -0
- package/pipeline/parseSource/extendSyntaxTokens.mjs +501 -43
- package/pipeline/parseSource/frameVisibility.d.mts +47 -0
- package/pipeline/parseSource/frameVisibility.mjs +114 -0
- package/pipeline/parseSource/grammarCache.d.mts +33 -0
- package/pipeline/parseSource/grammarCache.mjs +73 -0
- package/pipeline/parseSource/grammarLoaders.d.mts +14 -0
- package/pipeline/parseSource/grammarLoaders.mjs +24 -0
- package/pipeline/parseSource/grammarMaps.d.mts +21 -1
- package/pipeline/parseSource/grammarMaps.mjs +36 -0
- package/pipeline/parseSource/isFrameSpan.d.mts +19 -0
- package/pipeline/parseSource/isFrameSpan.mjs +24 -0
- package/pipeline/parseSource/parseSource.d.mts +41 -6
- package/pipeline/parseSource/parseSource.mjs +184 -36
- package/pipeline/parseSource/redistributeFrameFallbacks.d.mts +40 -0
- package/pipeline/parseSource/redistributeFrameFallbacks.mjs +138 -0
- package/pipeline/parseSource/restructureFrames.d.mts +5 -0
- package/pipeline/parseSource/restructureFrames.mjs +179 -16
- package/pipeline/syncPageIndex/metadataToMarkdown.mjs +6 -2
- package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.d.mts +26 -0
- package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +181 -114
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.d.mts +12 -0
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.mjs +52 -0
- package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +22 -1
- package/pipeline/transformTypescriptToJavascript/removeTypes.d.mts +5 -8
- package/pipeline/transformTypescriptToJavascript/removeTypes.mjs +27 -93
- package/useCode/EditableEngine.d.mts +233 -0
- package/useCode/EditableEngine.mjs +1712 -0
- package/useCode/EditingEngine.d.mts +13 -0
- package/useCode/EditingEngine.mjs +14 -0
- package/useCode/Pre.browser.mjs +5 -1
- package/useCode/Pre.d.mts +127 -1
- package/useCode/Pre.mjs +417 -165
- package/useCode/SourceEditingEngine.d.mts +50 -0
- package/useCode/SourceEditingEngine.mjs +461 -0
- package/useCode/TransformEngine.d.mts +39 -0
- package/useCode/TransformEngine.mjs +208 -0
- package/useCode/editingEngineCache.d.mts +29 -0
- package/useCode/editingEngineCache.mjs +68 -0
- package/useCode/sourceLineCounts.d.mts +80 -0
- package/useCode/sourceLineCounts.mjs +284 -0
- package/useCode/subscribeToggleNudge.d.mts +3 -0
- package/useCode/subscribeToggleNudge.mjs +95 -0
- package/useCode/transformEngineCache.d.mts +21 -0
- package/useCode/transformEngineCache.mjs +60 -0
- package/useCode/useCode.d.mts +140 -1
- package/useCode/useCode.mjs +250 -19
- package/useCode/useCodeUtils.d.mts +131 -20
- package/useCode/useCodeUtils.mjs +267 -194
- package/useCode/useCopyFunctionality.d.mts +13 -1
- package/useCode/useCopyFunctionality.mjs +39 -9
- package/useCode/useEditable.browser.mjs +10 -2
- package/useCode/useEditable.d.mts +27 -106
- package/useCode/useEditable.integration.browser.d.mts +1 -0
- package/useCode/useEditable.integration.browser.mjs +870 -0
- package/useCode/useEditable.mjs +198 -1247
- package/useCode/useEditableUtils.d.mts +50 -1
- package/useCode/useEditableUtils.mjs +29 -0
- package/useCode/useFileNavigation.d.mts +91 -3
- package/useCode/useFileNavigation.mjs +201 -41
- package/useCode/useHighlightGate.d.mts +17 -0
- package/useCode/useHighlightGate.mjs +147 -0
- package/useCode/useSourceEditing.d.mts +8 -0
- package/useCode/useSourceEditing.mjs +158 -314
- package/useCode/useSourceEnhancing.d.mts +5 -1
- package/useCode/useSourceEnhancing.mjs +22 -36
- package/useCode/useTransformManagement.d.mts +93 -5
- package/useCode/useTransformManagement.mjs +496 -28
- package/useCode/useTransitionPhase.d.mts +24 -0
- package/useCode/useTransitionPhase.mjs +49 -0
- package/useCode/useUIState.d.mts +2 -2
- package/useCode/useUIState.mjs +8 -8
- package/useCode/useVariantSelection.d.mts +130 -6
- package/useCode/useVariantSelection.mjs +529 -93
- package/useCodeWindow/useCodeWindow.d.mts +19 -2
- package/useCodeWindow/useCodeWindow.mjs +98 -71
- package/useCoordinated/coordinatePreference.d.mts +439 -0
- package/useCoordinated/coordinatePreference.mjs +951 -0
- package/useCoordinated/coordinatePreference.testUtils.d.mts +21 -0
- package/useCoordinated/coordinatePreference.testUtils.mjs +69 -0
- package/useCoordinated/createSettleGate.d.mts +96 -0
- package/useCoordinated/createSettleGate.mjs +171 -0
- package/useCoordinated/index.d.mts +8 -0
- package/useCoordinated/index.mjs +8 -0
- package/useCoordinated/layoutShiftGate.d.mts +24 -0
- package/useCoordinated/layoutShiftGate.mjs +79 -0
- package/useCoordinated/pageSettleGate.d.mts +11 -0
- package/useCoordinated/pageSettleGate.mjs +13 -0
- package/useCoordinated/scheduleTasks.d.mts +23 -0
- package/useCoordinated/scheduleTasks.mjs +45 -0
- package/useCoordinated/useCoordinated.d.mts +193 -0
- package/useCoordinated/useCoordinated.mjs +469 -0
- package/useCoordinated/useCoordinatedLazy.d.mts +17 -0
- package/useCoordinated/useCoordinatedLazy.mjs +38 -0
- package/useCoordinated/useCoordinatedLocalStorage.d.mts +16 -0
- package/useCoordinated/useCoordinatedLocalStorage.mjs +22 -0
- package/useCoordinated/useCoordinatedPreference.d.mts +20 -0
- package/useCoordinated/useCoordinatedPreference.mjs +26 -0
- package/useCoordinated/useSettleGate.d.mts +11 -0
- package/useCoordinated/useSettleGate.mjs +34 -0
- package/useDemo/exportVariant.d.mts +12 -5
- package/useDemo/exportVariant.mjs +59 -5
- package/useDemo/useDemo.d.mts +5 -2
- package/useScrollAnchor/useScrollAnchor.mjs +28 -5
- package/useStream/index.d.mts +6 -0
- package/useStream/index.mjs +6 -0
- package/useStream/streamChunks.d.mts +23 -0
- package/useStream/streamChunks.mjs +85 -0
- package/useStream/types.d.mts +45 -0
- package/useStream/types.mjs +1 -0
- package/useStream/useStream.d.mts +57 -0
- package/useStream/useStream.mjs +119 -0
- package/useStream/useStreamController.d.mts +15 -0
- package/useStream/useStreamController.mjs +90 -0
- package/withDocsInfra/withDocsInfra.d.mts +19 -0
- package/withDocsInfra/withDocsInfra.mjs +13 -5
- package/pipeline/loaderUtils/convertCommentsToOneIndexed.d.mts +0 -8
- package/pipeline/loaderUtils/convertCommentsToOneIndexed.mjs +0 -16
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import { performanceMeasure } from "../pipeline/loadPrecomputedCodeHighlighter/performanceLogger.mjs";
|
|
2
|
+
// `yieldToMain` is used here to push a user-supplied `preload` into a fresh
|
|
3
|
+
// macrotask so the browser can paint the just-announced loading state before the
|
|
4
|
+
// (potentially CPU-bound) preload monopolizes the main thread.
|
|
5
|
+
import { yieldToMain } from "./scheduleTasks.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generic same-tab preference coordinator. Its primary purpose is
|
|
9
|
+
* to **fold many concurrent value changes into a single layout-shift
|
|
10
|
+
* commit**: sibling component instances that share a `channelKey`
|
|
11
|
+
* coordinate so the visible flip happens together rather than as a
|
|
12
|
+
* cascade of independent re-layouts.
|
|
13
|
+
*
|
|
14
|
+
* Each peer self-classifies a given target value via
|
|
15
|
+
* `causesLayoutShift` (consulted for non-originator peers only;
|
|
16
|
+
* originators always take the barrier path so a user click always
|
|
17
|
+
* feels coordinated):
|
|
18
|
+
*
|
|
19
|
+
* - **`causesLayoutShift(target) === true`** — the peer joins a
|
|
20
|
+
* channel-wide barrier. Every joining peer's `preload` runs
|
|
21
|
+
* serially across the channel (so no main-thread contention
|
|
22
|
+
* while we prepare the swap), and all `onCommit`s fire together
|
|
23
|
+
* in a single microtask once everyone is ready. Use for changes
|
|
24
|
+
* that visibly resize content (collapse/expand, code transforms,
|
|
25
|
+
* image swaps with different aspect ratios).
|
|
26
|
+
* - **`causesLayoutShift(target) === false`** — the peer runs its
|
|
27
|
+
* `preload`+`commit` on its own self-serial chain. Multiple
|
|
28
|
+
* peers' lazy chains run concurrently with each other and with
|
|
29
|
+
* any in-flight barrier. Use for changes that are visually
|
|
30
|
+
* non-disruptive (e.g. updating a value that only shows on hover).
|
|
31
|
+
*
|
|
32
|
+
* Different peers may classify the same target differently — each
|
|
33
|
+
* peer's classification governs only that peer's path through the
|
|
34
|
+
* coordinator.
|
|
35
|
+
*
|
|
36
|
+
* **Cross-tab behavior is intentionally out of scope.** Tabs sync via
|
|
37
|
+
* the underlying state primitive (`useLocalStorageState` etc.); this
|
|
38
|
+
* coordinator only handles peers in the same JS context. A receiving
|
|
39
|
+
* tab independently runs its own barrier across its local peers,
|
|
40
|
+
* which is sequenced naturally after the originator's commit because
|
|
41
|
+
* the originator defers the underlying `setValue` write until its
|
|
42
|
+
* own barrier commits (see `useCoordinated`).
|
|
43
|
+
*
|
|
44
|
+
* No React, no DOM, no BroadcastChannel — pure module-scoped state
|
|
45
|
+
* suitable for any state primitive.
|
|
46
|
+
*
|
|
47
|
+
* **Browser-only.** All state (channels, barriers, lazy queues) is
|
|
48
|
+
* held in module scope and would persist across requests if this
|
|
49
|
+
* module were ever evaluated in a long-lived server-side runtime.
|
|
50
|
+
* The consuming surface is the `useCoordinated` React hook, whose
|
|
51
|
+
* `registerPeer`/`announceTarget` calls are gated behind
|
|
52
|
+
* `useLayoutEffect`/event handlers and therefore never reached
|
|
53
|
+
* during SSR. Do not import this module from server-side code
|
|
54
|
+
* paths that fan out per request.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** Identifier assigned to each peer at registration time. */
|
|
58
|
+
|
|
59
|
+
/** Channel scope. Peers sharing a `channelKey` coordinate with each other. */
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sentinel returned by the coordinator's `announceTarget` when no
|
|
63
|
+
* commit is needed for this peer (e.g. its current value already
|
|
64
|
+
* matches the target). Always-defined to keep the API ergonomic.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
const DEFAULT_MIN_WAIT_MS = 0;
|
|
68
|
+
const DEFAULT_GRACE_PERIOD_MS = 300;
|
|
69
|
+
const DEFAULT_ULTIMATE_TIMEOUT_MS = 10_000;
|
|
70
|
+
const PERF_FUNCTION_NAME = 'Coordinate Preference';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detects a browser-like host. Used by the public entry points to
|
|
74
|
+
* no-op when the module is reached outside the browser. The
|
|
75
|
+
* consuming `useCoordinated` hook already gates its calls behind
|
|
76
|
+
* `useLayoutEffect` and event handlers (which never run on the
|
|
77
|
+
* server), but this is defense-in-depth: a stray non-effect caller
|
|
78
|
+
* would otherwise leak module-state across SSR requests as warned
|
|
79
|
+
* in the file header. Detection runs once at module-evaluation time
|
|
80
|
+
* — re-evaluating per call would pessimize the hot path and the
|
|
81
|
+
* runtime can't switch between server and browser mid-process.
|
|
82
|
+
*/
|
|
83
|
+
const IS_BROWSER_HOST = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
84
|
+
const NOOP_UNREGISTER = () => {};
|
|
85
|
+
const NOOP_ANNOUNCE_HANDLE = {
|
|
86
|
+
cancel: () => {},
|
|
87
|
+
settled: Promise.resolve()
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fired on a registered peer when *another* peer in the same channel
|
|
92
|
+
* calls `announceTarget`. Lets a peer learn about a sibling-driven
|
|
93
|
+
* change without having to wait for the underlying state primitive
|
|
94
|
+
* (e.g. `useLocalStorageState`) to echo the new value back — that
|
|
95
|
+
* echo only happens after the originator commits, which itself is
|
|
96
|
+
* gated on every sibling joining the barrier. Without this hook
|
|
97
|
+
* sibling peers would deadlock the barrier until
|
|
98
|
+
* `ultimateTimeoutMs` for any same-tab coordination where the
|
|
99
|
+
* underlying primitive only notifies after the originator's write.
|
|
100
|
+
*
|
|
101
|
+
* Implementations should typically call into their local equivalent
|
|
102
|
+
* of `runCoordination(target, isOriginator=false)` so the peer
|
|
103
|
+
* joins the active barrier (or kicks off its own lazy chain on the
|
|
104
|
+
* same wall-clock window). Implementations must be idempotent for
|
|
105
|
+
* repeated calls with the same `target` because notifications can
|
|
106
|
+
* fan out from each subsequent join.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
const channels = new Map();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Override hook for tests that need a deterministic target encoder
|
|
113
|
+
* (e.g. when the value type contains unstable references). Production
|
|
114
|
+
* callers should leave this alone — the default `JSON.stringify` is
|
|
115
|
+
* sufficient for primitive and plain-object values.
|
|
116
|
+
*/
|
|
117
|
+
let encodeTargetImpl = value => {
|
|
118
|
+
if (value === null) {
|
|
119
|
+
return '\u0000null';
|
|
120
|
+
}
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return '\u0000undefined';
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'string') {
|
|
125
|
+
return `s:${value}`;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
128
|
+
return `p:${String(value)}`;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
return `j:${JSON.stringify(value)}`;
|
|
132
|
+
} catch {
|
|
133
|
+
return `o:${Object.prototype.toString.call(value)}`;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set a custom target encoder. Returns a function that restores the
|
|
139
|
+
* previous encoder. Intended for tests; consumed via
|
|
140
|
+
* `coordinatePreference.testUtils`.
|
|
141
|
+
*/
|
|
142
|
+
function setTargetEncoder(impl) {
|
|
143
|
+
const previous = encodeTargetImpl;
|
|
144
|
+
encodeTargetImpl = impl;
|
|
145
|
+
return () => {
|
|
146
|
+
encodeTargetImpl = previous;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function encodeTarget(value) {
|
|
150
|
+
return encodeTargetImpl(value);
|
|
151
|
+
}
|
|
152
|
+
function getOrCreateChannel(channelKey) {
|
|
153
|
+
let channel = channels.get(channelKey);
|
|
154
|
+
if (!channel) {
|
|
155
|
+
channel = {
|
|
156
|
+
channelKey,
|
|
157
|
+
peers: new Map(),
|
|
158
|
+
hasEverAnnounced: false,
|
|
159
|
+
barrierTail: Promise.resolve(),
|
|
160
|
+
pendingBarriers: new Map()
|
|
161
|
+
};
|
|
162
|
+
channels.set(channelKey, channel);
|
|
163
|
+
}
|
|
164
|
+
return channel;
|
|
165
|
+
}
|
|
166
|
+
function disposeChannelIfEmpty(channel) {
|
|
167
|
+
if (channel.peers.size === 0 && channel.pendingBarriers.size === 0) {
|
|
168
|
+
channels.delete(channel.channelKey);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Register a peer with a channel. Returns an `unregister` function
|
|
174
|
+
* that removes the peer; calling it cancels any in-flight lazy-path
|
|
175
|
+
* work owned by the peer and drops it from any open barrier-path barriers.
|
|
176
|
+
*
|
|
177
|
+
* Pass `onSiblingAnnounce` to learn about target announcements made
|
|
178
|
+
* by other peers on the channel — this is what lets a peer join the
|
|
179
|
+
* originator's barrier window without waiting for the underlying
|
|
180
|
+
* state primitive to echo the new value (which only happens after
|
|
181
|
+
* the originator commits, creating a deadlock when every peer is
|
|
182
|
+
* waiting on it).
|
|
183
|
+
*/
|
|
184
|
+
export function registerPeer(channelKey, peerId, onSiblingAnnounce) {
|
|
185
|
+
if (!IS_BROWSER_HOST) {
|
|
186
|
+
return NOOP_UNREGISTER;
|
|
187
|
+
}
|
|
188
|
+
const channel = getOrCreateChannel(channelKey);
|
|
189
|
+
if (channel.peers.has(peerId)) {
|
|
190
|
+
throw /* minify-error */new Error(`coordinatePreference: peer '${peerId}' is already registered on channel '${channelKey}'. ` + 'Each peer must have a unique id within a channel. ' + 'See https://mui.com/r/docs-infra-coordinate-preference for more info.');
|
|
191
|
+
}
|
|
192
|
+
const peer = {
|
|
193
|
+
id: peerId,
|
|
194
|
+
currentValue: {
|
|
195
|
+
has: false
|
|
196
|
+
},
|
|
197
|
+
onSiblingAnnounce: onSiblingAnnounce,
|
|
198
|
+
lazyInFlight: new Map(),
|
|
199
|
+
lazyQueue: [],
|
|
200
|
+
lazyActive: false
|
|
201
|
+
};
|
|
202
|
+
channel.peers.set(peerId, peer);
|
|
203
|
+
|
|
204
|
+
// Replay every open barrier's announcement to the newcomer so it
|
|
205
|
+
// joins the quorum (as waiter or skipped) instead of stalling the
|
|
206
|
+
// barrier until its `ultimateTimer` fires. Without this fan-out,
|
|
207
|
+
// a peer that registers after a barrier has opened never learns
|
|
208
|
+
// about the in-flight target: `maybeResolveBarrier` keeps blocking
|
|
209
|
+
// on `waiters + skipped >= peers.size` while the newcomer sits
|
|
210
|
+
// idle (it has nothing to receive against because no one announced
|
|
211
|
+
// *to* it). The newcomer's own `onSiblingAnnounce` is the exact
|
|
212
|
+
// hook the within-tab `notifySiblings` path uses for the analogous
|
|
213
|
+
// already-registered case, so reusing it keeps both paths in sync.
|
|
214
|
+
//
|
|
215
|
+
// Deferred to a microtask because consumers register from
|
|
216
|
+
// `useInsertionEffect` (where React forbids scheduling updates),
|
|
217
|
+
// and the announce callback ultimately drives `setState` through
|
|
218
|
+
// `runCoordination`. The microtask still fires before any paint so
|
|
219
|
+
// the barrier sees the newcomer join the same flush.
|
|
220
|
+
if (onSiblingAnnounce && channel.pendingBarriers.size > 0) {
|
|
221
|
+
const callback = onSiblingAnnounce;
|
|
222
|
+
const barriersSnapshot = Array.from(channel.pendingBarriers.values());
|
|
223
|
+
queueMicrotask(() => {
|
|
224
|
+
if (channel.peers.get(peerId) !== peer) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
for (const barrier of barriersSnapshot) {
|
|
228
|
+
const barrierKey = encodeTarget(barrier.target);
|
|
229
|
+
if (channel.pendingBarriers.get(barrierKey) !== barrier) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// Mirror the `notifySiblings` skip rule: if the newcomer has
|
|
233
|
+
// already reported a committed value matching the barrier
|
|
234
|
+
// target (e.g. `useTransformManagement` persists the
|
|
235
|
+
// transform before dispatch, so a late-mounting demo reads
|
|
236
|
+
// the target immediately and calls `reportValue` with it
|
|
237
|
+
// during its own insertion-effect), record it as skipped
|
|
238
|
+
// rather than firing the receiver flow. Without this, the
|
|
239
|
+
// replayed announce can re-enter `runCoordination`, get
|
|
240
|
+
// classified as layout-shifting, and turn a peer that
|
|
241
|
+
// should stay skipped into a waiter — needlessly extending
|
|
242
|
+
// the barrier and the preload work.
|
|
243
|
+
if (peer.currentValue.has && Object.is(peer.currentValue.value, barrier.target)) {
|
|
244
|
+
if (!barrier.waiters.has(peerId) && !barrier.skipped.has(peerId)) {
|
|
245
|
+
barrier.skipped.add(peerId);
|
|
246
|
+
maybeResolveBarrier(channel, barrier);
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (barrier.waiters.has(peerId) || barrier.skipped.has(peerId)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
callback(barrier.target);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.error(`[docs-infra/coordinatePreference] onSiblingAnnounce on register for peer ` + `'${peerId}' on channel '${channelKey}' threw:`, err);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return () => {
|
|
262
|
+
const stillPresent = channel.peers.get(peerId);
|
|
263
|
+
if (stillPresent !== peer) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
channel.peers.delete(peerId);
|
|
267
|
+
for (const controller of peer.lazyInFlight.keys()) {
|
|
268
|
+
controller.abort();
|
|
269
|
+
}
|
|
270
|
+
peer.lazyInFlight.clear();
|
|
271
|
+
peer.lazyQueue.length = 0;
|
|
272
|
+
peer.lazyActive = false;
|
|
273
|
+
// Drop this peer from any open barriers and re-check resolution.
|
|
274
|
+
// We must also clear it from `skipped` so a later replacement peer
|
|
275
|
+
// registering with the same coordinates isn't silently "covered"
|
|
276
|
+
// by the stale entry — the quorum check counts
|
|
277
|
+
// `waiters + skipped` against the current peer set, so a leftover
|
|
278
|
+
// skipped id could let a barrier commit without the new peer ever
|
|
279
|
+
// joining. We re-check every barrier (not just ones the peer was
|
|
280
|
+
// a waiter on) because shrinking `channel.peers.size` can make a
|
|
281
|
+
// barrier resolvable even when the departing peer had only
|
|
282
|
+
// registered and never joined — otherwise the barrier would sit
|
|
283
|
+
// open until the next unrelated event or the ultimate timeout.
|
|
284
|
+
for (const barrier of channel.pendingBarriers.values()) {
|
|
285
|
+
const waiter = barrier.waiters.get(peerId);
|
|
286
|
+
if (waiter) {
|
|
287
|
+
waiter.abort.abort();
|
|
288
|
+
barrier.waiters.delete(peerId);
|
|
289
|
+
}
|
|
290
|
+
barrier.skipped.delete(peerId);
|
|
291
|
+
maybeResolveBarrier(channel, barrier);
|
|
292
|
+
}
|
|
293
|
+
disposeChannelIfEmpty(channel);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Report a peer's current value to the coordinator. Used to exclude
|
|
299
|
+
* already-at-target peers from barrier expectations.
|
|
300
|
+
*/
|
|
301
|
+
export function reportValue(channelKey, peerId, currentValue) {
|
|
302
|
+
const channel = channels.get(channelKey);
|
|
303
|
+
if (!channel) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const peer = channel.peers.get(peerId);
|
|
307
|
+
if (!peer) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
peer.currentValue = {
|
|
311
|
+
has: true,
|
|
312
|
+
value: currentValue
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// A peer that is already committed to an open barrier's target does
|
|
316
|
+
// not need to announce again. Mark it as satisfied so the barrier
|
|
317
|
+
// doesn't wait for a no-op receiver flow that will never run.
|
|
318
|
+
for (const barrier of channel.pendingBarriers.values()) {
|
|
319
|
+
if (barrier.waiters.has(peerId)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!Object.is(barrier.target, currentValue)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
barrier.skipped.add(peerId);
|
|
326
|
+
maybeResolveBarrier(channel, barrier);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Announce a target value for this peer. Routes into the barrier or
|
|
332
|
+
* lazy path based on `causesLayoutShift(target)`.
|
|
333
|
+
*
|
|
334
|
+
* For the barrier path: the peer joins the channel-wide barrier for
|
|
335
|
+
* this target (creating it if needed), enqueues its `preload` into
|
|
336
|
+
* the channel's serial queue, and awaits the barrier's batched
|
|
337
|
+
* commit.
|
|
338
|
+
*
|
|
339
|
+
* For the lazy path: the peer enqueues `preload` + `onCommit` onto
|
|
340
|
+
* its own self-serial chain and returns immediately. Multiple peers'
|
|
341
|
+
* lazy chains run concurrently with each other and with any
|
|
342
|
+
* in-flight barrier work.
|
|
343
|
+
*/
|
|
344
|
+
export function announceTarget(channelKey, peerId, target, options) {
|
|
345
|
+
if (!IS_BROWSER_HOST) {
|
|
346
|
+
return NOOP_ANNOUNCE_HANDLE;
|
|
347
|
+
}
|
|
348
|
+
const channel = getOrCreateChannel(channelKey);
|
|
349
|
+
const peer = channel.peers.get(peerId);
|
|
350
|
+
if (!peer) {
|
|
351
|
+
throw /* minify-error */new Error(`coordinatePreference: peer '${peerId}' is not registered on channel '${channelKey}'. ` + 'Call `registerPeer` before `announceTarget`. ' + 'See https://mui.com/r/docs-infra-coordinate-preference for more info.');
|
|
352
|
+
}
|
|
353
|
+
if (options.isOriginator || options.causesLayoutShift(target)) {
|
|
354
|
+
channel.hasEverAnnounced = true;
|
|
355
|
+
performanceMeasure(undefined, {
|
|
356
|
+
mark: 'announce-barrier',
|
|
357
|
+
measure: 'announce-barrier'
|
|
358
|
+
}, [PERF_FUNCTION_NAME, channelKey, peerId]);
|
|
359
|
+
const handle = joinOrOpenBarrier(channel, peer, target, options);
|
|
360
|
+
notifySiblings(channel, peer, target);
|
|
361
|
+
return handle;
|
|
362
|
+
}
|
|
363
|
+
channel.hasEverAnnounced = true;
|
|
364
|
+
performanceMeasure(undefined, {
|
|
365
|
+
mark: 'announce-lazy',
|
|
366
|
+
measure: 'announce-lazy'
|
|
367
|
+
}, [PERF_FUNCTION_NAME, channelKey, peerId]);
|
|
368
|
+
const handle = enqueueLazy(channel, peer, target, options);
|
|
369
|
+
notifySiblings(channel, peer, target);
|
|
370
|
+
return handle;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Synchronously fan out a target announcement to every other peer on
|
|
375
|
+
* the channel whose state is known to differ from the target (or
|
|
376
|
+
* whose state is unknown). Peers that have already joined the active
|
|
377
|
+
* barrier for this target (as waiter or skipped) are excluded so we
|
|
378
|
+
* don't re-enter their `runCoordination` and cancel the in-flight
|
|
379
|
+
* announcement we're about to satisfy.
|
|
380
|
+
*
|
|
381
|
+
* This is the within-tab analogue of cross-tab storage echoes: it
|
|
382
|
+
* lets sibling peers join the originator's barrier window before the
|
|
383
|
+
* originator has written through to the underlying state primitive.
|
|
384
|
+
* Without it, two sibling peers sharing a `useLocalStorageState` (or
|
|
385
|
+
* any other primitive that only fires on commit) would deadlock the
|
|
386
|
+
* barrier until `ultimateTimeoutMs`.
|
|
387
|
+
*/
|
|
388
|
+
function notifySiblings(channel, announcer, target) {
|
|
389
|
+
if (channel.peers.size <= 1) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const barrierKey = encodeTarget(target);
|
|
393
|
+
const barrier = channel.pendingBarriers.get(barrierKey);
|
|
394
|
+
// Snapshot the peer list first — a callback may register or
|
|
395
|
+
// unregister peers reentrantly which would invalidate live iteration.
|
|
396
|
+
const siblings = [];
|
|
397
|
+
for (const otherPeer of channel.peers.values()) {
|
|
398
|
+
if (otherPeer.id === announcer.id) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!otherPeer.onSiblingAnnounce) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (otherPeer.currentValue.has && Object.is(otherPeer.currentValue.value, target)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (barrier && (barrier.waiters.has(otherPeer.id) || barrier.skipped.has(otherPeer.id))) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
siblings.push(otherPeer);
|
|
411
|
+
}
|
|
412
|
+
for (const otherPeer of siblings) {
|
|
413
|
+
try {
|
|
414
|
+
otherPeer.onSiblingAnnounce(target);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
console.error(`[docs-infra/coordinatePreference] onSiblingAnnounce for peer '${otherPeer.id}' on channel ` + `'${channel.channelKey}' threw:`, err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function joinOrOpenBarrier(channel, peer, target, options) {
|
|
421
|
+
const barrierKey = encodeTarget(target);
|
|
422
|
+
let barrier = channel.pendingBarriers.get(barrierKey);
|
|
423
|
+
const announceTime = options.announceTime;
|
|
424
|
+
const minWaitMs = (options.minWaitMs ?? DEFAULT_MIN_WAIT_MS) + (channel.peers.size > 1 ? options.multiPeerExtraMinWaitMs ?? 0 : 0);
|
|
425
|
+
const gracePeriodMs = options.gracePeriodMs ?? DEFAULT_GRACE_PERIOD_MS;
|
|
426
|
+
const ultimateTimeoutMs = options.ultimateTimeoutMs ?? DEFAULT_ULTIMATE_TIMEOUT_MS;
|
|
427
|
+
if (!barrier) {
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
const minRemaining = Math.max(0, announceTime + minWaitMs - now);
|
|
430
|
+
const waitingRemaining = Math.max(minRemaining, announceTime + minWaitMs + gracePeriodMs - now);
|
|
431
|
+
const ultimateRemaining = Math.max(waitingRemaining, announceTime + ultimateTimeoutMs - now);
|
|
432
|
+
const created = {
|
|
433
|
+
target,
|
|
434
|
+
announceTime,
|
|
435
|
+
waiters: new Map(),
|
|
436
|
+
skipped: new Set(),
|
|
437
|
+
minWaitPassed: minRemaining === 0,
|
|
438
|
+
minWaitTimer: setTimeout(() => {
|
|
439
|
+
const current = channel.pendingBarriers.get(barrierKey);
|
|
440
|
+
if (!current || current !== barrier) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
current.minWaitPassed = true;
|
|
444
|
+
maybeResolveBarrier(channel, current);
|
|
445
|
+
}, minRemaining),
|
|
446
|
+
waitingForPeersTimer: setTimeout(() => {
|
|
447
|
+
notifyWaitingForPeers(channel, barrierKey);
|
|
448
|
+
}, waitingRemaining),
|
|
449
|
+
waitingForPeersNotified: false,
|
|
450
|
+
ultimateTimer: setTimeout(() => {
|
|
451
|
+
const current = channel.pendingBarriers.get(barrierKey);
|
|
452
|
+
if (!current) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
console.warn(`[docs-infra/coordinatePreference] Barrier on channel '${channel.channelKey}' ` + `force-resolved after ${ultimateTimeoutMs}ms; ` + `${current.waiters.size} waiter(s) still pending. ` + 'A peer likely unmounted or crashed mid-preload.');
|
|
456
|
+
forceResolveBarrier(channel, barrierKey);
|
|
457
|
+
}, ultimateRemaining),
|
|
458
|
+
ultimateTimeoutMs,
|
|
459
|
+
deferredLazyReleases: []
|
|
460
|
+
};
|
|
461
|
+
barrier = created;
|
|
462
|
+
channel.pendingBarriers.set(barrierKey, barrier);
|
|
463
|
+
created.openMark = performanceMeasure(undefined, {
|
|
464
|
+
mark: 'barrier-open',
|
|
465
|
+
measure: 'barrier-open'
|
|
466
|
+
}, [PERF_FUNCTION_NAME, channel.channelKey, barrierKey]);
|
|
467
|
+
// Peers that already routed to the lazy path for *this same
|
|
468
|
+
// target* shouldn't gate the new barrier — they'll commit
|
|
469
|
+
// lazily on their own clock and we'd otherwise wait for a peer
|
|
470
|
+
// that has no intention of joining. Crucially, we must NOT skip
|
|
471
|
+
// a peer whose pending lazy work is for a *different* target:
|
|
472
|
+
// the upcoming `notifySiblings` call will pull that peer onto
|
|
473
|
+
// the new barrier, but if the barrier is also zero-wait with a
|
|
474
|
+
// synchronous originator preload it can `maybeResolveBarrier`
|
|
475
|
+
// before that notification runs, leaving the peer stranded on
|
|
476
|
+
// the wrong value.
|
|
477
|
+
for (const otherPeer of channel.peers.values()) {
|
|
478
|
+
if (otherPeer.id === peer.id) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (otherPeer.currentValue.has && Object.is(otherPeer.currentValue.value, target)) {
|
|
482
|
+
created.skipped.add(otherPeer.id);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
for (const lazyTarget of otherPeer.lazyInFlight.values()) {
|
|
486
|
+
if (Object.is(lazyTarget, target)) {
|
|
487
|
+
created.skipped.add(otherPeer.id);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
let settleResolver = () => {};
|
|
494
|
+
const settled = new Promise(resolve => {
|
|
495
|
+
settleResolver = resolve;
|
|
496
|
+
});
|
|
497
|
+
const abort = new AbortController();
|
|
498
|
+
const waiter = {
|
|
499
|
+
peerId: peer.id,
|
|
500
|
+
isOriginator: options.isOriginator,
|
|
501
|
+
preloaded: {
|
|
502
|
+
has: false
|
|
503
|
+
},
|
|
504
|
+
onCommit: options.onCommit,
|
|
505
|
+
onWaitingForPeers: options.onWaitingForPeers,
|
|
506
|
+
settle: settleResolver,
|
|
507
|
+
abort
|
|
508
|
+
};
|
|
509
|
+
barrier.waiters.set(peer.id, waiter);
|
|
510
|
+
barrier.skipped.delete(peer.id);
|
|
511
|
+
|
|
512
|
+
// If grace already fired, fire this waiter's onWaitingForPeers now
|
|
513
|
+
// so late originators still get the cue.
|
|
514
|
+
if (barrier.waitingForPeersNotified && waiter.onWaitingForPeers && waiter.isOriginator) {
|
|
515
|
+
try {
|
|
516
|
+
waiter.onWaitingForPeers();
|
|
517
|
+
} catch {
|
|
518
|
+
// Swallow per-waiter errors.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Queue this peer's preload onto the channel's serial barrier
|
|
523
|
+
// tail. Each peer's (possibly CPU-bound) preload runs in its own
|
|
524
|
+
// macrotask via `yieldToMain` so the browser can paint any
|
|
525
|
+
// intermediate loading state before preload monopolizes the main
|
|
526
|
+
// thread, and so siblings on the same barrier serialize cleanly
|
|
527
|
+
// instead of piling onto the announce-fanout task.
|
|
528
|
+
const preload = options.preload;
|
|
529
|
+
if (!preload) {
|
|
530
|
+
waiter.preloaded = {
|
|
531
|
+
has: true,
|
|
532
|
+
value: undefined
|
|
533
|
+
};
|
|
534
|
+
maybeResolveBarrier(channel, barrier);
|
|
535
|
+
} else {
|
|
536
|
+
const runPreload = async () => {
|
|
537
|
+
if (abort.signal.aborted) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
await yieldToMain();
|
|
541
|
+
if (abort.signal.aborted) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
let value;
|
|
545
|
+
try {
|
|
546
|
+
value = await preload(target, abort.signal);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
if (abort.signal.aborted) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
console.error(`[docs-infra/coordinatePreference] Preload for peer '${peer.id}' on channel ` + `'${channel.channelKey}' threw; treating as no-op. Error:`, err);
|
|
552
|
+
}
|
|
553
|
+
if (abort.signal.aborted) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
waiter.preloaded = {
|
|
557
|
+
has: true,
|
|
558
|
+
value
|
|
559
|
+
};
|
|
560
|
+
maybeResolveBarrier(channel, barrier);
|
|
561
|
+
};
|
|
562
|
+
const previousTail = channel.barrierTail;
|
|
563
|
+
const myTurn = previousTail.then(runPreload);
|
|
564
|
+
channel.barrierTail = myTurn.catch(() => undefined);
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
cancel: () => {
|
|
568
|
+
abort.abort();
|
|
569
|
+
const stillPending = channel.pendingBarriers.get(barrierKey);
|
|
570
|
+
if (stillPending && stillPending === barrier) {
|
|
571
|
+
stillPending.waiters.delete(peer.id);
|
|
572
|
+
if (stillPending.waiters.size === 0) {
|
|
573
|
+
clearTimeout(stillPending.minWaitTimer);
|
|
574
|
+
clearTimeout(stillPending.waitingForPeersTimer);
|
|
575
|
+
clearTimeout(stillPending.ultimateTimer);
|
|
576
|
+
channel.pendingBarriers.delete(barrierKey);
|
|
577
|
+
disposeChannelIfEmpty(channel);
|
|
578
|
+
} else {
|
|
579
|
+
maybeResolveBarrier(channel, stillPending);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
settleResolver();
|
|
583
|
+
},
|
|
584
|
+
settled
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function notifyWaitingForPeers(channel, barrierKey) {
|
|
588
|
+
const barrier = channel.pendingBarriers.get(barrierKey);
|
|
589
|
+
if (!barrier || barrier.waitingForPeersNotified) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
barrier.waitingForPeersNotified = true;
|
|
593
|
+
for (const waiter of barrier.waiters.values()) {
|
|
594
|
+
if (!waiter.onWaitingForPeers || !waiter.isOriginator) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
waiter.onWaitingForPeers();
|
|
599
|
+
} catch {
|
|
600
|
+
// Swallow per-waiter errors.
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function maybeResolveBarrier(channel, barrier) {
|
|
605
|
+
if (!barrier.minWaitPassed) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Wait for every *registered* peer on this channel to join the
|
|
609
|
+
// barrier, not just the ones that have already announced. A peer
|
|
610
|
+
// that registered (`registerPeer`) but hasn't yet announced a
|
|
611
|
+
// target is still expected to participate — resolving without it
|
|
612
|
+
// would let the originator commit early and break lockstep.
|
|
613
|
+
// The barrier's `ultimateTimer` is the safety net for a peer that
|
|
614
|
+
// never joins (crashed / unmounted mid-precompute).
|
|
615
|
+
if (barrier.waiters.size + barrier.skipped.size < channel.peers.size) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
for (const waiter of barrier.waiters.values()) {
|
|
619
|
+
if (!waiter.preloaded.has) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const barrierKey = encodeTarget(barrier.target);
|
|
624
|
+
forceResolveBarrier(channel, barrierKey);
|
|
625
|
+
}
|
|
626
|
+
function forceResolveBarrier(channel, barrierKey) {
|
|
627
|
+
const barrier = channel.pendingBarriers.get(barrierKey);
|
|
628
|
+
if (!barrier) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
performanceMeasure(barrier.openMark, {
|
|
632
|
+
mark: 'barrier-resolve',
|
|
633
|
+
measure: 'barrier-resolve'
|
|
634
|
+
}, [PERF_FUNCTION_NAME, channel.channelKey, barrierKey], true);
|
|
635
|
+
clearTimeout(barrier.minWaitTimer);
|
|
636
|
+
clearTimeout(barrier.waitingForPeersTimer);
|
|
637
|
+
clearTimeout(barrier.ultimateTimer);
|
|
638
|
+
channel.pendingBarriers.delete(barrierKey);
|
|
639
|
+
// Fire all onCommits in the same microtask so React batches them.
|
|
640
|
+
for (const waiter of barrier.waiters.values()) {
|
|
641
|
+
const preloaded = waiter.preloaded.has ? waiter.preloaded.value : undefined;
|
|
642
|
+
try {
|
|
643
|
+
waiter.onCommit(barrier.target, preloaded);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error(`[docs-infra/coordinatePreference] onCommit for peer '${waiter.peerId}' on channel ` + `'${channel.channelKey}' threw:`, err);
|
|
646
|
+
}
|
|
647
|
+
waiter.settle();
|
|
648
|
+
}
|
|
649
|
+
// Release any lazy peers that were gated on this barrier. The
|
|
650
|
+
// macrotask hop puts their commits in the *render after* the
|
|
651
|
+
// barrier's batched commit — keeping the main thread clear while
|
|
652
|
+
// the layout-shifting siblings paint, and ensuring the visible
|
|
653
|
+
// flip on the lazy peers never beats the barrier siblings to the
|
|
654
|
+
// DOM.
|
|
655
|
+
const releases = barrier.deferredLazyReleases;
|
|
656
|
+
if (releases.length > 0) {
|
|
657
|
+
setTimeout(() => {
|
|
658
|
+
for (const release of releases) {
|
|
659
|
+
try {
|
|
660
|
+
release();
|
|
661
|
+
} catch (err) {
|
|
662
|
+
console.error(`[docs-infra/coordinatePreference] deferred lazy release on channel ` + `'${channel.channelKey}' threw:`, err);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}, 0);
|
|
666
|
+
}
|
|
667
|
+
disposeChannelIfEmpty(channel);
|
|
668
|
+
}
|
|
669
|
+
function enqueueLazy(channel, peer, target, options) {
|
|
670
|
+
// Barrier-coordination is resolved inside `gateStart` below (one
|
|
671
|
+
// microtask after enqueue) so a barrier opened by a sibling peer
|
|
672
|
+
// in the same sync flush is observable before we route.
|
|
673
|
+
let settleResolver = () => {};
|
|
674
|
+
const settled = new Promise(resolve => {
|
|
675
|
+
settleResolver = resolve;
|
|
676
|
+
});
|
|
677
|
+
const abort = new AbortController();
|
|
678
|
+
peer.lazyInFlight.set(abort, target);
|
|
679
|
+
const lazyWait = options.lazyMinWaitMs ?? options.minWaitMs ?? 0;
|
|
680
|
+
|
|
681
|
+
// Per-peer serialization: callbacks for preload, timer, and commit
|
|
682
|
+
// are kicked off only when this peer's lazy queue reaches us. We
|
|
683
|
+
// chain via callbacks (not Promise.then) so the entire pipeline
|
|
684
|
+
// stays on macrotasks and can be driven by `vi.advanceTimersByTime`
|
|
685
|
+
// without manual microtask drains.
|
|
686
|
+
let preloaded;
|
|
687
|
+
let preloadDone = !options.preload;
|
|
688
|
+
let preloadStarted = false;
|
|
689
|
+
let preloadAwaiters = [];
|
|
690
|
+
const drainNext = () => {
|
|
691
|
+
const next = peer.lazyQueue.shift();
|
|
692
|
+
if (next) {
|
|
693
|
+
next();
|
|
694
|
+
} else {
|
|
695
|
+
peer.lazyActive = false;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
const finishCancelled = () => {
|
|
699
|
+
peer.lazyInFlight.delete(abort);
|
|
700
|
+
settleResolver();
|
|
701
|
+
drainNext();
|
|
702
|
+
};
|
|
703
|
+
const doCommit = () => {
|
|
704
|
+
if (abort.signal.aborted) {
|
|
705
|
+
finishCancelled();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
options.onCommit(target, preloaded);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(`[docs-infra/coordinatePreference] lazy-path onCommit for peer '${peer.id}' on channel ` + `'${channel.channelKey}' threw:`, err);
|
|
712
|
+
}
|
|
713
|
+
performanceMeasure(undefined, {
|
|
714
|
+
mark: 'lazy-commit',
|
|
715
|
+
measure: 'lazy-commit'
|
|
716
|
+
}, [PERF_FUNCTION_NAME, channel.channelKey, peer.id]);
|
|
717
|
+
peer.lazyInFlight.delete(abort);
|
|
718
|
+
settleResolver();
|
|
719
|
+
drainNext();
|
|
720
|
+
};
|
|
721
|
+
const scheduleIdleCommit = () => {
|
|
722
|
+
if (abort.signal.aborted) {
|
|
723
|
+
doCommit();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (options.lazyCommitPriority === 'normal') {
|
|
727
|
+
doCommit();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const ric = globalThis.requestIdleCallback;
|
|
731
|
+
const cic = globalThis.cancelIdleCallback;
|
|
732
|
+
if (typeof ric === 'function') {
|
|
733
|
+
const handle = ric(doCommit);
|
|
734
|
+
abort.signal.addEventListener('abort', () => {
|
|
735
|
+
if (typeof cic === 'function') {
|
|
736
|
+
cic(handle);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
} else {
|
|
740
|
+
doCommit();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const onTimerFired = () => {
|
|
744
|
+
if (abort.signal.aborted) {
|
|
745
|
+
doCommit();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (preloadDone) {
|
|
749
|
+
scheduleIdleCommit();
|
|
750
|
+
} else {
|
|
751
|
+
preloadAwaiters.push(scheduleIdleCommit);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const startPreload = () => {
|
|
755
|
+
if (preloadStarted) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
preloadStarted = true;
|
|
759
|
+
if (!options.preload) {
|
|
760
|
+
preloadDone = true;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const userPreload = options.preload;
|
|
764
|
+
// Always run preload in a fresh macrotask so the browser can
|
|
765
|
+
// paint any intermediate loading state before the (potentially
|
|
766
|
+
// CPU-bound) preload runs. Note: this doesn't change *when* the
|
|
767
|
+
// lazy path is allowed to start — `gateStart` still gates lazy
|
|
768
|
+
// preloads on the barrier's deferred-release fanout, which only
|
|
769
|
+
// fires one macrotask after the batched barrier commit.
|
|
770
|
+
const yielded = yieldToMain().then(() => {
|
|
771
|
+
if (abort.signal.aborted) {
|
|
772
|
+
return undefined;
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
return userPreload(target, abort.signal);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error(`[docs-infra/coordinatePreference] lazy-path preload for peer '${peer.id}' on channel ` + `'${channel.channelKey}' threw; treating as no-op. Error:`, err);
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
yielded.then(value => {
|
|
782
|
+
preloaded = value;
|
|
783
|
+
preloadDone = true;
|
|
784
|
+
const awaiters = preloadAwaiters;
|
|
785
|
+
preloadAwaiters = [];
|
|
786
|
+
for (const fn of awaiters) {
|
|
787
|
+
fn();
|
|
788
|
+
}
|
|
789
|
+
}, err => {
|
|
790
|
+
console.error(`[docs-infra/coordinatePreference] lazy-path preload for peer '${peer.id}' on channel ` + `'${channel.channelKey}' threw; treating as no-op. Error:`, err);
|
|
791
|
+
preloadDone = true;
|
|
792
|
+
const awaiters = preloadAwaiters;
|
|
793
|
+
preloadAwaiters = [];
|
|
794
|
+
for (const fn of awaiters) {
|
|
795
|
+
fn();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
};
|
|
799
|
+
const startTimerAndCommit = () => {
|
|
800
|
+
if (lazyWait > 0) {
|
|
801
|
+
const timer = setTimeout(onTimerFired, lazyWait);
|
|
802
|
+
abort.signal.addEventListener('abort', () => clearTimeout(timer));
|
|
803
|
+
} else {
|
|
804
|
+
onTimerFired();
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
const runPipeline = () => {
|
|
808
|
+
if (abort.signal.aborted) {
|
|
809
|
+
finishCancelled();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
startPreload();
|
|
813
|
+
startTimerAndCommit();
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// Decide barrier-gating one microtask after enqueue. By then any
|
|
817
|
+
// sibling peer that announced a barrier on the same sync flush has
|
|
818
|
+
// opened its barrier, so we can route consistently regardless of
|
|
819
|
+
// hook-declaration order.
|
|
820
|
+
const gateStart = () => {
|
|
821
|
+
if (abort.signal.aborted) {
|
|
822
|
+
finishCancelled();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const barrierKey = encodeTarget(target);
|
|
826
|
+
const existingBarrier = channel.pendingBarriers.get(barrierKey);
|
|
827
|
+
if (!existingBarrier) {
|
|
828
|
+
runPipeline();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// A same-target barrier is pending. Push our deferred release
|
|
832
|
+
// FIRST so that if `maybeResolveBarrier` (called below)
|
|
833
|
+
// synchronously force-resolves the barrier, our release is
|
|
834
|
+
// included in its `setTimeout` schedule. Then mark this peer as
|
|
835
|
+
// skipped and (optionally) overlap our preload with the
|
|
836
|
+
// barrier's via `preloadAll`. The barrier fires the deferred
|
|
837
|
+
// releases one macrotask after its batched commit so the
|
|
838
|
+
// visible flip on the lazy peers lands in the render *after*
|
|
839
|
+
// the layout-shifting barrier siblings have updated the DOM.
|
|
840
|
+
existingBarrier.deferredLazyReleases.push(() => {
|
|
841
|
+
if (abort.signal.aborted) {
|
|
842
|
+
finishCancelled();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (preloadStarted) {
|
|
846
|
+
startTimerAndCommit();
|
|
847
|
+
} else {
|
|
848
|
+
runPipeline();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
if (!existingBarrier.waiters.has(peer.id)) {
|
|
852
|
+
existingBarrier.skipped.add(peer.id);
|
|
853
|
+
maybeResolveBarrier(channel, existingBarrier);
|
|
854
|
+
}
|
|
855
|
+
if (options.preloadAll) {
|
|
856
|
+
startPreload();
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
const enqueueEntry = () => {
|
|
860
|
+
// If a same-target barrier is already open at enqueue time (the
|
|
861
|
+
// common case: this lazy announce was triggered by an
|
|
862
|
+
// originator's `notifySiblings` immediately after they opened
|
|
863
|
+
// the barrier), join it synchronously. Waiting one microtask
|
|
864
|
+
// would let the originator's `minWaitTimer` fire first under
|
|
865
|
+
// faked timers (`queueMicrotask` is faked by vitest), at which
|
|
866
|
+
// point `maybeResolveBarrier` would see `skipped.size === 0`
|
|
867
|
+
// and force-resolve only the originator's waiter — leaving us
|
|
868
|
+
// stranded. The microtask path below still covers the
|
|
869
|
+
// hook-declaration-order case where the barrier hasn't opened
|
|
870
|
+
// yet by the time we enqueue.
|
|
871
|
+
const barrierKey = encodeTarget(target);
|
|
872
|
+
if (channel.pendingBarriers.has(barrierKey)) {
|
|
873
|
+
gateStart();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
queueMicrotask(gateStart);
|
|
877
|
+
};
|
|
878
|
+
if (peer.lazyActive) {
|
|
879
|
+
peer.lazyQueue.push(enqueueEntry);
|
|
880
|
+
} else {
|
|
881
|
+
peer.lazyActive = true;
|
|
882
|
+
enqueueEntry();
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
cancel: () => {
|
|
886
|
+
abort.abort();
|
|
887
|
+
peer.lazyInFlight.delete(abort);
|
|
888
|
+
settleResolver();
|
|
889
|
+
// If this was queued but never started, remove it from the
|
|
890
|
+
// queue so the next caller's `start` actually runs. If it had
|
|
891
|
+
// already started, the in-flight setTimeout/preload paths will
|
|
892
|
+
// call `drainNext` themselves when they observe `aborted`.
|
|
893
|
+
const idx = peer.lazyQueue.indexOf(enqueueEntry);
|
|
894
|
+
if (idx !== -1) {
|
|
895
|
+
peer.lazyQueue.splice(idx, 1);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
settled
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Returns `true` if any peer has ever called `announceTarget` on this
|
|
904
|
+
* channel since the channel was created (i.e., since the first peer
|
|
905
|
+
* registered without an existing channel object). Useful for
|
|
906
|
+
* first-render reconciliation: a peer that wakes up post-hydration
|
|
907
|
+
* and finds the channel "fresh" (no announcements yet) can safely
|
|
908
|
+
* fast-forward its committed value to the latest underlying value
|
|
909
|
+
* without going through a barrier, because no peer is mid-animation.
|
|
910
|
+
*
|
|
911
|
+
* Returns `false` when the channel doesn't exist (no peers have
|
|
912
|
+
* registered yet) or exists but hasn't seen an announce.
|
|
913
|
+
*/
|
|
914
|
+
export function hasEverAnnounced(channelKey) {
|
|
915
|
+
const channel = channels.get(channelKey);
|
|
916
|
+
return channel ? channel.hasEverAnnounced : false;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Returns the `announceTime` recorded when the active barrier for
|
|
921
|
+
* `target` was opened, or `null` if no barrier is currently pending
|
|
922
|
+
* for that target on `channelKey`. Late-joining peers can use this
|
|
923
|
+
* to anchor their local timers to the originator's wall-clock window
|
|
924
|
+
* instead of restarting a fresh one \u2014 e.g. a peer whose state
|
|
925
|
+
* propagated 200ms after the originator's click should commit 200ms
|
|
926
|
+
* earlier than its local `Date.now()` would suggest, so the visible
|
|
927
|
+
* paint lines up.
|
|
928
|
+
*/
|
|
929
|
+
export function getBarrierAnnounceTime(channelKey, target) {
|
|
930
|
+
const channel = channels.get(channelKey);
|
|
931
|
+
if (!channel) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const barrierKey = encodeTarget(target);
|
|
935
|
+
const barrier = channel.pendingBarriers.get(barrierKey);
|
|
936
|
+
return barrier ? barrier.announceTime : null;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Internal handles for the `coordinatePreference.testUtils` sibling.
|
|
941
|
+
*
|
|
942
|
+
* Not part of the public API. Do not import this from production
|
|
943
|
+
* code or from tests directly — use the helpers re-exported from
|
|
944
|
+
* `./coordinatePreference.testUtils` instead so that the boundary
|
|
945
|
+
* between runtime API and test affordances stays clear.
|
|
946
|
+
*/
|
|
947
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -- intentional sentinel name marking this as a test-only sibling import
|
|
948
|
+
export const __testInternals = {
|
|
949
|
+
channels,
|
|
950
|
+
setTargetEncoder
|
|
951
|
+
};
|