| 1 |
import { useEffect, useRef } from '@wordpress/element'; |
| 2 |
import { select } from '@wordpress/data'; |
| 3 |
|
| 4 |
/** |
| 5 |
* IDs claimed this session before save round-trip. Save-time projection is the real dedup. |
| 6 |
*/ |
| 7 |
const sessionClaimedIds = new Set(); |
| 8 |
|
| 9 |
function collectAllIdsFromBlocks( blocks, out = [] ) { |
| 10 |
for ( const b of blocks ) { |
| 11 |
const id = b.attributes?.id; |
| 12 |
if ( typeof id === 'number' && id > 0 ) out.push( id ); |
| 13 |
if ( b.innerBlocks && b.innerBlocks.length ) { |
| 14 |
collectAllIdsFromBlocks( b.innerBlocks, out ); |
| 15 |
} |
| 16 |
} |
| 17 |
return out; |
| 18 |
} |
| 19 |
|
| 20 |
/** |
| 21 |
* Allocate a field ID via REST when `id === 0` on first mount. Failures stay at 0 until save. |
| 22 |
*/ |
| 23 |
const MAX_ALLOCATE_ATTEMPTS = 3; |
| 24 |
|
| 25 |
export function useAllocateFieldId( currentId, setAttributes ) { |
| 26 |
const attempted = useRef( false ); |
| 27 |
const failures = useRef( 0 ); |
| 28 |
|
| 29 |
useEffect( () => { |
| 30 |
if ( currentId > 0 ) return; |
| 31 |
if ( attempted.current ) return; |
| 32 |
attempted.current = true; |
| 33 |
|
| 34 |
const cfg = ( typeof window !== 'undefined' && window.wppbFb && window.wppbFb.allocate ) || {}; |
| 35 |
if ( ! cfg.restRoot || ! cfg.restNonce ) return; |
| 36 |
|
| 37 |
// Read the tree's claimed ids LAZILY, once, here. A `useSelect` would |
| 38 |
// re-walk the whole block tree in every mounted field block on every |
| 39 |
// core/block-editor emission (Gutenberg emits per keystroke) — O(n²) node |
| 40 |
// visits per keypress on a large form — to feed a value only ever consumed |
| 41 |
// on this one-shot path. |
| 42 |
const currentTreeIds = collectAllIdsFromBlocks( select( 'core/block-editor' ).getBlocks() ); |
| 43 |
|
| 44 |
const claimed = Array.from( new Set( [ ...currentTreeIds, ...sessionClaimedIds ] ) ); |
| 45 |
|
| 46 |
fetch( cfg.restRoot, { |
| 47 |
method: 'POST', |
| 48 |
credentials: 'same-origin', |
| 49 |
headers: { |
| 50 |
'X-WP-Nonce': cfg.restNonce, |
| 51 |
'Content-Type': 'application/json', |
| 52 |
}, |
| 53 |
body: JSON.stringify( { claimed } ), |
| 54 |
} ) |
| 55 |
.then( ( r ) => ( r.ok ? r.json() : null ) ) |
| 56 |
.then( ( data ) => { |
| 57 |
const allocated = data && typeof data.id === 'number' ? data.id : 0; |
| 58 |
if ( allocated > 0 ) { |
| 59 |
sessionClaimedIds.add( allocated ); |
| 60 |
setAttributes( { id: allocated } ); |
| 61 |
return; |
| 62 |
} |
| 63 |
releaseForRetry(); |
| 64 |
} ) |
| 65 |
.catch( releaseForRetry ); |
| 66 |
|
| 67 |
// A failed allocation used to be one-shot: `attempted` stayed true, the |
| 68 |
// block kept id === 0, and scheduleAttributePersist() bails on id <= 0 — |
| 69 |
// so ALL per-edit persistence for this block was silently dead for the |
| 70 |
// rest of the session (the save-time allocator only covers the on-canvas |
| 71 |
// path, never a not-in-form card). Re-open the gate, bounded, so a later |
| 72 |
// render tries again. |
| 73 |
function releaseForRetry() { |
| 74 |
failures.current += 1; |
| 75 |
if ( failures.current < MAX_ALLOCATE_ATTEMPTS ) attempted.current = false; |
| 76 |
} |
| 77 |
}, [ currentId ] ); |
| 78 |
} |
| 79 |
|