···1212This importer automatically protects your PDS by:
1313- Limiting imports to **1,000 records per day** (with 75% safety margin)
1414- Calculating optimal batch sizes and delays
1515-- Pausing 24 hours between days for large imports
1515+- Automatically waiting for rate limit resets when limits are hit
1616- Providing clear progress tracking and time estimates
17171818For more details, see the [Bluesky Rate Limits Documentation](https://docs.bsky.app/blog/rate-limits-pds-v3).
···5858- ✅ **Input Deduplication**: Removes duplicate entries within the source file before submission
5959- ✅ **Batch Operations**: Uses `com.atproto.repo.applyWrites` for efficient batch publishing (up to 200 records per call)
6060- ✅ **Rate Limiting**: Automatic daily limits prevent PDS rate limiting
6161-- ✅ **Multi-Day Imports**: Large imports automatically span multiple days with 24-hour pauses
6161+- ✅ **Automatic Rate Limiting**: Waits for limit resets when daily/hourly limits are reached
6262+- ✅ **Multi-Day Imports**: Large imports automatically span multiple days with automatic waits
6263- ✅ **Resume Support**: Safe to stop (Ctrl+C) and restart - continues from where it left off
6364- ✅ **Graceful Cancellation**: Press Ctrl+C to stop after the current batch completes
6465···4114123. Calculates optimal batch size and delay to spread records evenly
4124134. Enforces minimum delay between batches
4134145. Shows clear schedule before starting
415415+6. Logs waiting periods when rate limits are hit with duration
416416+417417+**Example rate-limit wait logging:**
418418+```
419419+ℹ Rate limit (hourly), waiting 23m 45s for reset
420420+ℹ Rate limit (daily), waiting 1h 12m 30s for reset
421421+```
414422415423### Multi-Day Imports
416424···4184261. **Calculates a schedule**: Splits your import across multiple days
4194272. **Shows the plan**: Displays which records will be imported each day
4204283. **Processes Day 1**: Imports the first batch of records
421421-4. **Pauses 24 hours**: Waits a full day before continuing
429429+4. **Waits for reset**: When limits are reached, waits for the hourly/daily reset
4224305. **Repeats**: Continues until all records are imported
423431424432**Example output for a 20,000 record import:**
425433```
426426-📊 Rate Limiting Information:
427427- Total records: 20,000
428428- Daily limit: 7,500 records/day
429429- Estimated duration: 3 days
430430- Batch size: 200 records
431431- Batch delay: 11.52s
434434+=== Batch Configuration ===
435435+ℹ Using auto-calculated batch size: 200 records
436436+ℹ Batch delay: 11520ms
437437+438438+=== Import Configuration ===
439439+ℹ Total records: 20,000
440440+ℹ Batch size: 200 records
441441+ℹ Batch delay: 11520ms
442442+ℹ Duration: 3 days (7,500 records/day limit)
443443+⚠️ Large import will span multiple days with automatic rate-limit waits
444444+445445+=== Publishing Records ===
446446+→ Processed batch 1-200 (0.0s, 173.2 rec/s, 2m 0s remaining)
447447+→ Processed batch 201-400 (2.0s, 100.0 rec/s, 1m 58s remaining)
448448+...
449449+ℹ Rate limit (hourly), waiting 45m 0s for reset
450450+→ Resuming after rate limit reset
451451+→ Processed batch 7801-8000 (45m 0s, 2.9 rec/s, 4h 35m 50s remaining)
432452```
433453434454**Important notes:**
···446466- **Yellow (⚠️)**: Warnings
447467- **Red (✗)**: Errors
448468- **Bold Red (🛑)**: Fatal errors
469469+- **Blue (ℹ)**: Informational messages (including rate-limit waits)
470470+471471+### Rate-Limit Wait Messages
472472+473473+When the importer hits a rate limit and needs to wait, it logs a clear message:
474474+475475+```
476476+ℹ Rate limit (hourly), waiting 23m 45s for reset
477477+ℹ Rate limit (daily), waiting 1h 12m 30s for reset
478478+```
479479+480480+These messages use `formatDuration()` for human-readable duration display (e.g., `23m 45s`, `1h 12m 30s`). The wait reason indicates which limit was hit:
481481+- **hourly**: Hourly rate limit reached
482482+- **daily**: Daily rate limit reached
449483450484### Verbosity Levels
451485···471505- **Network errors**: Failed records are logged but don't stop the import
472506- **Invalid data**: Skipped with error messages
473507- **Authentication issues**: Clear error messages with suggested fixes
474474-- **Rate limit hits**: Automatic adjustment and retry logic
508508+- **Rate limit hits**: Automatic adjustment with logged backoff duration and retry logic
475509- **Ctrl+C handling**: Gracefully stops after current batch
476510477511## Troubleshooting
···489523### Performance Issues
490524491525**"Rate limit exceeded"**
492492-- The importer should prevent this automatically
493493-- If you see this, wait 24 hours before retrying
526526+- The importer handles this automatically by waiting for reset
527527+- Progress messages show wait duration when rate limits are hit
494528- Consider reducing batch size with `-b` flag
495529496530**Import seems stuck**
497531- Check progress messages - large imports take time
498498-- Multi-day imports pause for 24 hours between days
532532+- Rate-limit waits may occur between days
499533- You can safely stop (Ctrl+C) and resume later
500534- Use `--verbose` flag to see detailed progress
501535
···11/**
22 * TID (Timestamp Identifier) generation for ATProto
33- *
33+ *
44 * This module provides a high-level API for TID generation.
55 * For the full implementation with monotonicity guarantees, see tid-clock.ts
66- *
66+ *
77 * Based on: https://atproto.com/specs/tid
88 */
99···1616 * Initialize the global TID clock
1717 * Should be called once at application startup
1818 */
1919-export function initTidClock(options: {
2020- mode?: 'production' | 'dry-run';
2121- statePath?: string;
2222- seed?: number;
2323- clockId?: number;
2424-} = {}): void {
1919+export function initTidClock(
2020+ options: {
2121+ mode?: 'production' | 'dry-run';
2222+ statePath?: string;
2323+ seed?: number;
2424+ clockId?: number;
2525+ } = {},
2626+): void {
2527 const { mode = 'production', statePath, seed, clockId } = options;
26282729 if (mode === 'dry-run' && seed !== undefined) {
2830 // Deterministic clock for dry-run with fixed clockId for reproducibility
2929- globalClock = new TidClock(new FakeClock(seed), undefined, {
3131+ globalClock = new TidClock(new FakeClock(seed), undefined, {
3032 statePath,
3131- clockId: clockId ?? 0 // Use fixed clockId for deterministic TIDs
3333+ clockId: clockId ?? 0, // Use fixed clockId for deterministic TIDs
3234 });
3335 } else {
3436 // Production real-time clock
3535- globalClock = new TidClock(new RealClock(), undefined, { statePath, clockId });
3737+ globalClock = new TidClock(new RealClock(), undefined, {
3838+ statePath,
3939+ clockId,
4040+ });
3641 }
3742}
3843···49545055/**
5156 * Generate a TID from a Date object with monotonicity guarantees
5252- *
5757+ *
5358 * This is the recommended way to generate TIDs for historical records.
5459 * TIDs are guaranteed to be:
5560 * - Spec-compliant (13 chars, valid base32)
5661 * - Strictly monotonic (even if dates are out of order)
5762 * - Collision-free (duplicate detection)
5858- *
6363+ *
5964 * @param date - The date to generate a TID from
6065 * @param context - Optional context for logging (e.g., "inject:playlist")
6166 * @returns A valid 13-character TID string
···67726873/**
6974 * Generate a TID from an ISO 8601 timestamp string
7070- *
7575+ *
7176 * @param isoString - ISO 8601 formatted datetime string
7277 * @param context - Optional context for logging
7378 * @returns A valid 13-character TID string
···78837984/**
8085 * Generate next TID using current time
8181- *
8686+ *
8287 * @param context - Optional context for logging
8388 * @returns A valid 13-character TID string
8489 */
···89949095/**
9196 * Validate a TID string
9292- *
9797+ *
9398 * @param tid - The TID to validate
9499 * @returns true if valid, false otherwise
95100 */