@mui/internal-docs-infra 0.11.1-canary.8 → 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 +85 -5
- 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,193 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type ChannelKey, type PeerId } from "./coordinatePreference.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link useCoordinated}. See `coordinatePreference` for
|
|
5
|
+
* the underlying semantics; only React-specific behaviors are
|
|
6
|
+
* documented here.
|
|
7
|
+
*/
|
|
8
|
+
export interface UseCoordinatedOptions<TValue, TPreload> {
|
|
9
|
+
/**
|
|
10
|
+
* Coordination scope. All peers (component instances) that share a
|
|
11
|
+
* `channelKey` participate in the same layout-shift barrier. Pass
|
|
12
|
+
* `null` to opt out of coordination entirely (the hook becomes a
|
|
13
|
+
* plain pass-through of the underlying `[value, setValue]`).
|
|
14
|
+
*/
|
|
15
|
+
channelKey: ChannelKey | null;
|
|
16
|
+
/**
|
|
17
|
+
* Stable identifier for *this* peer within the channel. Defaults to
|
|
18
|
+
* a freshly generated id on mount. Override when stable cross-mount
|
|
19
|
+
* identity matters (e.g. for analytics / debugging).
|
|
20
|
+
*/
|
|
21
|
+
peerId?: PeerId;
|
|
22
|
+
/**
|
|
23
|
+
* Return `true` when applying this target would visibly shift
|
|
24
|
+
* layout — the peer joins the channel-wide barrier so all such
|
|
25
|
+
* peers commit together. Return `false` for non-disruptive changes
|
|
26
|
+
* — the peer commits lazily on its own self-serial chain. See
|
|
27
|
+
* `coordinatePreference` for the full semantics.
|
|
28
|
+
*/
|
|
29
|
+
causesLayoutShift: (target: TValue) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Optional async work to run before the barrier commits (for
|
|
32
|
+
* `causesLayoutShift === true`) or before a lazy commit (for
|
|
33
|
+
* `false`). The resolved value is handed to `onCommit`.
|
|
34
|
+
*/
|
|
35
|
+
preload?: (target: TValue, signal: AbortSignal) => TPreload | Promise<TPreload>;
|
|
36
|
+
/**
|
|
37
|
+
* Hook fired inside the coordinated commit, before the visible
|
|
38
|
+
* value flips. Useful for installing precomputed payloads into
|
|
39
|
+
* neighboring state. The visible `value` returned from this hook
|
|
40
|
+
* always lags `pendingValue` until coordination settles, so this
|
|
41
|
+
* runs *with* the value flip, not before it.
|
|
42
|
+
*
|
|
43
|
+
* Also fires once on first mount, with the initial preloaded
|
|
44
|
+
* payload, so consumers can install precomputed state on hydration
|
|
45
|
+
* without a separate code path.
|
|
46
|
+
*
|
|
47
|
+
* Under normal conditions `preloaded` is whatever this peer's
|
|
48
|
+
* `preload` resolved to. It may still be `undefined` when:
|
|
49
|
+
* - the barrier force-resolved at `ultimateTimeoutMs` (a sibling
|
|
50
|
+
* peer crashed / hung; accompanied by a console warning),
|
|
51
|
+
* - `preload` threw (logged via `console.error`, treated as a
|
|
52
|
+
* no-op so the rest of the channel still commits), or
|
|
53
|
+
* - `preload` explicitly returned `undefined`.
|
|
54
|
+
*
|
|
55
|
+
* Handlers should tolerate the undefined case and fall back to a
|
|
56
|
+
* synchronous render path rather than throwing.
|
|
57
|
+
*/
|
|
58
|
+
onCommit?: (target: TValue, preloaded: TPreload | undefined) => void;
|
|
59
|
+
/**
|
|
60
|
+
* See {@link AnnounceOptions.minWaitMs}.
|
|
61
|
+
*/
|
|
62
|
+
minWaitMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* See {@link AnnounceOptions.multiPeerExtraMinWaitMs}.
|
|
65
|
+
*/
|
|
66
|
+
multiPeerExtraMinWaitMs?: number;
|
|
67
|
+
/**
|
|
68
|
+
* See {@link AnnounceOptions.lazyMinWaitMs}.
|
|
69
|
+
*/
|
|
70
|
+
lazyMinWaitMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* See {@link AnnounceOptions.gracePeriodMs}.
|
|
73
|
+
*/
|
|
74
|
+
gracePeriodMs?: number;
|
|
75
|
+
/**
|
|
76
|
+
* See {@link AnnounceOptions.ultimateTimeoutMs}.
|
|
77
|
+
*/
|
|
78
|
+
ultimateTimeoutMs?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Controls whether `isCoordinating` flips *during* the preload
|
|
81
|
+
* or *after* it. `pendingValue` (the user-facing "intent"
|
|
82
|
+
* signal) always flips synchronously regardless of this flag —
|
|
83
|
+
* toolbars and other affordances stay responsive on click.
|
|
84
|
+
*
|
|
85
|
+
* - `false` (default) — defer `isCoordinating` until the
|
|
86
|
+
* originator's `preload` settles. Use this when the preload
|
|
87
|
+
* is CPU-bound (parsing, syntax highlighting, layout
|
|
88
|
+
* measurement, etc.) and the consumer drives a visible
|
|
89
|
+
* animation off `isCoordinating`. Running the animation
|
|
90
|
+
* concurrently with the preload would steal main-thread time
|
|
91
|
+
* from the compositor and produce a janky transition; with
|
|
92
|
+
* the flip deferred the animation only starts once the heavy
|
|
93
|
+
* work is done.
|
|
94
|
+
* - `true` — flip `isCoordinating` synchronously on the
|
|
95
|
+
* originating setter call, so the animation runs in parallel
|
|
96
|
+
* with the preload. Use this when the preload is I/O-bound
|
|
97
|
+
* (network fetches, `localStorage` reads, etc.) so the
|
|
98
|
+
* animation and the I/O roundtrip overlap.
|
|
99
|
+
*
|
|
100
|
+
* The coordinator always yields to the browser (via
|
|
101
|
+
* `scheduler.yield()` when available, otherwise a `setTimeout`
|
|
102
|
+
* macrotask) before invoking `preload`, so even synchronous
|
|
103
|
+
* preloads settle one macrotask after the originating setter
|
|
104
|
+
* call. This lets the intermediate loading state paint before
|
|
105
|
+
* the (potentially CPU-bound) preload monopolizes the main
|
|
106
|
+
* thread. This flag has no effect when `preload` is omitted;
|
|
107
|
+
* the flip is synchronous either way.
|
|
108
|
+
*
|
|
109
|
+
* Only the originator's flip is affected. Sibling peers picked
|
|
110
|
+
* up via `notifySiblings` still observe the receiver flow's
|
|
111
|
+
* synchronous flip, because their `isCoordinating` is driven
|
|
112
|
+
* by the originator's broadcast rather than a local click.
|
|
113
|
+
*/
|
|
114
|
+
animateDuringPreload?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Scheduling priority for lazy-path commits.
|
|
117
|
+
*
|
|
118
|
+
* - `'idle'` (default) — lazy-path commits are deferred via
|
|
119
|
+
* `requestIdleCallback` so the browser can yield to in-flight
|
|
120
|
+
* paints and input. Use when the lazy peer's commit itself is
|
|
121
|
+
* main-thread heavy (DOM reconciliation of a freshly
|
|
122
|
+
* transformed tree, etc.).
|
|
123
|
+
* - `'normal'` — the commit lands as soon as the preload
|
|
124
|
+
* resolves. Use for I/O-bound `preload`s where the commit is
|
|
125
|
+
* cheap and you want each peer's swap to surface immediately;
|
|
126
|
+
* otherwise idle scheduling can cluster commits together near
|
|
127
|
+
* the slowest peer's settle.
|
|
128
|
+
*
|
|
129
|
+
* Has no effect when this peer takes the barrier path — barrier
|
|
130
|
+
* commits are batched synchronously inside the barrier's resolve
|
|
131
|
+
* microtask regardless.
|
|
132
|
+
*/
|
|
133
|
+
lazyCommitPriority?: 'idle' | 'normal';
|
|
134
|
+
}
|
|
135
|
+
export interface UseCoordinatedExtras<TValue> {
|
|
136
|
+
/**
|
|
137
|
+
* The most recently announced target value. Equals the committed
|
|
138
|
+
* `value` when no coordination is in flight. Useful for showing an
|
|
139
|
+
* optimistic preview UI (toolbar selection, etc.) that should react
|
|
140
|
+
* instantly to a click even when the visible content has a pending
|
|
141
|
+
* barrier.
|
|
142
|
+
*/
|
|
143
|
+
pendingValue: TValue;
|
|
144
|
+
/**
|
|
145
|
+
* `true` while a coordination is in flight that this peer can drive
|
|
146
|
+
* an animation off of. For receivers and barrier joiners that lands
|
|
147
|
+
* synchronously with the announce. For originators the flip is
|
|
148
|
+
* controlled by `animateDuringPreload`: with the default
|
|
149
|
+
* `animateDuringPreload: false`, `isCoordinating` stays `false`
|
|
150
|
+
* until the originator's `preload` settles, then flips `true` for
|
|
151
|
+
* the remainder of the barrier; with `animateDuringPreload: true`,
|
|
152
|
+
* it flips synchronously on the originating setter call so the
|
|
153
|
+
* animation overlaps with an I/O-bound preload. Use
|
|
154
|
+
* {@link pendingValue} to drive intent-based affordances (toolbar
|
|
155
|
+
* selection, etc.) that should react instantly to a click
|
|
156
|
+
* regardless of this flag. Surfaces as `data-coordinating` on
|
|
157
|
+
* consumers.
|
|
158
|
+
*/
|
|
159
|
+
isCoordinating: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* `true` once the grace period has elapsed with the barrier still
|
|
162
|
+
* unresolved. Only set on the originating peer. Surface as a
|
|
163
|
+
* "waiting" affordance to the user.
|
|
164
|
+
*/
|
|
165
|
+
isWaitingForPeers: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Coordinate a piece of state across sibling component instances on
|
|
169
|
+
* the same channel, so that visually disruptive value changes commit
|
|
170
|
+
* in a single layout pass rather than independently. Designed as a
|
|
171
|
+
* thin wrapper around any `useState`-shaped primitive (e.g.
|
|
172
|
+
* `useLocalStorageState`, `usePreference`, plain `useState`).
|
|
173
|
+
*
|
|
174
|
+
* **Originator flow** — calling the returned `setValue`:
|
|
175
|
+
* 1. `pendingValue` updates synchronously to the requested target
|
|
176
|
+
* 2. The coordinator runs `preload` (per phase rules) and waits for
|
|
177
|
+
* sibling peers (phase 1 only)
|
|
178
|
+
* 3. When the barrier resolves, the underlying `setValue` is called
|
|
179
|
+
* and this hook's visible `value` flips, so the swap is
|
|
180
|
+
* consistent with the optional `onCommit` side-effect
|
|
181
|
+
*
|
|
182
|
+
* **Receiver flow** — when the underlying `value` changes from outside
|
|
183
|
+
* (e.g. a storage event from another tab):
|
|
184
|
+
* 1. `pendingValue` updates to match
|
|
185
|
+
* 2. Coordination runs locally (this tab's peers run their own
|
|
186
|
+
* phase-1 barrier)
|
|
187
|
+
* 3. The visible `value` returned by this hook is held at the
|
|
188
|
+
* previous value until the barrier resolves, then flips
|
|
189
|
+
*
|
|
190
|
+
* Pass `channelKey: null` to disable coordination — the hook becomes
|
|
191
|
+
* a transparent pass-through of the underlying tuple.
|
|
192
|
+
*/
|
|
193
|
+
export declare function useCoordinated<TValue, TPreload = void>(underlying: [TValue, (next: TValue) => void], options: UseCoordinatedOptions<TValue, TPreload>): [TValue, React.Dispatch<React.SetStateAction<TValue>>, UseCoordinatedExtras<TValue>];
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { registerPeer, reportValue, announceTarget, getBarrierAnnounceTime } from "./coordinatePreference.mjs";
|
|
5
|
+
import { whenLayoutShiftsSettled, layoutShiftsSettled } from "./layoutShiftGate.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for {@link useCoordinated}. See `coordinatePreference` for
|
|
9
|
+
* the underlying semantics; only React-specific behaviors are
|
|
10
|
+
* documented here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
let nextAutoPeerId = 0;
|
|
14
|
+
function generatePeerId() {
|
|
15
|
+
nextAutoPeerId += 1;
|
|
16
|
+
return `peer-${nextAutoPeerId}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Coordinate a piece of state across sibling component instances on
|
|
21
|
+
* the same channel, so that visually disruptive value changes commit
|
|
22
|
+
* in a single layout pass rather than independently. Designed as a
|
|
23
|
+
* thin wrapper around any `useState`-shaped primitive (e.g.
|
|
24
|
+
* `useLocalStorageState`, `usePreference`, plain `useState`).
|
|
25
|
+
*
|
|
26
|
+
* **Originator flow** — calling the returned `setValue`:
|
|
27
|
+
* 1. `pendingValue` updates synchronously to the requested target
|
|
28
|
+
* 2. The coordinator runs `preload` (per phase rules) and waits for
|
|
29
|
+
* sibling peers (phase 1 only)
|
|
30
|
+
* 3. When the barrier resolves, the underlying `setValue` is called
|
|
31
|
+
* and this hook's visible `value` flips, so the swap is
|
|
32
|
+
* consistent with the optional `onCommit` side-effect
|
|
33
|
+
*
|
|
34
|
+
* **Receiver flow** — when the underlying `value` changes from outside
|
|
35
|
+
* (e.g. a storage event from another tab):
|
|
36
|
+
* 1. `pendingValue` updates to match
|
|
37
|
+
* 2. Coordination runs locally (this tab's peers run their own
|
|
38
|
+
* phase-1 barrier)
|
|
39
|
+
* 3. The visible `value` returned by this hook is held at the
|
|
40
|
+
* previous value until the barrier resolves, then flips
|
|
41
|
+
*
|
|
42
|
+
* Pass `channelKey: null` to disable coordination — the hook becomes
|
|
43
|
+
* a transparent pass-through of the underlying tuple.
|
|
44
|
+
*/
|
|
45
|
+
export function useCoordinated(underlying, options) {
|
|
46
|
+
const [underlyingValue, setUnderlyingValue] = underlying;
|
|
47
|
+
const {
|
|
48
|
+
channelKey,
|
|
49
|
+
peerId: explicitPeerId,
|
|
50
|
+
causesLayoutShift,
|
|
51
|
+
preload,
|
|
52
|
+
onCommit,
|
|
53
|
+
minWaitMs,
|
|
54
|
+
multiPeerExtraMinWaitMs,
|
|
55
|
+
lazyMinWaitMs,
|
|
56
|
+
gracePeriodMs,
|
|
57
|
+
ultimateTimeoutMs,
|
|
58
|
+
animateDuringPreload = false,
|
|
59
|
+
lazyCommitPriority = 'idle'
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
// Stable peer id for the lifetime of the mounted component. Held in a ref so an
|
|
63
|
+
// auto-generated fallback stays stable across renders, an explicit `peerId` prop
|
|
64
|
+
// wins and resyncs on change, and an explicit id later removed keeps the last
|
|
65
|
+
// explicit value. The registration effect keys off [channelKey, peerId].
|
|
66
|
+
/* eslint-disable react-hooks/refs -- deliberate stable-id ref: lazy init + explicit-prop resync during render; the ref is identity memory, never read for rendering output */
|
|
67
|
+
const peerIdRef = React.useRef(null);
|
|
68
|
+
if (peerIdRef.current === null) {
|
|
69
|
+
peerIdRef.current = explicitPeerId ?? generatePeerId();
|
|
70
|
+
} else if (explicitPeerId !== undefined && explicitPeerId !== peerIdRef.current) {
|
|
71
|
+
peerIdRef.current = explicitPeerId;
|
|
72
|
+
}
|
|
73
|
+
const peerId = peerIdRef.current;
|
|
74
|
+
/* eslint-enable react-hooks/refs */
|
|
75
|
+
|
|
76
|
+
// The visible (committed) value. Lags `underlyingValue` while a
|
|
77
|
+
// phase-1 barrier is open in the receiver flow.
|
|
78
|
+
const [committedValue, setCommittedValue] = React.useState(underlyingValue);
|
|
79
|
+
// The latest target value (originator click or external change).
|
|
80
|
+
const [pendingValue, setPendingValue] = React.useState(underlyingValue);
|
|
81
|
+
const [isCoordinating, setIsCoordinating] = React.useState(false);
|
|
82
|
+
const [isWaitingForPeers, setIsWaitingForPeers] = React.useState(false);
|
|
83
|
+
|
|
84
|
+
// Keep latest callbacks in a ref so we don't re-register the peer
|
|
85
|
+
// when the consumer passes inline functions.
|
|
86
|
+
const callbacksRef = React.useRef({
|
|
87
|
+
causesLayoutShift,
|
|
88
|
+
preload,
|
|
89
|
+
onCommit,
|
|
90
|
+
setUnderlyingValue
|
|
91
|
+
});
|
|
92
|
+
/* eslint-disable react-hooks/refs -- latest-ref pattern: cache current callbacks for the once-registered peer and long-lived runCoordination closure; intentionally excluded from effect/useCallback deps to avoid re-registering on inline-function identity churn */
|
|
93
|
+
callbacksRef.current.causesLayoutShift = causesLayoutShift;
|
|
94
|
+
callbacksRef.current.preload = preload;
|
|
95
|
+
callbacksRef.current.onCommit = onCommit;
|
|
96
|
+
callbacksRef.current.setUnderlyingValue = setUnderlyingValue;
|
|
97
|
+
/* eslint-enable react-hooks/refs */
|
|
98
|
+
|
|
99
|
+
const timingRef = React.useRef({
|
|
100
|
+
minWaitMs,
|
|
101
|
+
multiPeerExtraMinWaitMs,
|
|
102
|
+
lazyMinWaitMs,
|
|
103
|
+
gracePeriodMs,
|
|
104
|
+
ultimateTimeoutMs,
|
|
105
|
+
animateDuringPreload,
|
|
106
|
+
lazyCommitPriority
|
|
107
|
+
});
|
|
108
|
+
/* eslint-disable react-hooks/refs -- latest-ref pattern: cache current timing options for the once-registered peer and long-lived runCoordination closure; intentionally excluded from effect/useCallback deps to avoid re-registering on option identity churn */
|
|
109
|
+
timingRef.current.minWaitMs = minWaitMs;
|
|
110
|
+
timingRef.current.multiPeerExtraMinWaitMs = multiPeerExtraMinWaitMs;
|
|
111
|
+
timingRef.current.lazyMinWaitMs = lazyMinWaitMs;
|
|
112
|
+
timingRef.current.gracePeriodMs = gracePeriodMs;
|
|
113
|
+
timingRef.current.ultimateTimeoutMs = ultimateTimeoutMs;
|
|
114
|
+
timingRef.current.animateDuringPreload = animateDuringPreload;
|
|
115
|
+
timingRef.current.lazyCommitPriority = lazyCommitPriority;
|
|
116
|
+
/* eslint-enable react-hooks/refs */
|
|
117
|
+
|
|
118
|
+
// In-flight handle so we can cancel/supersede.
|
|
119
|
+
const handleRef = React.useRef(null);
|
|
120
|
+
// Track the value we last asked the underlying primitive to take —
|
|
121
|
+
// when we see it echoed back via `underlyingValue` we skip the
|
|
122
|
+
// receiver flow so we don't double-coordinate our own writes.
|
|
123
|
+
const lastWrittenRef = React.useRef({
|
|
124
|
+
has: false
|
|
125
|
+
});
|
|
126
|
+
// The latest target the coordinator is working on; used to dedupe
|
|
127
|
+
// re-entrant effect runs when `underlyingValue` changes mid-flight.
|
|
128
|
+
const inFlightTargetRef = React.useRef({
|
|
129
|
+
has: false
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Forward-declared ref so the peer registration's `onSiblingAnnounce`
|
|
133
|
+
// can call the latest `runCoordination` without re-registering the
|
|
134
|
+
// peer every time the callback identity changes.
|
|
135
|
+
const runCoordinationRef = React.useRef(null);
|
|
136
|
+
// Register / unregister this peer with the channel.
|
|
137
|
+
React.useInsertionEffect(() => {
|
|
138
|
+
if (channelKey === null) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const unregister = registerPeer(channelKey, peerId, target => {
|
|
142
|
+
runCoordinationRef.current?.(target, false);
|
|
143
|
+
});
|
|
144
|
+
reportValue(channelKey, peerId, committedValue);
|
|
145
|
+
return () => {
|
|
146
|
+
const handle = handleRef.current;
|
|
147
|
+
handleRef.current = null;
|
|
148
|
+
if (handle) {
|
|
149
|
+
handle.cancel();
|
|
150
|
+
}
|
|
151
|
+
inFlightTargetRef.current = {
|
|
152
|
+
has: false
|
|
153
|
+
};
|
|
154
|
+
unregister();
|
|
155
|
+
};
|
|
156
|
+
}, [channelKey, peerId]);
|
|
157
|
+
|
|
158
|
+
// Tell the coordinator our latest committed value so it can skip
|
|
159
|
+
// already-at-target peers when classifying barrier expectations.
|
|
160
|
+
React.useInsertionEffect(() => {
|
|
161
|
+
if (channelKey === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
reportValue(channelKey, peerId, committedValue);
|
|
165
|
+
}, [channelKey, peerId, committedValue]);
|
|
166
|
+
const runCoordination = React.useCallback((target, isOriginator) => {
|
|
167
|
+
if (channelKey === null) {
|
|
168
|
+
// Keep `inFlightTargetRef` in sync so two synchronous calls
|
|
169
|
+
// like `setValue(p => p + 1); setValue(p => p + 1)` see the
|
|
170
|
+
// freshest base — `setPendingValue` is async, so the second
|
|
171
|
+
// updater would otherwise read the pre-render value and
|
|
172
|
+
// collapse to a single increment. The receiver-flow effect
|
|
173
|
+
// resets the ref on every external `underlyingValue` change,
|
|
174
|
+
// and `runCoordination` overwrites it on every subsequent
|
|
175
|
+
// call, so this won't leak across coordination cycles.
|
|
176
|
+
inFlightTargetRef.current = {
|
|
177
|
+
has: true,
|
|
178
|
+
value: target
|
|
179
|
+
};
|
|
180
|
+
setPendingValue(target);
|
|
181
|
+
setCommittedValue(target);
|
|
182
|
+
if (isOriginator) {
|
|
183
|
+
lastWrittenRef.current = {
|
|
184
|
+
has: true,
|
|
185
|
+
value: target
|
|
186
|
+
};
|
|
187
|
+
callbacksRef.current.setUnderlyingValue(target);
|
|
188
|
+
}
|
|
189
|
+
callbacksRef.current.onCommit?.(target, undefined);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Dedupe: if we're already coordinating exactly this target
|
|
193
|
+
// (e.g. a sibling-announce notification arrived after we
|
|
194
|
+
// already kicked off our own receiver-flow announce, our own
|
|
195
|
+
// originator-flow `announceTarget` call is still on the stack,
|
|
196
|
+
// or the user clicked the same value twice in quick
|
|
197
|
+
// succession), skip restarting the announcement. Without this
|
|
198
|
+
// the second call would cancel our in-flight handle and we'd
|
|
199
|
+
// lose our place in the barrier — or, when the dedupe also
|
|
200
|
+
// fires re-entrantly mid-announce, we'd recurse forever.
|
|
201
|
+
if (inFlightTargetRef.current.has && Object.is(inFlightTargetRef.current.value, target)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Supersede any in-flight announcement.
|
|
205
|
+
const previousHandle = handleRef.current;
|
|
206
|
+
if (previousHandle) {
|
|
207
|
+
previousHandle.cancel();
|
|
208
|
+
}
|
|
209
|
+
inFlightTargetRef.current = {
|
|
210
|
+
has: true,
|
|
211
|
+
value: target
|
|
212
|
+
};
|
|
213
|
+
// `pendingValue` is the user-facing "intent" signal and
|
|
214
|
+
// always flips synchronously so toolbars / pickers stay
|
|
215
|
+
// responsive on click. `isCoordinating` is what consumers
|
|
216
|
+
// typically gate an animation on — when
|
|
217
|
+
// `animateDuringPreload === false` we hold it until the
|
|
218
|
+
// originator's preload settles so a CPU-bound preload
|
|
219
|
+
// doesn't steal main-thread time from the animation that
|
|
220
|
+
// follows. The coordinator yields to the browser before
|
|
221
|
+
// invoking `preload`, so even sync preloads settle one
|
|
222
|
+
// macrotask later; receivers flip in the same tick
|
|
223
|
+
// regardless of the flag.
|
|
224
|
+
setPendingValue(target);
|
|
225
|
+
// Always clear stale waiting state from any previous
|
|
226
|
+
// coordination cycle synchronously — otherwise a later
|
|
227
|
+
// `onWaitingForPeers` (which fires from a timer relative to
|
|
228
|
+
// the announce) could be overwritten when we flip
|
|
229
|
+
// `isCoordinating`.
|
|
230
|
+
setIsWaitingForPeers(false);
|
|
231
|
+
const deferFlipForPreload = isOriginator && !timingRef.current.animateDuringPreload && callbacksRef.current.preload !== undefined;
|
|
232
|
+
let coordinatingFlipped = false;
|
|
233
|
+
const flipCoordinating = () => {
|
|
234
|
+
if (coordinatingFlipped) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
coordinatingFlipped = true;
|
|
238
|
+
setIsCoordinating(true);
|
|
239
|
+
};
|
|
240
|
+
if (!deferFlipForPreload) {
|
|
241
|
+
flipCoordinating();
|
|
242
|
+
}
|
|
243
|
+
const userPreload = callbacksRef.current.preload;
|
|
244
|
+
const flipWrappedPreload = deferFlipForPreload ? (preloadTarget, signal) => {
|
|
245
|
+
// Wrap the user's preload so we flip `isCoordinating`
|
|
246
|
+
// the instant it settles — whether sync or async —
|
|
247
|
+
// but not before. Errors still flip; the engine logs
|
|
248
|
+
// them and treats the result as `undefined`, and the
|
|
249
|
+
// consumer expects the signal to converge regardless
|
|
250
|
+
// of the preload outcome.
|
|
251
|
+
let result;
|
|
252
|
+
let threw;
|
|
253
|
+
let didThrow = false;
|
|
254
|
+
try {
|
|
255
|
+
result = userPreload(preloadTarget, signal);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
didThrow = true;
|
|
258
|
+
threw = err;
|
|
259
|
+
}
|
|
260
|
+
const isThenable = !didThrow && result !== null && result !== undefined && typeof result.then === 'function';
|
|
261
|
+
if (!isThenable) {
|
|
262
|
+
if (!signal.aborted) {
|
|
263
|
+
flipCoordinating();
|
|
264
|
+
}
|
|
265
|
+
if (didThrow) {
|
|
266
|
+
throw threw;
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
return result.then(value => {
|
|
271
|
+
if (!signal.aborted) {
|
|
272
|
+
flipCoordinating();
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}, err => {
|
|
276
|
+
if (!signal.aborted) {
|
|
277
|
+
flipCoordinating();
|
|
278
|
+
}
|
|
279
|
+
throw err;
|
|
280
|
+
});
|
|
281
|
+
} : userPreload;
|
|
282
|
+
|
|
283
|
+
// Automatically hold the *commit* until the page's initial layout-shift
|
|
284
|
+
// sources have settled (see `layoutShiftGate`), for layout-shifting
|
|
285
|
+
// targets only. The consumer's preload still starts immediately and flips
|
|
286
|
+
// `isCoordinating` on its own settle — the gate wait runs in parallel and
|
|
287
|
+
// only delays the commit, so the first page-wide transform/variant change
|
|
288
|
+
// lands as one unified update. A no-op when nothing has registered with
|
|
289
|
+
// the gate (`whenLayoutShiftsSettled` returns `null`), so plain
|
|
290
|
+
// `useCoordinated` consumers are unaffected.
|
|
291
|
+
let wrappedPreload;
|
|
292
|
+
if (userPreload) {
|
|
293
|
+
wrappedPreload = (preloadTarget, signal) => {
|
|
294
|
+
const inner = flipWrappedPreload(preloadTarget, signal);
|
|
295
|
+
const gateWait = callbacksRef.current.causesLayoutShift(preloadTarget) ? whenLayoutShiftsSettled(signal) : null;
|
|
296
|
+
if (gateWait === null) {
|
|
297
|
+
return inner;
|
|
298
|
+
}
|
|
299
|
+
return Promise.all([gateWait, Promise.resolve(inner)]).then(([, result]) => result);
|
|
300
|
+
};
|
|
301
|
+
} else if (!layoutShiftsSettled() && callbacksRef.current.causesLayoutShift(target)) {
|
|
302
|
+
// No user preload, but the target shifts layout and the gate is still
|
|
303
|
+
// closed: synthesize a preload that only awaits the gate so the commit
|
|
304
|
+
// still holds until the page settles (the gate is otherwise consulted
|
|
305
|
+
// only inside the user-preload wrapper, so without this branch a
|
|
306
|
+
// layout-shifting peer with no preload would skip coordination entirely).
|
|
307
|
+
// Once the gate has opened
|
|
308
|
+
// this branch is skipped, so steady-state layout-shifting commits keep
|
|
309
|
+
// the synchronous fast path. The gate wait rejects `AbortError` on
|
|
310
|
+
// supersede, which propagates into the engine's `await preload(...)`
|
|
311
|
+
// and is swallowed there because the signal is aborted — exactly like
|
|
312
|
+
// the user-preload path above.
|
|
313
|
+
wrappedPreload = (preloadTarget, signal) => {
|
|
314
|
+
const gateWait = callbacksRef.current.causesLayoutShift(preloadTarget) ? whenLayoutShiftsSettled(signal) : null;
|
|
315
|
+
if (gateWait === null) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
return gateWait.then(() => undefined);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const handle = announceTarget(channelKey, peerId, target, {
|
|
322
|
+
causesLayoutShift: callbacksRef.current.causesLayoutShift,
|
|
323
|
+
preload: wrappedPreload,
|
|
324
|
+
onCommit: (committedTarget, preloaded) => {
|
|
325
|
+
// Side-effect first so consumers can install precomputed
|
|
326
|
+
// payloads before the value flip becomes visible.
|
|
327
|
+
callbacksRef.current.onCommit?.(committedTarget, preloaded);
|
|
328
|
+
// If the engine force-resolved the barrier while our
|
|
329
|
+
// deferred preload was still in flight (e.g. at
|
|
330
|
+
// `ultimateTimeoutMs`), `flipCoordinating` may not have
|
|
331
|
+
// run — flush it now so `isCoordinating` reaches the
|
|
332
|
+
// true → false transition consumers expect at least
|
|
333
|
+
// once per cycle.
|
|
334
|
+
flipCoordinating();
|
|
335
|
+
if (isOriginator) {
|
|
336
|
+
lastWrittenRef.current = {
|
|
337
|
+
has: true,
|
|
338
|
+
value: committedTarget
|
|
339
|
+
};
|
|
340
|
+
callbacksRef.current.setUnderlyingValue(committedTarget);
|
|
341
|
+
}
|
|
342
|
+
setCommittedValue(committedTarget);
|
|
343
|
+
// Clear coordination flags synchronously alongside the
|
|
344
|
+
// value flip so the next render reflects both at once.
|
|
345
|
+
setIsCoordinating(false);
|
|
346
|
+
setIsWaitingForPeers(false);
|
|
347
|
+
},
|
|
348
|
+
minWaitMs: timingRef.current.minWaitMs,
|
|
349
|
+
multiPeerExtraMinWaitMs: timingRef.current.multiPeerExtraMinWaitMs,
|
|
350
|
+
lazyMinWaitMs: timingRef.current.lazyMinWaitMs,
|
|
351
|
+
gracePeriodMs: timingRef.current.gracePeriodMs,
|
|
352
|
+
ultimateTimeoutMs: timingRef.current.ultimateTimeoutMs,
|
|
353
|
+
lazyCommitPriority: timingRef.current.lazyCommitPriority,
|
|
354
|
+
onWaitingForPeers: () => {
|
|
355
|
+
setIsWaitingForPeers(true);
|
|
356
|
+
},
|
|
357
|
+
isOriginator,
|
|
358
|
+
// Non-originators (e.g. storage echoes) anchor to the
|
|
359
|
+
// originator's announce time if a barrier is already open
|
|
360
|
+
// so late joiners share the same wall-clock deadline.
|
|
361
|
+
announceTime: !isOriginator && getBarrierAnnounceTime(channelKey, target) || Date.now()
|
|
362
|
+
});
|
|
363
|
+
handleRef.current = handle;
|
|
364
|
+
handle.settled.then(() => {
|
|
365
|
+
if (handleRef.current === handle) {
|
|
366
|
+
handleRef.current = null;
|
|
367
|
+
inFlightTargetRef.current = {
|
|
368
|
+
has: false
|
|
369
|
+
};
|
|
370
|
+
setIsCoordinating(false);
|
|
371
|
+
setIsWaitingForPeers(false);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}, [channelKey, peerId]);
|
|
375
|
+
|
|
376
|
+
// Keep the ref pointed at the latest `runCoordination` so the
|
|
377
|
+
// peer-registration callback (set once at mount) always invokes
|
|
378
|
+
// the current closure.
|
|
379
|
+
// eslint-disable-next-line react-hooks/refs -- forward-declared latest-ref: the once-registered peer callback must invoke the current runCoordination closure without re-registering
|
|
380
|
+
runCoordinationRef.current = runCoordination;
|
|
381
|
+
|
|
382
|
+
// Receiver flow: external `underlyingValue` change that we did not
|
|
383
|
+
// originate. Trigger a local coordination so this tab's peers commit
|
|
384
|
+
// together. Uses `useLayoutEffect` so the announcement lands in the
|
|
385
|
+
// same synchronous flush as the originator's broadcast — otherwise
|
|
386
|
+
// a sibling peer's `runCoordination` would slip past whatever
|
|
387
|
+
// `setTimeout` cadence the test (or browser) is driving and miss
|
|
388
|
+
// the originator's barrier window.
|
|
389
|
+
React.useLayoutEffect(() => {
|
|
390
|
+
if (channelKey === null) {
|
|
391
|
+
// Coordination disabled: the hook is a transparent pass-through,
|
|
392
|
+
// so the visible value / pending value are derived directly from
|
|
393
|
+
// `underlyingValue` at the return rather than mirrored into state.
|
|
394
|
+
// Only reset the in-flight ref here.
|
|
395
|
+
inFlightTargetRef.current = {
|
|
396
|
+
has: false
|
|
397
|
+
};
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (lastWrittenRef.current.has) {
|
|
401
|
+
if (Object.is(lastWrittenRef.current.value, underlyingValue)) {
|
|
402
|
+
// Echo of our own write — already coordinated. Treat the
|
|
403
|
+
// sentinel as one-shot: consume it here so a *subsequent*
|
|
404
|
+
// external write that happens to round-trip to the same
|
|
405
|
+
// value (e.g. local→`b`, external→`c`, external→`b`) is
|
|
406
|
+
// recognized as a genuine external change rather than
|
|
407
|
+
// misclassified as another echo.
|
|
408
|
+
lastWrittenRef.current = {
|
|
409
|
+
has: false
|
|
410
|
+
};
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Sentinel is stale: the underlying moved to something other
|
|
414
|
+
// than what we last wrote, so an earlier no-op originator
|
|
415
|
+
// write (which produced no echo) must have left the sentinel
|
|
416
|
+
// armed. Clear it now so a later external round-trip back to
|
|
417
|
+
// that value isn't suppressed.
|
|
418
|
+
lastWrittenRef.current = {
|
|
419
|
+
has: false
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (handleRef.current && inFlightTargetRef.current.has && Object.is(inFlightTargetRef.current.value, underlyingValue) && Object.is(pendingValue, underlyingValue)) {
|
|
423
|
+
// Already coordinating this target.
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (Object.is(committedValue, underlyingValue)) {
|
|
427
|
+
// No external change to react to. We deliberately skip running
|
|
428
|
+
// `preload` on mount when the underlying value already matches
|
|
429
|
+
// our committed value: preload only needs to run when the value
|
|
430
|
+
// changes out from under us without the consumer calling the
|
|
431
|
+
// setter (e.g. a hydration write from `localStorage` or a
|
|
432
|
+
// parent state update). After the consumer interacts with the
|
|
433
|
+
// setter, the originator flow drives every cycle.
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- receiver flow: runCoordination drives the coordination state machine in response to an external underlyingValue change; this is the genuine effect side-effect, not derivable during render
|
|
437
|
+
runCoordination(underlyingValue, false);
|
|
438
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
439
|
+
}, [channelKey, underlyingValue, runCoordination]);
|
|
440
|
+
const coordinatedSetValue = React.useCallback(action => {
|
|
441
|
+
// Functional updaters need to see the latest target — even
|
|
442
|
+
// before React has re-rendered with the new `pendingValue` —
|
|
443
|
+
// so that bursts like `setValue(p => p + 1); setValue(p => p +
|
|
444
|
+
// 1)` compose to +2 instead of +1. `inFlightTargetRef` is
|
|
445
|
+
// updated synchronously inside `runCoordination` before
|
|
446
|
+
// `announceTarget` runs, so it's the freshest source.
|
|
447
|
+
const base = inFlightTargetRef.current.has ? inFlightTargetRef.current.value : pendingValue;
|
|
448
|
+
const next = typeof action === 'function' ? action(base) : action;
|
|
449
|
+
runCoordination(next, true);
|
|
450
|
+
}, [pendingValue, runCoordination]);
|
|
451
|
+
const extras = React.useMemo(() => ({
|
|
452
|
+
pendingValue,
|
|
453
|
+
isCoordinating,
|
|
454
|
+
isWaitingForPeers
|
|
455
|
+
}), [pendingValue, isCoordinating, isWaitingForPeers]);
|
|
456
|
+
|
|
457
|
+
// When coordination is disabled the hook is a transparent
|
|
458
|
+
// pass-through: derive the visible value, pending value, and inert
|
|
459
|
+
// coordination flags straight from `underlyingValue` rather than
|
|
460
|
+
// mirroring it into `committedValue` / `pendingValue` state.
|
|
461
|
+
if (channelKey === null) {
|
|
462
|
+
return [underlyingValue, coordinatedSetValue, {
|
|
463
|
+
pendingValue: underlyingValue,
|
|
464
|
+
isCoordinating: false,
|
|
465
|
+
isWaitingForPeers: false
|
|
466
|
+
}];
|
|
467
|
+
}
|
|
468
|
+
return [committedValue, coordinatedSetValue, extras];
|
|
469
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declares the calling component as a source of an initial, post-hydration
|
|
3
|
+
* layout shift — e.g. a code block that swaps from its plain fallback to
|
|
4
|
+
* highlighted (and focus-collapsed) output once it hydrates.
|
|
5
|
+
*
|
|
6
|
+
* While any registered source is unsettled, {@link useCoordinated} holds its
|
|
7
|
+
* layout-shifting commits, so a page-wide transform/variant change lands as a
|
|
8
|
+
* single unified update instead of a cascade as blocks swap in at staggered
|
|
9
|
+
* idle times. The host doesn't wire anything into its coordinated hooks — this
|
|
10
|
+
* registration is the only opt-in.
|
|
11
|
+
*
|
|
12
|
+
* Pass `settled: true` once the component has reached its stable
|
|
13
|
+
* post-hydration layout. Registration happens on mount and is released on
|
|
14
|
+
* unmount, so a component that unmounts before it settles can't hold the gate
|
|
15
|
+
* open for the rest of the page.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useCoordinatedLazy(settled: boolean): void;
|