handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs

fix: wait for ratelimit on error

mary.my.id e62aa970 13a90873

verified
+67 -41
+67 -41
src/views/bluesky/threadgate-applicator/steps/step4_confirmation.tsx
··· 1 - import { createSignal } from 'solid-js'; 1 + import { createSignal, Show } from 'solid-js'; 2 2 3 - import { HeadersObject, XRPC } from '@atcute/client'; 3 + import { XRPC, XRPCError } from '@atcute/client'; 4 4 import { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 5 5 import { chunked } from '@mary/array-fns'; 6 6 ··· 12 12 import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 13 14 14 import { parseAtUri } from '~/api/utils/strings'; 15 + import Logger, { createLogger } from '~/components/logger'; 15 16 import { ThreadgateApplicatorConstraints } from '../page'; 16 17 17 18 const Step4_Confirmation = ({ ··· 22 23 }: WizardStepProps<ThreadgateApplicatorConstraints, 'Step4_Confirmation'>) => { 23 24 const [checked, setChecked] = createSignal(false); 24 25 25 - const [status, setStatus] = createSignal<string>(); 26 26 const [error, setError] = createSignal<string>(); 27 27 28 + const [isLoggerVisible, setIsLoggerVisible] = createSignal(false); 29 + const logger = createLogger(); 30 + 28 31 const mutation = createMutation({ 29 32 async mutationFn() { 30 - setStatus(`Preparing records`); 33 + logger.log(`Preparing writes`); 31 34 32 35 const rules = data.rules; 33 36 const writes: ComAtprotoRepoApplyWrites.Input['writes'] = []; ··· 83 86 } 84 87 } 85 88 89 + logger.log(`${writes.length} write operations to apply`); 90 + 86 91 const did = data.profile.didDoc.id; 87 92 const rpc = new XRPC({ handler: data.manager }); 88 93 89 - const total = writes.length; 90 - let written = 0; 91 - for (const chunk of chunked(writes, 200)) { 92 - setStatus(`Writing records (${written}/${total})`); 94 + const RATELIMIT_POINT_LIMIT = 150 * 3; 93 95 94 - const { headers } = await rpc.call('com.atproto.repo.applyWrites', { 95 - data: { 96 - repo: did, 97 - writes: chunk, 98 - }, 99 - }); 96 + { 97 + using progress = logger.progress(`Applying writes`); 100 98 101 - written += chunk.length; 99 + let written = 0; 100 + for (const chunk of chunked(writes, 200)) { 101 + try { 102 + const { headers } = await rpc.call('com.atproto.repo.applyWrites', { 103 + data: { 104 + repo: did, 105 + writes: chunk, 106 + }, 107 + }); 102 108 103 - await waitForRatelimit(headers, 150 * 3); 109 + written += chunk.length; 110 + progress.update(`Applying writes (${written} applied)`); 111 + 112 + if ('ratelimit-remaining' in headers) { 113 + const remaining = +headers['ratelimit-remaining']; 114 + const reset = +headers['ratelimit-reset'] * 1_000; 115 + 116 + if (remaining < RATELIMIT_POINT_LIMIT) { 117 + // add some delay to be sure 118 + const delta = reset - Date.now() + 5_000; 119 + using _progress = logger.progress(`Reached ratelimit, waiting ${delta}ms`); 120 + 121 + await new Promise((resolve) => setTimeout(resolve, delta)); 122 + } 123 + } 124 + } catch (err) { 125 + if (!(err instanceof XRPCError) || err.kind !== 'RateLimitExceeded') { 126 + throw err; 127 + } 128 + 129 + const headers = err.headers; 130 + if ('ratelimit-remaining' in headers) { 131 + const remaining = +headers['ratelimit-remaining']; 132 + const reset = +headers['ratelimit-reset'] * 1_000; 133 + 134 + if (remaining < RATELIMIT_POINT_LIMIT) { 135 + // add some delay to be sure 136 + const delta = reset - Date.now() + 5_000; 137 + using _progress = logger.progress(`Reached ratelimit, waiting ${delta}ms`); 138 + 139 + await new Promise((resolve) => setTimeout(resolve, delta)); 140 + } 141 + } else { 142 + using _progress = logger.progress(`Reached ratelimit, waiting 1 minute`); 143 + 144 + await new Promise((resolve) => setTimeout(resolve, 60 * 1_000)); 145 + } 146 + } 147 + } 104 148 } 105 149 }, 106 150 onMutate() { 107 151 setError(); 108 - }, 109 - onSettled() { 110 - setStatus(); 152 + setIsLoggerVisible(true); 111 153 }, 112 154 onSuccess() { 155 + logger.log(`All writes applied`); 113 156 onNext('Step5_Finished', {}); 114 157 }, 115 158 onError(error) { 116 159 let message: string | undefined; 117 160 118 161 if (message !== undefined) { 119 - setError(message); 162 + logger.error(message); 120 163 } else { 121 164 console.error(error); 122 - setError(`Something went wrong: ${error}`); 165 + logger.error(`Something went wrong:\n${error}`); 123 166 } 124 167 }, 125 168 }); ··· 139 182 140 183 <ToggleInput label="I understand" required checked={checked()} onChange={setChecked} /> 141 184 142 - <div 143 - hidden={status() === undefined} 144 - class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-gray-500" 145 - > 146 - {status()} 147 - </div> 185 + <Show when={isLoggerVisible()}> 186 + <Logger logger={logger} /> 187 + </Show> 148 188 149 189 <StageErrorView error={error()} /> 150 190 ··· 161 201 }; 162 202 163 203 export default Step4_Confirmation; 164 - 165 - const waitForRatelimit = async (headers: HeadersObject, expected: number) => { 166 - if ('ratelimit-remaining' in headers) { 167 - const remaining = +headers['ratelimit-remaining']; 168 - const reset = +headers['ratelimit-reset'] * 1_000; 169 - 170 - if (remaining < expected) { 171 - // add some delay to be sure 172 - const delta = reset - Date.now() + 5_000; 173 - 174 - await new Promise((resolve) => setTimeout(resolve, delta)); 175 - } 176 - } 177 - };