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'; 2 3 - import { HeadersObject, XRPC } from '@atcute/client'; 4 import { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 5 import { chunked } from '@mary/array-fns'; 6 ··· 12 import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 14 import { parseAtUri } from '~/api/utils/strings'; 15 import { ThreadgateApplicatorConstraints } from '../page'; 16 17 const Step4_Confirmation = ({ ··· 22 }: WizardStepProps<ThreadgateApplicatorConstraints, 'Step4_Confirmation'>) => { 23 const [checked, setChecked] = createSignal(false); 24 25 - const [status, setStatus] = createSignal<string>(); 26 const [error, setError] = createSignal<string>(); 27 28 const mutation = createMutation({ 29 async mutationFn() { 30 - setStatus(`Preparing records`); 31 32 const rules = data.rules; 33 const writes: ComAtprotoRepoApplyWrites.Input['writes'] = []; ··· 83 } 84 } 85 86 const did = data.profile.didDoc.id; 87 const rpc = new XRPC({ handler: data.manager }); 88 89 - const total = writes.length; 90 - let written = 0; 91 - for (const chunk of chunked(writes, 200)) { 92 - setStatus(`Writing records (${written}/${total})`); 93 94 - const { headers } = await rpc.call('com.atproto.repo.applyWrites', { 95 - data: { 96 - repo: did, 97 - writes: chunk, 98 - }, 99 - }); 100 101 - written += chunk.length; 102 103 - await waitForRatelimit(headers, 150 * 3); 104 } 105 }, 106 onMutate() { 107 setError(); 108 - }, 109 - onSettled() { 110 - setStatus(); 111 }, 112 onSuccess() { 113 onNext('Step5_Finished', {}); 114 }, 115 onError(error) { 116 let message: string | undefined; 117 118 if (message !== undefined) { 119 - setError(message); 120 } else { 121 console.error(error); 122 - setError(`Something went wrong: ${error}`); 123 } 124 }, 125 }); ··· 139 140 <ToggleInput label="I understand" required checked={checked()} onChange={setChecked} /> 141 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> 148 149 <StageErrorView error={error()} /> 150 ··· 161 }; 162 163 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 - };
··· 1 + import { createSignal, Show } from 'solid-js'; 2 3 + import { XRPC, XRPCError } from '@atcute/client'; 4 import { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 5 import { chunked } from '@mary/array-fns'; 6 ··· 12 import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 14 import { parseAtUri } from '~/api/utils/strings'; 15 + import Logger, { createLogger } from '~/components/logger'; 16 import { ThreadgateApplicatorConstraints } from '../page'; 17 18 const Step4_Confirmation = ({ ··· 23 }: WizardStepProps<ThreadgateApplicatorConstraints, 'Step4_Confirmation'>) => { 24 const [checked, setChecked] = createSignal(false); 25 26 const [error, setError] = createSignal<string>(); 27 28 + const [isLoggerVisible, setIsLoggerVisible] = createSignal(false); 29 + const logger = createLogger(); 30 + 31 const mutation = createMutation({ 32 async mutationFn() { 33 + logger.log(`Preparing writes`); 34 35 const rules = data.rules; 36 const writes: ComAtprotoRepoApplyWrites.Input['writes'] = []; ··· 86 } 87 } 88 89 + logger.log(`${writes.length} write operations to apply`); 90 + 91 const did = data.profile.didDoc.id; 92 const rpc = new XRPC({ handler: data.manager }); 93 94 + const RATELIMIT_POINT_LIMIT = 150 * 3; 95 96 + { 97 + using progress = logger.progress(`Applying writes`); 98 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 + }); 108 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 + } 148 } 149 }, 150 onMutate() { 151 setError(); 152 + setIsLoggerVisible(true); 153 }, 154 onSuccess() { 155 + logger.log(`All writes applied`); 156 onNext('Step5_Finished', {}); 157 }, 158 onError(error) { 159 let message: string | undefined; 160 161 if (message !== undefined) { 162 + logger.error(message); 163 } else { 164 console.error(error); 165 + logger.error(`Something went wrong:\n${error}`); 166 } 167 }, 168 }); ··· 182 183 <ToggleInput label="I understand" required checked={checked()} onChange={setChecked} /> 184 185 + <Show when={isLoggerVisible()}> 186 + <Logger logger={logger} /> 187 + </Show> 188 189 <StageErrorView error={error()} /> 190 ··· 201 }; 202 203 export default Step4_Confirmation;