tangled
alpha
login
or
join now
mary.my.id
/
boat
22
fork
atom
handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
22
fork
atom
overview
issues
pulls
pipelines
fix: wait for ratelimit on error
mary.my.id
1 year ago
e62aa970
13a90873
verified
This commit was signed with the committer's
known signature
.
mary.my.id
SSH Key Fingerprint:
SHA256:ZlTP/auFSGpGnaoDg4mCTG1g9OZvXp62jWR4c6H4O3c=
+67
-41
1 changed file
expand all
collapse all
unified
split
src
views
bluesky
threadgate-applicator
steps
step4_confirmation.tsx
+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';
0
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
0
0
0
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
0
0
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;
0
0
0
0
0
0
0
0
102
103
-
await waitForRatelimit(headers, 150 * 3);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
104
}
105
},
106
onMutate() {
107
setError();
108
-
},
109
-
onSettled() {
110
-
setStatus();
111
},
112
onSuccess() {
0
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
0
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;
0
0
0
95
96
+
{
97
+
using progress = logger.progress(`Applying writes`);
0
0
0
0
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);
0
0
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>
0
0
0
188
189
<StageErrorView error={error()} />
190
···
201
};
202
203
export default Step4_Confirmation;
0
0
0
0
0
0
0
0
0
0
0
0
0
0