A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

Stabilize scheduler wakeups and PM2 restart behavior

jack a7c53b8f 847962d5

+127 -29
+31 -4
src/index.ts
··· 2153 2153 clearBackfill, 2154 2154 getNextCheckTime, 2155 2155 getPendingBackfills, 2156 + getSchedulerWakeSignal, 2156 2157 startServer, 2157 2158 updateAppStatus, 2158 2159 updateLastCheckTime, ··· 2285 2286 // Concurrency limit for processing accounts 2286 2287 const runLimit = pLimit(3); 2287 2288 let deferredScheduledRun = false; 2289 + let lastWakeSignal = getSchedulerWakeSignal(); 2290 + 2291 + const sleepWithWake = async (durationMs: number) => { 2292 + const intervalMs = 250; 2293 + const end = Date.now() + durationMs; 2294 + 2295 + while (Date.now() < end) { 2296 + const wakeSignal = getSchedulerWakeSignal(); 2297 + if (wakeSignal > lastWakeSignal) { 2298 + lastWakeSignal = wakeSignal; 2299 + return; 2300 + } 2301 + 2302 + const remainingMs = Math.max(0, end - Date.now()); 2303 + await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remainingMs))); 2304 + } 2305 + }; 2288 2306 2289 2307 // Main loop 2290 2308 while (true) { ··· 2294 2312 2295 2313 const isScheduledRunDue = now >= nextTime; 2296 2314 const pendingBackfills = getPendingBackfills(); 2297 - const shouldRunScheduledCycle = isScheduledRunDue || (deferredScheduledRun && pendingBackfills.length === 0); 2315 + const wakeSignal = getSchedulerWakeSignal(); 2316 + const wakeRequested = wakeSignal > lastWakeSignal; 2317 + if (wakeRequested) { 2318 + lastWakeSignal = wakeSignal; 2319 + } 2320 + 2321 + const shouldRunScheduledCycle = 2322 + isScheduledRunDue || 2323 + (deferredScheduledRun && pendingBackfills.length === 0) || 2324 + (wakeRequested && pendingBackfills.length === 0); 2298 2325 2299 2326 if (isScheduledRunDue && pendingBackfills.length > 0) { 2300 2327 deferredScheduledRun = true; ··· 2337 2364 }); 2338 2365 } 2339 2366 2340 - await new Promise((resolve) => setTimeout(resolve, 2000)); 2367 + await sleepWithWake(2000); 2341 2368 } else if (shouldRunScheduledCycle) { 2342 2369 console.log( 2343 2370 deferredScheduledRun && !isScheduledRunDue ··· 2369 2396 updateAppStatus({ state: 'idle', message: 'Scheduled checks complete' }); 2370 2397 } 2371 2398 2372 - // Sleep for 5 seconds 2373 - await new Promise((resolve) => setTimeout(resolve, 5000)); 2399 + // Sleep briefly between loop iterations, but wake early when UI actions request work. 2400 + await sleepWithWake(5000); 2374 2401 } 2375 2402 } 2376 2403
+23 -2
src/server.ts
··· 476 476 } 477 477 let pendingBackfills: PendingBackfill[] = []; 478 478 let backfillSequence = 0; 479 + let schedulerWakeSignal = 0; // Monotonic counter to wake scheduler loop immediately. 479 480 480 481 interface AppStatus { 481 482 state: 'idle' | 'checking' | 'backfilling' | 'pacing' | 'processing'; ··· 496 497 let updateJobState: UpdateJobState = { 497 498 running: false, 498 499 }; 500 + 501 + function signalSchedulerWake(): void { 502 + schedulerWakeSignal += 1; 503 + } 504 + 505 + function requestImmediateSchedulerPass(): void { 506 + lastCheckTime = 0; 507 + nextCheckTime = Date.now() + 250; 508 + signalSchedulerWake(); 509 + } 499 510 500 511 app.use(cors()); 501 512 app.use(express.json()); ··· 1880 1891 return; 1881 1892 } 1882 1893 1883 - lastCheckTime = 0; 1884 - nextCheckTime = Date.now() + 1000; 1894 + requestImmediateSchedulerPass(); 1885 1895 res.json({ success: true, message: 'Check triggered' }); 1886 1896 }); 1887 1897 ··· 1893 1903 backfillMappingId: undefined, 1894 1904 backfillRequestId: undefined, 1895 1905 }); 1906 + signalSchedulerWake(); 1896 1907 res.json({ success: true, message: 'All backfills cleared' }); 1897 1908 }); 1898 1909 ··· 1931 1942 requestId, 1932 1943 }); 1933 1944 pendingBackfills.sort((a, b) => a.sequence - b.sequence); 1945 + signalSchedulerWake(); 1934 1946 1935 1947 res.json({ 1936 1948 success: true, ··· 1955 1967 } 1956 1968 1957 1969 pendingBackfills = pendingBackfills.filter((entry) => entry.id !== id); 1970 + signalSchedulerWake(); 1958 1971 res.json({ success: true }); 1959 1972 }); 1960 1973 ··· 2122 2135 2123 2136 export function getNextCheckTime(): number { 2124 2137 return nextCheckTime; 2138 + } 2139 + 2140 + export function getSchedulerWakeSignal(): number { 2141 + return schedulerWakeSignal; 2142 + } 2143 + 2144 + export function triggerImmediateRun(): void { 2145 + requestImmediateSchedulerPass(); 2125 2146 } 2126 2147 2127 2148 export function clearBackfill(id: string, requestId?: string) {
+73 -23
update.sh
··· 24 24 STASH_REF="" 25 25 STASH_CREATED=0 26 26 STASH_RESTORED=0 27 + UNTRACKED_COUNT=0 27 28 28 29 BACKUP_SOURCES=() 29 30 BACKUP_PATHS=() ··· 89 90 local base 90 91 base="$(basename "$file")" 91 92 local backup_path 92 - backup_path="$(mktemp "${TMPDIR:-/tmp}/tweets2bsky-${base}.XXXXXX")" 93 + backup_path="$(mktemp_file "tweets2bsky-${base}")" 93 94 cp "$file" "$backup_path" 94 95 BACKUP_SOURCES+=("$file") 95 96 BACKUP_PATHS+=("$backup_path") ··· 112 113 release_lock 113 114 } 114 115 116 + mktemp_file() { 117 + local prefix="$1" 118 + 119 + if mktemp --version >/dev/null 2>&1; then 120 + mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX" 121 + return 0 122 + fi 123 + 124 + local tmp_root 125 + tmp_root="${TMPDIR:-/tmp}" 126 + mktemp -t "${prefix}.XXXXXX" 2>/dev/null || mktemp "${tmp_root}/${prefix}.XXXXXX" 127 + } 128 + 115 129 ensure_git_repo() { 116 130 if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 117 131 echo "❌ This directory is not a git repository: $SCRIPT_DIR" ··· 185 199 exit 1 186 200 } 187 201 188 - working_tree_dirty() { 189 - [[ -n "$(git status --porcelain --untracked-files=normal)" ]] 202 + tracked_tree_dirty() { 203 + [[ -n "$(git status --porcelain --untracked-files=no)" ]] 190 204 } 191 205 192 206 stash_local_changes() { 193 - if ! working_tree_dirty; then 207 + if ! tracked_tree_dirty; then 194 208 return 0 195 209 fi 196 210 ··· 199 213 local before after message 200 214 before="$(git stash list -n 1 --format=%gd || true)" 201 215 message="tweets-2-bsky-update-autostash-$(date -u +%Y%m%d-%H%M%S)" 202 - git stash push -u -m "$message" >/dev/null 216 + git stash push -m "$message" >/dev/null 203 217 after="$(git stash list -n 1 --format=%gd || true)" 204 218 205 219 if [[ -n "$after" && "$after" != "$before" ]]; then ··· 209 223 fi 210 224 } 211 225 226 + print_untracked_notice() { 227 + local count 228 + count="$(git ls-files --others --exclude-standard | wc -l | tr -d '[:space:]')" 229 + UNTRACKED_COUNT="$count" 230 + 231 + if [[ "$count" -gt 0 ]]; then 232 + echo "ℹ️ Leaving $count untracked file(s) untouched (not stashed)." 233 + echo " This avoids slow/hanging updates on large data directories." 234 + fi 235 + } 236 + 212 237 restore_stash_if_needed() { 213 238 if [[ "$STASH_CREATED" -ne 1 || -z "$STASH_REF" ]]; then 214 239 return 0 ··· 344 369 345 370 echo "🔄 Restarting runtime..." 346 371 347 - if pm2_has_process "$APP_NAME"; then 348 - pm2 restart "$APP_NAME" --update-env >/dev/null 2>&1 || { 349 - echo "⚠️ PM2 restart failed for $APP_NAME. Recreating process..." 350 - pm2 delete "$APP_NAME" >/dev/null 2>&1 || true 351 - pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 352 - } 353 - pm2 save >/dev/null 2>&1 || true 354 - echo "✅ Restarted PM2 process: $APP_NAME" 355 - return 0 356 - fi 372 + if command -v pm2 >/dev/null 2>&1; then 373 + local has_app=0 374 + local has_legacy=0 357 375 358 - if pm2_has_process "$LEGACY_APP_NAME"; then 359 - pm2 restart "$LEGACY_APP_NAME" --update-env >/dev/null 2>&1 || { 360 - echo "⚠️ PM2 restart failed for $LEGACY_APP_NAME. Recreating under $APP_NAME..." 376 + if pm2_has_process "$APP_NAME"; then 377 + has_app=1 378 + fi 379 + if pm2_has_process "$LEGACY_APP_NAME"; then 380 + has_legacy=1 381 + fi 382 + 383 + if [[ "$has_app" -eq 1 && "$has_legacy" -eq 1 ]]; then 384 + echo "ℹ️ Found both PM2 processes ($APP_NAME and $LEGACY_APP_NAME). Consolidating to $APP_NAME..." 385 + pm2 restart "$APP_NAME" --update-env >/dev/null 2>&1 || { 386 + pm2 delete "$APP_NAME" >/dev/null 2>&1 || true 387 + pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 388 + } 361 389 pm2 delete "$LEGACY_APP_NAME" >/dev/null 2>&1 || true 362 - pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 363 - } 364 - pm2 save >/dev/null 2>&1 || true 365 - echo "✅ Restarted PM2 process." 366 - return 0 390 + pm2 save >/dev/null 2>&1 || true 391 + echo "✅ Restarted PM2 process: $APP_NAME" 392 + return 0 393 + fi 394 + 395 + if [[ "$has_app" -eq 1 ]]; then 396 + pm2 restart "$APP_NAME" --update-env >/dev/null 2>&1 || { 397 + echo "⚠️ PM2 restart failed for $APP_NAME. Recreating process..." 398 + pm2 delete "$APP_NAME" >/dev/null 2>&1 || true 399 + pm2 start dist/index.js --name "$APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 400 + } 401 + pm2 save >/dev/null 2>&1 || true 402 + echo "✅ Restarted PM2 process: $APP_NAME" 403 + return 0 404 + fi 405 + 406 + if [[ "$has_legacy" -eq 1 ]]; then 407 + pm2 restart "$LEGACY_APP_NAME" --update-env >/dev/null 2>&1 || { 408 + echo "⚠️ PM2 restart failed for $LEGACY_APP_NAME. Recreating it..." 409 + pm2 delete "$LEGACY_APP_NAME" >/dev/null 2>&1 || true 410 + pm2 start dist/index.js --name "$LEGACY_APP_NAME" --cwd "$SCRIPT_DIR" --update-env >/dev/null 2>&1 411 + } 412 + pm2 save >/dev/null 2>&1 || true 413 + echo "✅ Restarted PM2 process: $LEGACY_APP_NAME" 414 + return 0 415 + fi 367 416 fi 368 417 369 418 if nohup_process_running; then ··· 457 506 backup_file "$ENV_FILE" 458 507 459 508 stash_local_changes 509 + print_untracked_notice 460 510 461 511 remote="$(resolve_remote)" 462 512 branch="$(resolve_branch "$remote")"