+4
api/.env.example
+4
api/.env.example
+22
api/.sqlx/query-8fe482ef09e83d77ac7679d98e5c36c10c6b00c30cc289616a6da9c86ac11006.json
+22
api/.sqlx/query-8fe482ef09e83d77ac7679d98e5c36c10c6b00c30cc289616a6da9c86ac11006.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT time_us\n FROM jetstream_cursor\n WHERE id = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "time_us",
9
+
"type_info": "Int8"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "8fe482ef09e83d77ac7679d98e5c36c10c6b00c30cc289616a6da9c86ac11006"
22
+
}
+15
api/.sqlx/query-ae961e575c6f9d0761401115344ae5993753bd5064fc66344b402aa4ee8cc177.json
+15
api/.sqlx/query-ae961e575c6f9d0761401115344ae5993753bd5064fc66344b402aa4ee8cc177.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n INSERT INTO jetstream_cursor (id, time_us, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (id)\n DO UPDATE SET time_us = $2, updated_at = NOW()\n ",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Int8"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "ae961e575c6f9d0761401115344ae5993753bd5064fc66344b402aa4ee8cc177"
15
+
}
+15
api/.sqlx/query-e167d0d1f047f25f6116097ff025352727645c99faf8fc451678d2a130df7e78.json
+15
api/.sqlx/query-e167d0d1f047f25f6116097ff025352727645c99faf8fc451678d2a130df7e78.json
···
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n INSERT INTO jetstream_cursor (id, time_us, updated_at)\n VALUES ($1, $2, NOW())\n ON CONFLICT (id)\n DO UPDATE SET time_us = $2, updated_at = NOW()\n ",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Int8"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "e167d0d1f047f25f6116097ff025352727645c99faf8fc451678d2a130df7e78"
15
+
}
+43
api/CLAUDE.md
+43
api/CLAUDE.md
···
85
stable pagination
86
- **OAuth DPoP authentication** integrated with AIP server for ATProto
87
authentication
88
89
### Module Organization
90
···
95
- `src/jetstream.rs` - Real-time event processing from ATProto firehose
96
- `src/sync.rs` - Bulk synchronization operations with ATProto relay
97
- `src/auth.rs` - OAuth verification and DPoP authentication setup
98
- `src/errors.rs` - Error type definitions (reference for new errors)
99
100
## Error Handling
···
131
132
- After updating, run `cargo check` to fix errors and warnings
133
- Don't use dead code, if it's not used remove it
···
85
stable pagination
86
- **OAuth DPoP authentication** integrated with AIP server for ATProto
87
authentication
88
+
- **Multi-tier caching** with Redis (if configured) or in-memory fallback for
89
+
performance optimization
90
91
### Module Organization
92
···
97
- `src/jetstream.rs` - Real-time event processing from ATProto firehose
98
- `src/sync.rs` - Bulk synchronization operations with ATProto relay
99
- `src/auth.rs` - OAuth verification and DPoP authentication setup
100
+
- `src/cache.rs` - Generic caching interface and in-memory cache implementation
101
+
- `src/redis_cache.rs` - Redis cache implementation for distributed caching
102
- `src/errors.rs` - Error type definitions (reference for new errors)
103
104
## Error Handling
···
135
136
- After updating, run `cargo check` to fix errors and warnings
137
- Don't use dead code, if it's not used remove it
138
+
139
+
## Caching Architecture
140
+
141
+
The application uses a flexible caching system that supports both Redis and in-memory caching with automatic fallback.
142
+
143
+
### Cache Configuration
144
+
145
+
Configure caching via environment variables:
146
+
147
+
```bash
148
+
# Redis configuration (optional)
149
+
REDIS_URL=redis://localhost:6379
150
+
REDIS_TTL_SECONDS=3600
151
+
```
152
+
153
+
If `REDIS_URL` is not set, the application automatically falls back to in-memory caching.
154
+
155
+
### Cache Types and TTLs
156
+
157
+
- **Actor Cache** (Jetstream): No TTL (permanent cache for slice actors)
158
+
- **Lexicon Cache**: 2 hours (7200s) - lexicons change infrequently
159
+
- **Domain Cache**: 4 hours (14400s) - slice domain mappings rarely change
160
+
- **Collections Cache**: 2 hours (7200s) - slice collections change infrequently
161
+
- **Auth Cache**: 5 minutes (300s) - OAuth tokens and AT Protocol sessions
162
+
- **DID Resolution Cache**: 24 hours (86400s) - DID documents change rarely
163
+
164
+
### Cache Implementation
165
+
166
+
- `src/cache.rs` - Defines the `Cache` trait and `SliceCache` wrapper with domain-specific methods
167
+
- `src/redis_cache.rs` - Redis implementation of the `Cache` trait
168
+
- Both implementations provide the same interface through `SliceCache`
169
+
- Cache keys use prefixed formats (e.g., `actor:{did}:{slice_uri}`, `oauth_userinfo:{token}`)
170
+
171
+
### Cache Usage Patterns
172
+
173
+
- **Jetstream Consumer**: Creates 4 separate cache instances for actors, lexicons, domains, and collections
174
+
- **Auth System**: Uses dedicated auth cache for OAuth and AT Protocol session caching
175
+
- **Actor Resolution**: Caches DID resolution results to avoid repeated lookups
176
+
- **Automatic Fallback**: Redis failures automatically fall back to in-memory caching without errors
+350
-347
api/Cargo.lock
+350
-347
api/Cargo.lock
···
4
5
[[package]]
6
name = "addr2line"
7
-
version = "0.24.2"
8
source = "registry+https://github.com/rust-lang/crates.io-index"
9
-
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10
dependencies = [
11
"gimli",
12
]
···
33
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
34
35
[[package]]
36
-
name = "android-tzdata"
37
-
version = "0.1.1"
38
-
source = "registry+https://github.com/rust-lang/crates.io-index"
39
-
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
40
-
41
-
[[package]]
42
name = "android_system_properties"
43
version = "0.1.5"
44
source = "registry+https://github.com/rust-lang/crates.io-index"
···
49
50
[[package]]
51
name = "anyhow"
52
-
version = "1.0.99"
53
source = "registry+https://github.com/rust-lang/crates.io-index"
54
-
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
55
56
[[package]]
57
name = "anymap2"
58
version = "0.13.0"
59
source = "registry+https://github.com/rust-lang/crates.io-index"
60
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
61
62
[[package]]
63
name = "async-trait"
···
87
88
[[package]]
89
name = "atproto-client"
90
-
version = "0.13.0"
91
source = "registry+https://github.com/rust-lang/crates.io-index"
92
-
checksum = "c34ed7ebeec01cd7775c1c7841838c142d123d403983ed7179b31850435b5c7c"
93
dependencies = [
94
"anyhow",
95
"atproto-identity",
···
101
"reqwest-middleware",
102
"serde",
103
"serde_json",
104
-
"thiserror 2.0.14",
105
"tokio",
106
"tracing",
107
"urlencoding",
···
109
110
[[package]]
111
name = "atproto-identity"
112
-
version = "0.13.0"
113
source = "registry+https://github.com/rust-lang/crates.io-index"
114
-
checksum = "b956c07726fce812630be63c5cb31b1961cbb70f0a05614278523102d78c3a48"
115
dependencies = [
116
"anyhow",
117
"async-trait",
···
128
"serde",
129
"serde_ipld_dagcbor",
130
"serde_json",
131
-
"thiserror 2.0.14",
132
"tokio",
133
"tracing",
134
"urlencoding",
···
136
137
[[package]]
138
name = "atproto-jetstream"
139
-
version = "0.13.0"
140
source = "registry+https://github.com/rust-lang/crates.io-index"
141
-
checksum = "7b1897fb2f7c6d02d46f7b8d25d653c141cee4a68a10efd135d46201a95034db"
142
dependencies = [
143
"anyhow",
144
"async-trait",
···
147
"http",
148
"serde",
149
"serde_json",
150
-
"thiserror 2.0.14",
151
"tokio",
152
"tokio-util",
153
"tokio-websockets",
···
159
160
[[package]]
161
name = "atproto-oauth"
162
-
version = "0.13.0"
163
source = "registry+https://github.com/rust-lang/crates.io-index"
164
-
checksum = "3ea205901c33d074a1b498591d0511bcd788b6772ec0ca6e09a92c4327ddbdff"
165
dependencies = [
166
"anyhow",
167
"async-trait",
···
183
"serde_ipld_dagcbor",
184
"serde_json",
185
"sha2",
186
-
"thiserror 2.0.14",
187
"tokio",
188
"tracing",
189
"ulid",
···
191
192
[[package]]
193
name = "atproto-record"
194
-
version = "0.13.0"
195
source = "registry+https://github.com/rust-lang/crates.io-index"
196
-
checksum = "0550f74423ca745132dc07ba1cb01f2f08243a7bf7497f5c3f2185a774c92ca2"
197
dependencies = [
198
"anyhow",
199
"atproto-identity",
···
202
"serde",
203
"serde_ipld_dagcbor",
204
"serde_json",
205
-
"thiserror 2.0.14",
206
]
207
208
[[package]]
···
306
]
307
308
[[package]]
309
name = "backtrace"
310
-
version = "0.3.75"
311
source = "registry+https://github.com/rust-lang/crates.io-index"
312
-
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
313
dependencies = [
314
"addr2line",
315
"cfg-if",
···
317
"miniz_oxide",
318
"object",
319
"rustc-demangle",
320
-
"windows-targets 0.52.6",
321
]
322
323
[[package]]
···
352
353
[[package]]
354
name = "bitflags"
355
-
version = "2.9.1"
356
source = "registry+https://github.com/rust-lang/crates.io-index"
357
-
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
358
dependencies = [
359
"serde",
360
]
···
397
398
[[package]]
399
name = "cc"
400
-
version = "1.2.33"
401
source = "registry+https://github.com/rust-lang/crates.io-index"
402
-
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
403
dependencies = [
404
"jobserver",
405
"libc",
406
"shlex",
···
408
409
[[package]]
410
name = "cfg-if"
411
-
version = "1.0.1"
412
source = "registry+https://github.com/rust-lang/crates.io-index"
413
-
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
414
415
[[package]]
416
name = "cfg_aliases"
···
420
421
[[package]]
422
name = "chrono"
423
-
version = "0.4.41"
424
source = "registry+https://github.com/rust-lang/crates.io-index"
425
-
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
426
dependencies = [
427
-
"android-tzdata",
428
"iana-time-zone",
429
"js-sys",
430
"num-traits",
431
"serde",
432
"wasm-bindgen",
433
-
"windows-link",
434
]
435
436
[[package]]
···
448
]
449
450
[[package]]
451
name = "concurrent-queue"
452
version = "2.5.0"
453
source = "registry+https://github.com/rust-lang/crates.io-index"
···
725
726
[[package]]
727
name = "errno"
728
-
version = "0.3.13"
729
source = "registry+https://github.com/rust-lang/crates.io-index"
730
-
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
731
dependencies = [
732
"libc",
733
-
"windows-sys 0.60.2",
734
]
735
736
[[package]]
···
772
]
773
774
[[package]]
775
name = "flume"
776
version = "0.11.1"
777
source = "registry+https://github.com/rust-lang/crates.io-index"
···
811
812
[[package]]
813
name = "form_urlencoded"
814
-
version = "1.2.1"
815
source = "registry+https://github.com/rust-lang/crates.io-index"
816
-
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
817
dependencies = [
818
"percent-encoding",
819
]
···
919
]
920
921
[[package]]
922
-
name = "generator"
923
-
version = "0.8.5"
924
-
source = "registry+https://github.com/rust-lang/crates.io-index"
925
-
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
926
-
dependencies = [
927
-
"cc",
928
-
"cfg-if",
929
-
"libc",
930
-
"log",
931
-
"rustversion",
932
-
"windows",
933
-
]
934
-
935
-
[[package]]
936
name = "generic-array"
937
version = "0.14.7"
938
source = "registry+https://github.com/rust-lang/crates.io-index"
···
966
"js-sys",
967
"libc",
968
"r-efi",
969
-
"wasi 0.14.2+wasi-0.2.4",
970
"wasm-bindgen",
971
]
972
973
[[package]]
974
name = "gimli"
975
-
version = "0.31.1"
976
source = "registry+https://github.com/rust-lang/crates.io-index"
977
-
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
978
979
[[package]]
980
name = "group"
···
1018
]
1019
1020
[[package]]
1021
name = "hashlink"
1022
version = "0.10.0"
1023
source = "registry+https://github.com/rust-lang/crates.io-index"
1024
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
1025
dependencies = [
1026
-
"hashbrown",
1027
]
1028
1029
[[package]]
···
1056
"once_cell",
1057
"rand 0.9.2",
1058
"ring",
1059
-
"thiserror 2.0.14",
1060
"tinyvec",
1061
"tokio",
1062
"tracing",
···
1079
"rand 0.9.2",
1080
"resolv-conf",
1081
"smallvec",
1082
-
"thiserror 2.0.14",
1083
"tokio",
1084
"tracing",
1085
]
···
1159
1160
[[package]]
1161
name = "hyper"
1162
-
version = "1.6.0"
1163
source = "registry+https://github.com/rust-lang/crates.io-index"
1164
-
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
1165
dependencies = [
1166
"bytes",
1167
"futures-channel",
1168
-
"futures-util",
1169
"h2",
1170
"http",
1171
"http-body",
···
1173
"httpdate",
1174
"itoa",
1175
"pin-project-lite",
1176
"smallvec",
1177
"tokio",
1178
"want",
···
1213
1214
[[package]]
1215
name = "hyper-util"
1216
-
version = "0.1.16"
1217
source = "registry+https://github.com/rust-lang/crates.io-index"
1218
-
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
1219
dependencies = [
1220
"base64 0.22.1",
1221
"bytes",
···
1239
1240
[[package]]
1241
name = "iana-time-zone"
1242
-
version = "0.1.63"
1243
source = "registry+https://github.com/rust-lang/crates.io-index"
1244
-
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
1245
dependencies = [
1246
"android_system_properties",
1247
"core-foundation-sys",
···
1349
1350
[[package]]
1351
name = "idna"
1352
-
version = "1.0.3"
1353
source = "registry+https://github.com/rust-lang/crates.io-index"
1354
-
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
1355
dependencies = [
1356
"idna_adapter",
1357
"smallvec",
···
1370
1371
[[package]]
1372
name = "indexmap"
1373
-
version = "2.10.0"
1374
source = "registry+https://github.com/rust-lang/crates.io-index"
1375
-
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
1376
dependencies = [
1377
"equivalent",
1378
-
"hashbrown",
1379
]
1380
1381
[[package]]
1382
name = "io-uring"
1383
-
version = "0.7.9"
1384
source = "registry+https://github.com/rust-lang/crates.io-index"
1385
-
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
1386
dependencies = [
1387
"bitflags",
1388
"cfg-if",
···
1446
1447
[[package]]
1448
name = "js-sys"
1449
-
version = "0.3.77"
1450
source = "registry+https://github.com/rust-lang/crates.io-index"
1451
-
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
1452
dependencies = [
1453
"once_cell",
1454
"wasm-bindgen",
···
1479
1480
[[package]]
1481
name = "libc"
1482
-
version = "0.2.175"
1483
source = "registry+https://github.com/rust-lang/crates.io-index"
1484
-
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
1485
1486
[[package]]
1487
name = "libm"
···
1491
1492
[[package]]
1493
name = "libredox"
1494
-
version = "0.1.9"
1495
source = "registry+https://github.com/rust-lang/crates.io-index"
1496
-
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
1497
dependencies = [
1498
"bitflags",
1499
"libc",
···
1512
1513
[[package]]
1514
name = "linux-raw-sys"
1515
-
version = "0.9.4"
1516
source = "registry+https://github.com/rust-lang/crates.io-index"
1517
-
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
1518
1519
[[package]]
1520
name = "litemap"
···
1534
1535
[[package]]
1536
name = "log"
1537
-
version = "0.4.27"
1538
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1539
-
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
1540
-
1541
-
[[package]]
1542
-
name = "loom"
1543
-
version = "0.7.2"
1544
source = "registry+https://github.com/rust-lang/crates.io-index"
1545
-
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
1546
-
dependencies = [
1547
-
"cfg-if",
1548
-
"generator",
1549
-
"scoped-tls",
1550
-
"tracing",
1551
-
"tracing-subscriber",
1552
-
]
1553
1554
[[package]]
1555
name = "lru"
···
1557
source = "registry+https://github.com/rust-lang/crates.io-index"
1558
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
1559
dependencies = [
1560
-
"hashbrown",
1561
]
1562
1563
[[package]]
···
1568
1569
[[package]]
1570
name = "matchers"
1571
-
version = "0.1.0"
1572
source = "registry+https://github.com/rust-lang/crates.io-index"
1573
-
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
1574
dependencies = [
1575
-
"regex-automata 0.1.10",
1576
]
1577
1578
[[package]]
···
1593
1594
[[package]]
1595
name = "memchr"
1596
-
version = "2.7.5"
1597
source = "registry+https://github.com/rust-lang/crates.io-index"
1598
-
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
1599
1600
[[package]]
1601
name = "mime"
···
1635
1636
[[package]]
1637
name = "moka"
1638
-
version = "0.12.10"
1639
source = "registry+https://github.com/rust-lang/crates.io-index"
1640
-
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
1641
dependencies = [
1642
"crossbeam-channel",
1643
"crossbeam-epoch",
1644
"crossbeam-utils",
1645
-
"loom",
1646
"parking_lot",
1647
"portable-atomic",
1648
"rustc_version",
1649
"smallvec",
1650
"tagptr",
1651
-
"thiserror 1.0.69",
1652
"uuid",
1653
]
1654
···
1710
1711
[[package]]
1712
name = "nu-ansi-term"
1713
-
version = "0.46.0"
1714
source = "registry+https://github.com/rust-lang/crates.io-index"
1715
-
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
1716
dependencies = [
1717
-
"overload",
1718
-
"winapi",
1719
]
1720
1721
[[package]]
···
1767
1768
[[package]]
1769
name = "object"
1770
-
version = "0.36.7"
1771
source = "registry+https://github.com/rust-lang/crates.io-index"
1772
-
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
1773
dependencies = [
1774
"memchr",
1775
]
···
1829
]
1830
1831
[[package]]
1832
-
name = "overload"
1833
-
version = "0.1.1"
1834
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1835
-
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
1836
-
1837
-
[[package]]
1838
name = "p256"
1839
version = "0.13.2"
1840
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1900
1901
[[package]]
1902
name = "percent-encoding"
1903
-
version = "2.3.1"
1904
source = "registry+https://github.com/rust-lang/crates.io-index"
1905
-
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
1906
1907
[[package]]
1908
name = "pin-project-lite"
···
1951
1952
[[package]]
1953
name = "potential_utf"
1954
-
version = "0.1.2"
1955
source = "registry+https://github.com/rust-lang/crates.io-index"
1956
-
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
1957
dependencies = [
1958
"zerovec",
1959
]
···
1979
1980
[[package]]
1981
name = "proc-macro2"
1982
-
version = "1.0.97"
1983
source = "registry+https://github.com/rust-lang/crates.io-index"
1984
-
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
1985
dependencies = [
1986
"unicode-ident",
1987
]
1988
1989
[[package]]
1990
name = "quinn"
1991
-
version = "0.11.8"
1992
source = "registry+https://github.com/rust-lang/crates.io-index"
1993
-
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
1994
dependencies = [
1995
"bytes",
1996
"cfg_aliases",
···
1999
"quinn-udp",
2000
"rustc-hash",
2001
"rustls",
2002
-
"socket2 0.5.10",
2003
-
"thiserror 2.0.14",
2004
"tokio",
2005
"tracing",
2006
"web-time",
···
2008
2009
[[package]]
2010
name = "quinn-proto"
2011
-
version = "0.11.12"
2012
source = "registry+https://github.com/rust-lang/crates.io-index"
2013
-
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
2014
dependencies = [
2015
"bytes",
2016
"getrandom 0.3.3",
···
2021
"rustls",
2022
"rustls-pki-types",
2023
"slab",
2024
-
"thiserror 2.0.14",
2025
"tinyvec",
2026
"tracing",
2027
"web-time",
···
2029
2030
[[package]]
2031
name = "quinn-udp"
2032
-
version = "0.5.13"
2033
source = "registry+https://github.com/rust-lang/crates.io-index"
2034
-
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
2035
dependencies = [
2036
"cfg_aliases",
2037
"libc",
2038
"once_cell",
2039
-
"socket2 0.5.10",
2040
"tracing",
2041
-
"windows-sys 0.59.0",
2042
]
2043
2044
[[package]]
···
2116
]
2117
2118
[[package]]
2119
name = "redox_syscall"
2120
version = "0.5.17"
2121
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2126
2127
[[package]]
2128
name = "regex"
2129
-
version = "1.11.2"
2130
source = "registry+https://github.com/rust-lang/crates.io-index"
2131
-
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
2132
dependencies = [
2133
"aho-corasick",
2134
"memchr",
2135
-
"regex-automata 0.4.9",
2136
-
"regex-syntax 0.8.5",
2137
-
]
2138
-
2139
-
[[package]]
2140
-
name = "regex-automata"
2141
-
version = "0.1.10"
2142
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2143
-
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
2144
-
dependencies = [
2145
-
"regex-syntax 0.6.29",
2146
]
2147
2148
[[package]]
2149
name = "regex-automata"
2150
-
version = "0.4.9"
2151
source = "registry+https://github.com/rust-lang/crates.io-index"
2152
-
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
2153
dependencies = [
2154
"aho-corasick",
2155
"memchr",
2156
-
"regex-syntax 0.8.5",
2157
]
2158
2159
[[package]]
2160
name = "regex-syntax"
2161
-
version = "0.6.29"
2162
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2163
-
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
2164
-
2165
-
[[package]]
2166
-
name = "regex-syntax"
2167
-
version = "0.8.5"
2168
source = "registry+https://github.com/rust-lang/crates.io-index"
2169
-
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
2170
2171
[[package]]
2172
name = "reqwest"
···
2245
2246
[[package]]
2247
name = "resolv-conf"
2248
-
version = "0.7.4"
2249
source = "registry+https://github.com/rust-lang/crates.io-index"
2250
-
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
2251
2252
[[package]]
2253
name = "rfc6979"
···
2316
2317
[[package]]
2318
name = "rustix"
2319
-
version = "1.0.8"
2320
source = "registry+https://github.com/rust-lang/crates.io-index"
2321
-
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
2322
dependencies = [
2323
"bitflags",
2324
"errno",
2325
"libc",
2326
"linux-raw-sys",
2327
-
"windows-sys 0.60.2",
2328
]
2329
2330
[[package]]
2331
name = "rustls"
2332
-
version = "0.23.31"
2333
source = "registry+https://github.com/rust-lang/crates.io-index"
2334
-
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
2335
dependencies = [
2336
"once_cell",
2337
"ring",
···
2350
"openssl-probe",
2351
"rustls-pki-types",
2352
"schannel",
2353
-
"security-framework 3.3.0",
2354
]
2355
2356
[[package]]
···
2365
2366
[[package]]
2367
name = "rustls-webpki"
2368
-
version = "0.103.4"
2369
source = "registry+https://github.com/rust-lang/crates.io-index"
2370
-
checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
2371
dependencies = [
2372
"ring",
2373
"rustls-pki-types",
···
2388
2389
[[package]]
2390
name = "schannel"
2391
-
version = "0.1.27"
2392
source = "registry+https://github.com/rust-lang/crates.io-index"
2393
-
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
2394
dependencies = [
2395
-
"windows-sys 0.59.0",
2396
]
2397
-
2398
-
[[package]]
2399
-
name = "scoped-tls"
2400
-
version = "1.0.1"
2401
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2402
-
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
2403
2404
[[package]]
2405
name = "scopeguard"
···
2437
2438
[[package]]
2439
name = "security-framework"
2440
-
version = "3.3.0"
2441
source = "registry+https://github.com/rust-lang/crates.io-index"
2442
-
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
2443
dependencies = [
2444
"bitflags",
2445
"core-foundation 0.10.1",
···
2450
2451
[[package]]
2452
name = "security-framework-sys"
2453
-
version = "2.14.0"
2454
source = "registry+https://github.com/rust-lang/crates.io-index"
2455
-
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
2456
dependencies = [
2457
"core-foundation-sys",
2458
"libc",
···
2460
2461
[[package]]
2462
name = "semver"
2463
-
version = "1.0.26"
2464
source = "registry+https://github.com/rust-lang/crates.io-index"
2465
-
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
2466
2467
[[package]]
2468
name = "serde"
2469
-
version = "1.0.219"
2470
source = "registry+https://github.com/rust-lang/crates.io-index"
2471
-
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
2472
dependencies = [
2473
"serde_derive",
2474
]
2475
2476
[[package]]
2477
name = "serde_bytes"
2478
-
version = "0.11.17"
2479
source = "registry+https://github.com/rust-lang/crates.io-index"
2480
-
checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
2481
dependencies = [
2482
"serde",
2483
]
2484
2485
[[package]]
2486
name = "serde_derive"
2487
-
version = "1.0.219"
2488
source = "registry+https://github.com/rust-lang/crates.io-index"
2489
-
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
2490
dependencies = [
2491
"proc-macro2",
2492
"quote",
···
2495
2496
[[package]]
2497
name = "serde_html_form"
2498
-
version = "0.2.7"
2499
source = "registry+https://github.com/rust-lang/crates.io-index"
2500
-
checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4"
2501
dependencies = [
2502
"form_urlencoded",
2503
"indexmap",
2504
"itoa",
2505
"ryu",
2506
-
"serde",
2507
]
2508
2509
[[package]]
2510
name = "serde_ipld_dagcbor"
2511
-
version = "0.6.3"
2512
source = "registry+https://github.com/rust-lang/crates.io-index"
2513
-
checksum = "99600723cf53fb000a66175555098db7e75217c415bdd9a16a65d52a19dcc4fc"
2514
dependencies = [
2515
"cbor4ii",
2516
"ipld-core",
···
2520
2521
[[package]]
2522
name = "serde_json"
2523
-
version = "1.0.142"
2524
source = "registry+https://github.com/rust-lang/crates.io-index"
2525
-
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
2526
dependencies = [
2527
"itoa",
2528
"memchr",
2529
"ryu",
2530
"serde",
2531
]
2532
2533
[[package]]
2534
name = "serde_path_to_error"
2535
-
version = "0.1.17"
2536
source = "registry+https://github.com/rust-lang/crates.io-index"
2537
-
checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
2538
dependencies = [
2539
"itoa",
2540
"serde",
2541
]
2542
2543
[[package]]
···
2574
]
2575
2576
[[package]]
2577
name = "sha2"
2578
version = "0.10.9"
2579
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2646
"chrono",
2647
"dotenvy",
2648
"futures-util",
2649
"regex",
2650
"reqwest",
2651
"reqwest-chain",
···
2674
"regex",
2675
"serde",
2676
"serde_json",
2677
-
"thiserror 2.0.14",
2678
"unicode-segmentation",
2679
]
2680
···
2756
"futures-intrusive",
2757
"futures-io",
2758
"futures-util",
2759
-
"hashbrown",
2760
"hashlink",
2761
"indexmap",
2762
"log",
···
2769
"serde_json",
2770
"sha2",
2771
"smallvec",
2772
-
"thiserror 2.0.14",
2773
"tokio",
2774
"tokio-stream",
2775
"tracing",
···
2854
"smallvec",
2855
"sqlx-core",
2856
"stringprep",
2857
-
"thiserror 2.0.14",
2858
"tracing",
2859
"uuid",
2860
"whoami",
···
2893
"smallvec",
2894
"sqlx-core",
2895
"stringprep",
2896
-
"thiserror 2.0.14",
2897
"tracing",
2898
"uuid",
2899
"whoami",
···
2919
"serde",
2920
"serde_urlencoded",
2921
"sqlx-core",
2922
-
"thiserror 2.0.14",
2923
"tracing",
2924
"url",
2925
"uuid",
···
3048
3049
[[package]]
3050
name = "tempfile"
3051
-
version = "3.20.0"
3052
source = "registry+https://github.com/rust-lang/crates.io-index"
3053
-
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
3054
dependencies = [
3055
"fastrand",
3056
"getrandom 0.3.3",
3057
"once_cell",
3058
"rustix",
3059
-
"windows-sys 0.59.0",
3060
]
3061
3062
[[package]]
···
3070
3071
[[package]]
3072
name = "thiserror"
3073
-
version = "2.0.14"
3074
source = "registry+https://github.com/rust-lang/crates.io-index"
3075
-
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
3076
dependencies = [
3077
-
"thiserror-impl 2.0.14",
3078
]
3079
3080
[[package]]
···
3090
3091
[[package]]
3092
name = "thiserror-impl"
3093
-
version = "2.0.14"
3094
source = "registry+https://github.com/rust-lang/crates.io-index"
3095
-
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
3096
dependencies = [
3097
"proc-macro2",
3098
"quote",
···
3120
3121
[[package]]
3122
name = "tinyvec"
3123
-
version = "1.9.0"
3124
source = "registry+https://github.com/rust-lang/crates.io-index"
3125
-
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
3126
dependencies = [
3127
"tinyvec_macros",
3128
]
···
3176
3177
[[package]]
3178
name = "tokio-rustls"
3179
-
version = "0.26.2"
3180
source = "registry+https://github.com/rust-lang/crates.io-index"
3181
-
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
3182
dependencies = [
3183
"rustls",
3184
"tokio",
···
3335
3336
[[package]]
3337
name = "tracing-subscriber"
3338
-
version = "0.3.19"
3339
source = "registry+https://github.com/rust-lang/crates.io-index"
3340
-
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
3341
dependencies = [
3342
"matchers",
3343
"nu-ansi-term",
3344
"once_cell",
3345
-
"regex",
3346
"sharded-slab",
3347
"smallvec",
3348
"thread_local",
···
3405
3406
[[package]]
3407
name = "unicode-ident"
3408
-
version = "1.0.18"
3409
source = "registry+https://github.com/rust-lang/crates.io-index"
3410
-
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
3411
3412
[[package]]
3413
name = "unicode-normalization"
···
3444
3445
[[package]]
3446
name = "url"
3447
-
version = "2.5.4"
3448
source = "registry+https://github.com/rust-lang/crates.io-index"
3449
-
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
3450
dependencies = [
3451
"form_urlencoded",
3452
"idna",
3453
"percent-encoding",
3454
]
3455
3456
[[package]]
···
3473
3474
[[package]]
3475
name = "uuid"
3476
-
version = "1.18.0"
3477
source = "registry+https://github.com/rust-lang/crates.io-index"
3478
-
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
3479
dependencies = [
3480
"getrandom 0.3.3",
3481
"js-sys",
···
3518
3519
[[package]]
3520
name = "wasi"
3521
-
version = "0.14.2+wasi-0.2.4"
3522
source = "registry+https://github.com/rust-lang/crates.io-index"
3523
-
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
3524
dependencies = [
3525
-
"wit-bindgen-rt",
3526
]
3527
3528
[[package]]
···
3533
3534
[[package]]
3535
name = "wasm-bindgen"
3536
-
version = "0.2.100"
3537
source = "registry+https://github.com/rust-lang/crates.io-index"
3538
-
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
3539
dependencies = [
3540
"cfg-if",
3541
"once_cell",
3542
"rustversion",
3543
"wasm-bindgen-macro",
3544
]
3545
3546
[[package]]
3547
name = "wasm-bindgen-backend"
3548
-
version = "0.2.100"
3549
source = "registry+https://github.com/rust-lang/crates.io-index"
3550
-
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
3551
dependencies = [
3552
"bumpalo",
3553
"log",
···
3559
3560
[[package]]
3561
name = "wasm-bindgen-futures"
3562
-
version = "0.4.50"
3563
source = "registry+https://github.com/rust-lang/crates.io-index"
3564
-
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
3565
dependencies = [
3566
"cfg-if",
3567
"js-sys",
···
3572
3573
[[package]]
3574
name = "wasm-bindgen-macro"
3575
-
version = "0.2.100"
3576
source = "registry+https://github.com/rust-lang/crates.io-index"
3577
-
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
3578
dependencies = [
3579
"quote",
3580
"wasm-bindgen-macro-support",
···
3582
3583
[[package]]
3584
name = "wasm-bindgen-macro-support"
3585
-
version = "0.2.100"
3586
source = "registry+https://github.com/rust-lang/crates.io-index"
3587
-
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
3588
dependencies = [
3589
"proc-macro2",
3590
"quote",
···
3595
3596
[[package]]
3597
name = "wasm-bindgen-shared"
3598
-
version = "0.2.100"
3599
source = "registry+https://github.com/rust-lang/crates.io-index"
3600
-
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
3601
dependencies = [
3602
"unicode-ident",
3603
]
···
3617
3618
[[package]]
3619
name = "web-sys"
3620
-
version = "0.3.77"
3621
source = "registry+https://github.com/rust-lang/crates.io-index"
3622
-
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
3623
dependencies = [
3624
"js-sys",
3625
"wasm-bindgen",
···
3670
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
3671
3672
[[package]]
3673
-
name = "winapi"
3674
-
version = "0.3.9"
3675
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3676
-
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
3677
-
dependencies = [
3678
-
"winapi-i686-pc-windows-gnu",
3679
-
"winapi-x86_64-pc-windows-gnu",
3680
-
]
3681
-
3682
-
[[package]]
3683
-
name = "winapi-i686-pc-windows-gnu"
3684
-
version = "0.4.0"
3685
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3686
-
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
3687
-
3688
-
[[package]]
3689
-
name = "winapi-x86_64-pc-windows-gnu"
3690
-
version = "0.4.0"
3691
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3692
-
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
3693
-
3694
-
[[package]]
3695
-
name = "windows"
3696
-
version = "0.61.3"
3697
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3698
-
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
3699
-
dependencies = [
3700
-
"windows-collections",
3701
-
"windows-core",
3702
-
"windows-future",
3703
-
"windows-link",
3704
-
"windows-numerics",
3705
-
]
3706
-
3707
-
[[package]]
3708
-
name = "windows-collections"
3709
-
version = "0.2.0"
3710
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3711
-
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
3712
-
dependencies = [
3713
-
"windows-core",
3714
-
]
3715
-
3716
-
[[package]]
3717
name = "windows-core"
3718
-
version = "0.61.2"
3719
source = "registry+https://github.com/rust-lang/crates.io-index"
3720
-
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
3721
dependencies = [
3722
"windows-implement",
3723
"windows-interface",
3724
-
"windows-link",
3725
-
"windows-result",
3726
-
"windows-strings",
3727
-
]
3728
-
3729
-
[[package]]
3730
-
name = "windows-future"
3731
-
version = "0.2.1"
3732
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3733
-
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
3734
-
dependencies = [
3735
-
"windows-core",
3736
-
"windows-link",
3737
-
"windows-threading",
3738
]
3739
3740
[[package]]
3741
name = "windows-implement"
3742
-
version = "0.60.0"
3743
source = "registry+https://github.com/rust-lang/crates.io-index"
3744
-
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
3745
dependencies = [
3746
"proc-macro2",
3747
"quote",
···
3750
3751
[[package]]
3752
name = "windows-interface"
3753
-
version = "0.59.1"
3754
source = "registry+https://github.com/rust-lang/crates.io-index"
3755
-
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
3756
dependencies = [
3757
"proc-macro2",
3758
"quote",
···
3766
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
3767
3768
[[package]]
3769
-
name = "windows-numerics"
3770
version = "0.2.0"
3771
source = "registry+https://github.com/rust-lang/crates.io-index"
3772
-
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
3773
-
dependencies = [
3774
-
"windows-core",
3775
-
"windows-link",
3776
-
]
3777
3778
[[package]]
3779
name = "windows-registry"
···
3781
source = "registry+https://github.com/rust-lang/crates.io-index"
3782
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
3783
dependencies = [
3784
-
"windows-link",
3785
-
"windows-result",
3786
-
"windows-strings",
3787
]
3788
3789
[[package]]
···
3792
source = "registry+https://github.com/rust-lang/crates.io-index"
3793
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
3794
dependencies = [
3795
-
"windows-link",
3796
]
3797
3798
[[package]]
···
3801
source = "registry+https://github.com/rust-lang/crates.io-index"
3802
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
3803
dependencies = [
3804
-
"windows-link",
3805
]
3806
3807
[[package]]
···
3837
source = "registry+https://github.com/rust-lang/crates.io-index"
3838
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
3839
dependencies = [
3840
-
"windows-targets 0.53.3",
3841
]
3842
3843
[[package]]
···
3873
3874
[[package]]
3875
name = "windows-targets"
3876
-
version = "0.53.3"
3877
source = "registry+https://github.com/rust-lang/crates.io-index"
3878
-
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
3879
dependencies = [
3880
-
"windows-link",
3881
"windows_aarch64_gnullvm 0.53.0",
3882
"windows_aarch64_msvc 0.53.0",
3883
"windows_i686_gnu 0.53.0",
···
3886
"windows_x86_64_gnu 0.53.0",
3887
"windows_x86_64_gnullvm 0.53.0",
3888
"windows_x86_64_msvc 0.53.0",
3889
-
]
3890
-
3891
-
[[package]]
3892
-
name = "windows-threading"
3893
-
version = "0.1.0"
3894
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3895
-
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
3896
-
dependencies = [
3897
-
"windows-link",
3898
]
3899
3900
[[package]]
···
4046
]
4047
4048
[[package]]
4049
-
name = "wit-bindgen-rt"
4050
-
version = "0.39.0"
4051
source = "registry+https://github.com/rust-lang/crates.io-index"
4052
-
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
4053
-
dependencies = [
4054
-
"bitflags",
4055
-
]
4056
4057
[[package]]
4058
name = "writeable"
···
4086
4087
[[package]]
4088
name = "zerocopy"
4089
-
version = "0.8.26"
4090
source = "registry+https://github.com/rust-lang/crates.io-index"
4091
-
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
4092
dependencies = [
4093
"zerocopy-derive",
4094
]
4095
4096
[[package]]
4097
name = "zerocopy-derive"
4098
-
version = "0.8.26"
4099
source = "registry+https://github.com/rust-lang/crates.io-index"
4100
-
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
4101
dependencies = [
4102
"proc-macro2",
4103
"quote",
···
4184
4185
[[package]]
4186
name = "zstd-sys"
4187
-
version = "2.0.15+zstd.1.5.7"
4188
source = "registry+https://github.com/rust-lang/crates.io-index"
4189
-
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
4190
dependencies = [
4191
"cc",
4192
"pkg-config",
···
4
5
[[package]]
6
name = "addr2line"
7
+
version = "0.25.1"
8
source = "registry+https://github.com/rust-lang/crates.io-index"
9
+
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
10
dependencies = [
11
"gimli",
12
]
···
33
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
34
35
[[package]]
36
name = "android_system_properties"
37
version = "0.1.5"
38
source = "registry+https://github.com/rust-lang/crates.io-index"
···
43
44
[[package]]
45
name = "anyhow"
46
+
version = "1.0.100"
47
source = "registry+https://github.com/rust-lang/crates.io-index"
48
+
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
49
50
[[package]]
51
name = "anymap2"
52
version = "0.13.0"
53
source = "registry+https://github.com/rust-lang/crates.io-index"
54
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
55
+
56
+
[[package]]
57
+
name = "arc-swap"
58
+
version = "1.7.1"
59
+
source = "registry+https://github.com/rust-lang/crates.io-index"
60
+
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
61
62
[[package]]
63
name = "async-trait"
···
87
88
[[package]]
89
name = "atproto-client"
90
+
version = "0.12.0"
91
source = "registry+https://github.com/rust-lang/crates.io-index"
92
+
checksum = "9f388d83aa9552d7c7b80cc131558fb86be5a46a104742fb4ccf38e22a3c8620"
93
dependencies = [
94
"anyhow",
95
"atproto-identity",
···
101
"reqwest-middleware",
102
"serde",
103
"serde_json",
104
+
"thiserror 2.0.16",
105
"tokio",
106
"tracing",
107
"urlencoding",
···
109
110
[[package]]
111
name = "atproto-identity"
112
+
version = "0.12.0"
113
source = "registry+https://github.com/rust-lang/crates.io-index"
114
+
checksum = "65e405e13a96ce91d1e832f56b90ae3f1fcbe29a5d9731e417074bfe1df71a5f"
115
dependencies = [
116
"anyhow",
117
"async-trait",
···
128
"serde",
129
"serde_ipld_dagcbor",
130
"serde_json",
131
+
"thiserror 2.0.16",
132
"tokio",
133
"tracing",
134
"urlencoding",
···
136
137
[[package]]
138
name = "atproto-jetstream"
139
+
version = "0.12.0"
140
source = "registry+https://github.com/rust-lang/crates.io-index"
141
+
checksum = "b36d0d4fec207d04563bdb151ca793dbfbb9c4708b5c9320d65da4b564b186d2"
142
dependencies = [
143
"anyhow",
144
"async-trait",
···
147
"http",
148
"serde",
149
"serde_json",
150
+
"thiserror 2.0.16",
151
"tokio",
152
"tokio-util",
153
"tokio-websockets",
···
159
160
[[package]]
161
name = "atproto-oauth"
162
+
version = "0.12.0"
163
source = "registry+https://github.com/rust-lang/crates.io-index"
164
+
checksum = "3f7a82388b59f83c2c141af434afa96d41841e4e7c1b361afc83193f2ee75d63"
165
dependencies = [
166
"anyhow",
167
"async-trait",
···
183
"serde_ipld_dagcbor",
184
"serde_json",
185
"sha2",
186
+
"thiserror 2.0.16",
187
"tokio",
188
"tracing",
189
"ulid",
···
191
192
[[package]]
193
name = "atproto-record"
194
+
version = "0.12.0"
195
source = "registry+https://github.com/rust-lang/crates.io-index"
196
+
checksum = "accec4d63f3f653e947b051b8c9f56f6d939d7a646e44b845eeabc03c713dad5"
197
dependencies = [
198
"anyhow",
199
"atproto-identity",
···
202
"serde",
203
"serde_ipld_dagcbor",
204
"serde_json",
205
+
"thiserror 2.0.16",
206
]
207
208
[[package]]
···
306
]
307
308
[[package]]
309
+
name = "backon"
310
+
version = "1.5.2"
311
+
source = "registry+https://github.com/rust-lang/crates.io-index"
312
+
checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d"
313
+
dependencies = [
314
+
"fastrand",
315
+
]
316
+
317
+
[[package]]
318
name = "backtrace"
319
+
version = "0.3.76"
320
source = "registry+https://github.com/rust-lang/crates.io-index"
321
+
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
322
dependencies = [
323
"addr2line",
324
"cfg-if",
···
326
"miniz_oxide",
327
"object",
328
"rustc-demangle",
329
+
"windows-link 0.2.0",
330
]
331
332
[[package]]
···
361
362
[[package]]
363
name = "bitflags"
364
+
version = "2.9.4"
365
source = "registry+https://github.com/rust-lang/crates.io-index"
366
+
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
367
dependencies = [
368
"serde",
369
]
···
406
407
[[package]]
408
name = "cc"
409
+
version = "1.2.39"
410
source = "registry+https://github.com/rust-lang/crates.io-index"
411
+
checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f"
412
dependencies = [
413
+
"find-msvc-tools",
414
"jobserver",
415
"libc",
416
"shlex",
···
418
419
[[package]]
420
name = "cfg-if"
421
+
version = "1.0.3"
422
source = "registry+https://github.com/rust-lang/crates.io-index"
423
+
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
424
425
[[package]]
426
name = "cfg_aliases"
···
430
431
[[package]]
432
name = "chrono"
433
+
version = "0.4.42"
434
source = "registry+https://github.com/rust-lang/crates.io-index"
435
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
436
dependencies = [
437
"iana-time-zone",
438
"js-sys",
439
"num-traits",
440
"serde",
441
"wasm-bindgen",
442
+
"windows-link 0.2.0",
443
]
444
445
[[package]]
···
457
]
458
459
[[package]]
460
+
name = "combine"
461
+
version = "4.6.7"
462
+
source = "registry+https://github.com/rust-lang/crates.io-index"
463
+
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
464
+
dependencies = [
465
+
"bytes",
466
+
"futures-core",
467
+
"memchr",
468
+
"pin-project-lite",
469
+
"tokio",
470
+
"tokio-util",
471
+
]
472
+
473
+
[[package]]
474
name = "concurrent-queue"
475
version = "2.5.0"
476
source = "registry+https://github.com/rust-lang/crates.io-index"
···
748
749
[[package]]
750
name = "errno"
751
+
version = "0.3.14"
752
source = "registry+https://github.com/rust-lang/crates.io-index"
753
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
754
dependencies = [
755
"libc",
756
+
"windows-sys 0.61.1",
757
]
758
759
[[package]]
···
795
]
796
797
[[package]]
798
+
name = "find-msvc-tools"
799
+
version = "0.1.2"
800
+
source = "registry+https://github.com/rust-lang/crates.io-index"
801
+
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
802
+
803
+
[[package]]
804
name = "flume"
805
version = "0.11.1"
806
source = "registry+https://github.com/rust-lang/crates.io-index"
···
840
841
[[package]]
842
name = "form_urlencoded"
843
+
version = "1.2.2"
844
source = "registry+https://github.com/rust-lang/crates.io-index"
845
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
846
dependencies = [
847
"percent-encoding",
848
]
···
948
]
949
950
[[package]]
951
name = "generic-array"
952
version = "0.14.7"
953
source = "registry+https://github.com/rust-lang/crates.io-index"
···
981
"js-sys",
982
"libc",
983
"r-efi",
984
+
"wasi 0.14.7+wasi-0.2.4",
985
"wasm-bindgen",
986
]
987
988
[[package]]
989
name = "gimli"
990
+
version = "0.32.3"
991
source = "registry+https://github.com/rust-lang/crates.io-index"
992
+
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
993
994
[[package]]
995
name = "group"
···
1033
]
1034
1035
[[package]]
1036
+
name = "hashbrown"
1037
+
version = "0.16.0"
1038
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1039
+
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
1040
+
1041
+
[[package]]
1042
name = "hashlink"
1043
version = "0.10.0"
1044
source = "registry+https://github.com/rust-lang/crates.io-index"
1045
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
1046
dependencies = [
1047
+
"hashbrown 0.15.5",
1048
]
1049
1050
[[package]]
···
1077
"once_cell",
1078
"rand 0.9.2",
1079
"ring",
1080
+
"thiserror 2.0.16",
1081
"tinyvec",
1082
"tokio",
1083
"tracing",
···
1100
"rand 0.9.2",
1101
"resolv-conf",
1102
"smallvec",
1103
+
"thiserror 2.0.16",
1104
"tokio",
1105
"tracing",
1106
]
···
1180
1181
[[package]]
1182
name = "hyper"
1183
+
version = "1.7.0"
1184
source = "registry+https://github.com/rust-lang/crates.io-index"
1185
+
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
1186
dependencies = [
1187
+
"atomic-waker",
1188
"bytes",
1189
"futures-channel",
1190
+
"futures-core",
1191
"h2",
1192
"http",
1193
"http-body",
···
1195
"httpdate",
1196
"itoa",
1197
"pin-project-lite",
1198
+
"pin-utils",
1199
"smallvec",
1200
"tokio",
1201
"want",
···
1236
1237
[[package]]
1238
name = "hyper-util"
1239
+
version = "0.1.17"
1240
source = "registry+https://github.com/rust-lang/crates.io-index"
1241
+
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
1242
dependencies = [
1243
"base64 0.22.1",
1244
"bytes",
···
1262
1263
[[package]]
1264
name = "iana-time-zone"
1265
+
version = "0.1.64"
1266
source = "registry+https://github.com/rust-lang/crates.io-index"
1267
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
1268
dependencies = [
1269
"android_system_properties",
1270
"core-foundation-sys",
···
1372
1373
[[package]]
1374
name = "idna"
1375
+
version = "1.1.0"
1376
source = "registry+https://github.com/rust-lang/crates.io-index"
1377
+
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
1378
dependencies = [
1379
"idna_adapter",
1380
"smallvec",
···
1393
1394
[[package]]
1395
name = "indexmap"
1396
+
version = "2.11.4"
1397
source = "registry+https://github.com/rust-lang/crates.io-index"
1398
+
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
1399
dependencies = [
1400
"equivalent",
1401
+
"hashbrown 0.16.0",
1402
]
1403
1404
[[package]]
1405
name = "io-uring"
1406
+
version = "0.7.10"
1407
source = "registry+https://github.com/rust-lang/crates.io-index"
1408
+
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
1409
dependencies = [
1410
"bitflags",
1411
"cfg-if",
···
1469
1470
[[package]]
1471
name = "js-sys"
1472
+
version = "0.3.81"
1473
source = "registry+https://github.com/rust-lang/crates.io-index"
1474
+
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
1475
dependencies = [
1476
"once_cell",
1477
"wasm-bindgen",
···
1502
1503
[[package]]
1504
name = "libc"
1505
+
version = "0.2.176"
1506
source = "registry+https://github.com/rust-lang/crates.io-index"
1507
+
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
1508
1509
[[package]]
1510
name = "libm"
···
1514
1515
[[package]]
1516
name = "libredox"
1517
+
version = "0.1.10"
1518
source = "registry+https://github.com/rust-lang/crates.io-index"
1519
+
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
1520
dependencies = [
1521
"bitflags",
1522
"libc",
···
1535
1536
[[package]]
1537
name = "linux-raw-sys"
1538
+
version = "0.11.0"
1539
source = "registry+https://github.com/rust-lang/crates.io-index"
1540
+
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
1541
1542
[[package]]
1543
name = "litemap"
···
1557
1558
[[package]]
1559
name = "log"
1560
+
version = "0.4.28"
1561
source = "registry+https://github.com/rust-lang/crates.io-index"
1562
+
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
1563
1564
[[package]]
1565
name = "lru"
···
1567
source = "registry+https://github.com/rust-lang/crates.io-index"
1568
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
1569
dependencies = [
1570
+
"hashbrown 0.15.5",
1571
]
1572
1573
[[package]]
···
1578
1579
[[package]]
1580
name = "matchers"
1581
+
version = "0.2.0"
1582
source = "registry+https://github.com/rust-lang/crates.io-index"
1583
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
1584
dependencies = [
1585
+
"regex-automata",
1586
]
1587
1588
[[package]]
···
1603
1604
[[package]]
1605
name = "memchr"
1606
+
version = "2.7.6"
1607
source = "registry+https://github.com/rust-lang/crates.io-index"
1608
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
1609
1610
[[package]]
1611
name = "mime"
···
1645
1646
[[package]]
1647
name = "moka"
1648
+
version = "0.12.11"
1649
source = "registry+https://github.com/rust-lang/crates.io-index"
1650
+
checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077"
1651
dependencies = [
1652
"crossbeam-channel",
1653
"crossbeam-epoch",
1654
"crossbeam-utils",
1655
+
"equivalent",
1656
"parking_lot",
1657
"portable-atomic",
1658
"rustc_version",
1659
"smallvec",
1660
"tagptr",
1661
"uuid",
1662
]
1663
···
1719
1720
[[package]]
1721
name = "nu-ansi-term"
1722
+
version = "0.50.1"
1723
source = "registry+https://github.com/rust-lang/crates.io-index"
1724
+
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
1725
+
dependencies = [
1726
+
"windows-sys 0.52.0",
1727
+
]
1728
+
1729
+
[[package]]
1730
+
name = "num-bigint"
1731
+
version = "0.4.6"
1732
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1733
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
1734
dependencies = [
1735
+
"num-integer",
1736
+
"num-traits",
1737
]
1738
1739
[[package]]
···
1785
1786
[[package]]
1787
name = "object"
1788
+
version = "0.37.3"
1789
source = "registry+https://github.com/rust-lang/crates.io-index"
1790
+
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
1791
dependencies = [
1792
"memchr",
1793
]
···
1847
]
1848
1849
[[package]]
1850
name = "p256"
1851
version = "0.13.2"
1852
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1912
1913
[[package]]
1914
name = "percent-encoding"
1915
+
version = "2.3.2"
1916
source = "registry+https://github.com/rust-lang/crates.io-index"
1917
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1918
1919
[[package]]
1920
name = "pin-project-lite"
···
1963
1964
[[package]]
1965
name = "potential_utf"
1966
+
version = "0.1.3"
1967
source = "registry+https://github.com/rust-lang/crates.io-index"
1968
+
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
1969
dependencies = [
1970
"zerovec",
1971
]
···
1991
1992
[[package]]
1993
name = "proc-macro2"
1994
+
version = "1.0.101"
1995
source = "registry+https://github.com/rust-lang/crates.io-index"
1996
+
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
1997
dependencies = [
1998
"unicode-ident",
1999
]
2000
2001
[[package]]
2002
name = "quinn"
2003
+
version = "0.11.9"
2004
source = "registry+https://github.com/rust-lang/crates.io-index"
2005
+
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
2006
dependencies = [
2007
"bytes",
2008
"cfg_aliases",
···
2011
"quinn-udp",
2012
"rustc-hash",
2013
"rustls",
2014
+
"socket2 0.6.0",
2015
+
"thiserror 2.0.16",
2016
"tokio",
2017
"tracing",
2018
"web-time",
···
2020
2021
[[package]]
2022
name = "quinn-proto"
2023
+
version = "0.11.13"
2024
source = "registry+https://github.com/rust-lang/crates.io-index"
2025
+
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
2026
dependencies = [
2027
"bytes",
2028
"getrandom 0.3.3",
···
2033
"rustls",
2034
"rustls-pki-types",
2035
"slab",
2036
+
"thiserror 2.0.16",
2037
"tinyvec",
2038
"tracing",
2039
"web-time",
···
2041
2042
[[package]]
2043
name = "quinn-udp"
2044
+
version = "0.5.14"
2045
source = "registry+https://github.com/rust-lang/crates.io-index"
2046
+
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
2047
dependencies = [
2048
"cfg_aliases",
2049
"libc",
2050
"once_cell",
2051
+
"socket2 0.6.0",
2052
"tracing",
2053
+
"windows-sys 0.60.2",
2054
]
2055
2056
[[package]]
···
2128
]
2129
2130
[[package]]
2131
+
name = "redis"
2132
+
version = "0.32.6"
2133
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2134
+
checksum = "15965fbccb975c38a08a68beca6bdb57da9081cd0859417c5975a160d968c3cb"
2135
+
dependencies = [
2136
+
"arc-swap",
2137
+
"backon",
2138
+
"bytes",
2139
+
"cfg-if",
2140
+
"combine",
2141
+
"futures-channel",
2142
+
"futures-util",
2143
+
"itoa",
2144
+
"num-bigint",
2145
+
"percent-encoding",
2146
+
"pin-project-lite",
2147
+
"ryu",
2148
+
"sha1_smol",
2149
+
"socket2 0.6.0",
2150
+
"tokio",
2151
+
"tokio-util",
2152
+
"url",
2153
+
]
2154
+
2155
+
[[package]]
2156
name = "redox_syscall"
2157
version = "0.5.17"
2158
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2163
2164
[[package]]
2165
name = "regex"
2166
+
version = "1.11.3"
2167
source = "registry+https://github.com/rust-lang/crates.io-index"
2168
+
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
2169
dependencies = [
2170
"aho-corasick",
2171
"memchr",
2172
+
"regex-automata",
2173
+
"regex-syntax",
2174
]
2175
2176
[[package]]
2177
name = "regex-automata"
2178
+
version = "0.4.11"
2179
source = "registry+https://github.com/rust-lang/crates.io-index"
2180
+
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
2181
dependencies = [
2182
"aho-corasick",
2183
"memchr",
2184
+
"regex-syntax",
2185
]
2186
2187
[[package]]
2188
name = "regex-syntax"
2189
+
version = "0.8.6"
2190
source = "registry+https://github.com/rust-lang/crates.io-index"
2191
+
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
2192
2193
[[package]]
2194
name = "reqwest"
···
2267
2268
[[package]]
2269
name = "resolv-conf"
2270
+
version = "0.7.5"
2271
source = "registry+https://github.com/rust-lang/crates.io-index"
2272
+
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
2273
2274
[[package]]
2275
name = "rfc6979"
···
2338
2339
[[package]]
2340
name = "rustix"
2341
+
version = "1.1.2"
2342
source = "registry+https://github.com/rust-lang/crates.io-index"
2343
+
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
2344
dependencies = [
2345
"bitflags",
2346
"errno",
2347
"libc",
2348
"linux-raw-sys",
2349
+
"windows-sys 0.61.1",
2350
]
2351
2352
[[package]]
2353
name = "rustls"
2354
+
version = "0.23.32"
2355
source = "registry+https://github.com/rust-lang/crates.io-index"
2356
+
checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
2357
dependencies = [
2358
"once_cell",
2359
"ring",
···
2372
"openssl-probe",
2373
"rustls-pki-types",
2374
"schannel",
2375
+
"security-framework 3.5.0",
2376
]
2377
2378
[[package]]
···
2387
2388
[[package]]
2389
name = "rustls-webpki"
2390
+
version = "0.103.6"
2391
source = "registry+https://github.com/rust-lang/crates.io-index"
2392
+
checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
2393
dependencies = [
2394
"ring",
2395
"rustls-pki-types",
···
2410
2411
[[package]]
2412
name = "schannel"
2413
+
version = "0.1.28"
2414
source = "registry+https://github.com/rust-lang/crates.io-index"
2415
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
2416
dependencies = [
2417
+
"windows-sys 0.61.1",
2418
]
2419
2420
[[package]]
2421
name = "scopeguard"
···
2453
2454
[[package]]
2455
name = "security-framework"
2456
+
version = "3.5.0"
2457
source = "registry+https://github.com/rust-lang/crates.io-index"
2458
+
checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a"
2459
dependencies = [
2460
"bitflags",
2461
"core-foundation 0.10.1",
···
2466
2467
[[package]]
2468
name = "security-framework-sys"
2469
+
version = "2.15.0"
2470
source = "registry+https://github.com/rust-lang/crates.io-index"
2471
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
2472
dependencies = [
2473
"core-foundation-sys",
2474
"libc",
···
2476
2477
[[package]]
2478
name = "semver"
2479
+
version = "1.0.27"
2480
source = "registry+https://github.com/rust-lang/crates.io-index"
2481
+
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
2482
2483
[[package]]
2484
name = "serde"
2485
+
version = "1.0.227"
2486
source = "registry+https://github.com/rust-lang/crates.io-index"
2487
+
checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245"
2488
dependencies = [
2489
+
"serde_core",
2490
"serde_derive",
2491
]
2492
2493
[[package]]
2494
name = "serde_bytes"
2495
+
version = "0.11.19"
2496
source = "registry+https://github.com/rust-lang/crates.io-index"
2497
+
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
2498
dependencies = [
2499
"serde",
2500
+
"serde_core",
2501
+
]
2502
+
2503
+
[[package]]
2504
+
name = "serde_core"
2505
+
version = "1.0.227"
2506
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2507
+
checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5"
2508
+
dependencies = [
2509
+
"serde_derive",
2510
]
2511
2512
[[package]]
2513
name = "serde_derive"
2514
+
version = "1.0.227"
2515
source = "registry+https://github.com/rust-lang/crates.io-index"
2516
+
checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04"
2517
dependencies = [
2518
"proc-macro2",
2519
"quote",
···
2522
2523
[[package]]
2524
name = "serde_html_form"
2525
+
version = "0.2.8"
2526
source = "registry+https://github.com/rust-lang/crates.io-index"
2527
+
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
2528
dependencies = [
2529
"form_urlencoded",
2530
"indexmap",
2531
"itoa",
2532
"ryu",
2533
+
"serde_core",
2534
]
2535
2536
[[package]]
2537
name = "serde_ipld_dagcbor"
2538
+
version = "0.6.4"
2539
source = "registry+https://github.com/rust-lang/crates.io-index"
2540
+
checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778"
2541
dependencies = [
2542
"cbor4ii",
2543
"ipld-core",
···
2547
2548
[[package]]
2549
name = "serde_json"
2550
+
version = "1.0.145"
2551
source = "registry+https://github.com/rust-lang/crates.io-index"
2552
+
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
2553
dependencies = [
2554
"itoa",
2555
"memchr",
2556
"ryu",
2557
"serde",
2558
+
"serde_core",
2559
]
2560
2561
[[package]]
2562
name = "serde_path_to_error"
2563
+
version = "0.1.20"
2564
source = "registry+https://github.com/rust-lang/crates.io-index"
2565
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
2566
dependencies = [
2567
"itoa",
2568
"serde",
2569
+
"serde_core",
2570
]
2571
2572
[[package]]
···
2603
]
2604
2605
[[package]]
2606
+
name = "sha1_smol"
2607
+
version = "1.0.1"
2608
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2609
+
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
2610
+
2611
+
[[package]]
2612
name = "sha2"
2613
version = "0.10.9"
2614
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2681
"chrono",
2682
"dotenvy",
2683
"futures-util",
2684
+
"redis",
2685
"regex",
2686
"reqwest",
2687
"reqwest-chain",
···
2710
"regex",
2711
"serde",
2712
"serde_json",
2713
+
"thiserror 2.0.16",
2714
"unicode-segmentation",
2715
]
2716
···
2792
"futures-intrusive",
2793
"futures-io",
2794
"futures-util",
2795
+
"hashbrown 0.15.5",
2796
"hashlink",
2797
"indexmap",
2798
"log",
···
2805
"serde_json",
2806
"sha2",
2807
"smallvec",
2808
+
"thiserror 2.0.16",
2809
"tokio",
2810
"tokio-stream",
2811
"tracing",
···
2890
"smallvec",
2891
"sqlx-core",
2892
"stringprep",
2893
+
"thiserror 2.0.16",
2894
"tracing",
2895
"uuid",
2896
"whoami",
···
2929
"smallvec",
2930
"sqlx-core",
2931
"stringprep",
2932
+
"thiserror 2.0.16",
2933
"tracing",
2934
"uuid",
2935
"whoami",
···
2955
"serde",
2956
"serde_urlencoded",
2957
"sqlx-core",
2958
+
"thiserror 2.0.16",
2959
"tracing",
2960
"url",
2961
"uuid",
···
3084
3085
[[package]]
3086
name = "tempfile"
3087
+
version = "3.23.0"
3088
source = "registry+https://github.com/rust-lang/crates.io-index"
3089
+
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
3090
dependencies = [
3091
"fastrand",
3092
"getrandom 0.3.3",
3093
"once_cell",
3094
"rustix",
3095
+
"windows-sys 0.61.1",
3096
]
3097
3098
[[package]]
···
3106
3107
[[package]]
3108
name = "thiserror"
3109
+
version = "2.0.16"
3110
source = "registry+https://github.com/rust-lang/crates.io-index"
3111
+
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
3112
dependencies = [
3113
+
"thiserror-impl 2.0.16",
3114
]
3115
3116
[[package]]
···
3126
3127
[[package]]
3128
name = "thiserror-impl"
3129
+
version = "2.0.16"
3130
source = "registry+https://github.com/rust-lang/crates.io-index"
3131
+
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
3132
dependencies = [
3133
"proc-macro2",
3134
"quote",
···
3156
3157
[[package]]
3158
name = "tinyvec"
3159
+
version = "1.10.0"
3160
source = "registry+https://github.com/rust-lang/crates.io-index"
3161
+
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
3162
dependencies = [
3163
"tinyvec_macros",
3164
]
···
3212
3213
[[package]]
3214
name = "tokio-rustls"
3215
+
version = "0.26.4"
3216
source = "registry+https://github.com/rust-lang/crates.io-index"
3217
+
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
3218
dependencies = [
3219
"rustls",
3220
"tokio",
···
3371
3372
[[package]]
3373
name = "tracing-subscriber"
3374
+
version = "0.3.20"
3375
source = "registry+https://github.com/rust-lang/crates.io-index"
3376
+
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
3377
dependencies = [
3378
"matchers",
3379
"nu-ansi-term",
3380
"once_cell",
3381
+
"regex-automata",
3382
"sharded-slab",
3383
"smallvec",
3384
"thread_local",
···
3441
3442
[[package]]
3443
name = "unicode-ident"
3444
+
version = "1.0.19"
3445
source = "registry+https://github.com/rust-lang/crates.io-index"
3446
+
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
3447
3448
[[package]]
3449
name = "unicode-normalization"
···
3480
3481
[[package]]
3482
name = "url"
3483
+
version = "2.5.7"
3484
source = "registry+https://github.com/rust-lang/crates.io-index"
3485
+
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
3486
dependencies = [
3487
"form_urlencoded",
3488
"idna",
3489
"percent-encoding",
3490
+
"serde",
3491
]
3492
3493
[[package]]
···
3510
3511
[[package]]
3512
name = "uuid"
3513
+
version = "1.18.1"
3514
source = "registry+https://github.com/rust-lang/crates.io-index"
3515
+
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
3516
dependencies = [
3517
"getrandom 0.3.3",
3518
"js-sys",
···
3555
3556
[[package]]
3557
name = "wasi"
3558
+
version = "0.14.7+wasi-0.2.4"
3559
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3560
+
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
3561
+
dependencies = [
3562
+
"wasip2",
3563
+
]
3564
+
3565
+
[[package]]
3566
+
name = "wasip2"
3567
+
version = "1.0.1+wasi-0.2.4"
3568
source = "registry+https://github.com/rust-lang/crates.io-index"
3569
+
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
3570
dependencies = [
3571
+
"wit-bindgen",
3572
]
3573
3574
[[package]]
···
3579
3580
[[package]]
3581
name = "wasm-bindgen"
3582
+
version = "0.2.104"
3583
source = "registry+https://github.com/rust-lang/crates.io-index"
3584
+
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
3585
dependencies = [
3586
"cfg-if",
3587
"once_cell",
3588
"rustversion",
3589
"wasm-bindgen-macro",
3590
+
"wasm-bindgen-shared",
3591
]
3592
3593
[[package]]
3594
name = "wasm-bindgen-backend"
3595
+
version = "0.2.104"
3596
source = "registry+https://github.com/rust-lang/crates.io-index"
3597
+
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
3598
dependencies = [
3599
"bumpalo",
3600
"log",
···
3606
3607
[[package]]
3608
name = "wasm-bindgen-futures"
3609
+
version = "0.4.54"
3610
source = "registry+https://github.com/rust-lang/crates.io-index"
3611
+
checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
3612
dependencies = [
3613
"cfg-if",
3614
"js-sys",
···
3619
3620
[[package]]
3621
name = "wasm-bindgen-macro"
3622
+
version = "0.2.104"
3623
source = "registry+https://github.com/rust-lang/crates.io-index"
3624
+
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
3625
dependencies = [
3626
"quote",
3627
"wasm-bindgen-macro-support",
···
3629
3630
[[package]]
3631
name = "wasm-bindgen-macro-support"
3632
+
version = "0.2.104"
3633
source = "registry+https://github.com/rust-lang/crates.io-index"
3634
+
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
3635
dependencies = [
3636
"proc-macro2",
3637
"quote",
···
3642
3643
[[package]]
3644
name = "wasm-bindgen-shared"
3645
+
version = "0.2.104"
3646
source = "registry+https://github.com/rust-lang/crates.io-index"
3647
+
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
3648
dependencies = [
3649
"unicode-ident",
3650
]
···
3664
3665
[[package]]
3666
name = "web-sys"
3667
+
version = "0.3.81"
3668
source = "registry+https://github.com/rust-lang/crates.io-index"
3669
+
checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
3670
dependencies = [
3671
"js-sys",
3672
"wasm-bindgen",
···
3717
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
3718
3719
[[package]]
3720
name = "windows-core"
3721
+
version = "0.62.1"
3722
source = "registry+https://github.com/rust-lang/crates.io-index"
3723
+
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
3724
dependencies = [
3725
"windows-implement",
3726
"windows-interface",
3727
+
"windows-link 0.2.0",
3728
+
"windows-result 0.4.0",
3729
+
"windows-strings 0.5.0",
3730
]
3731
3732
[[package]]
3733
name = "windows-implement"
3734
+
version = "0.60.1"
3735
source = "registry+https://github.com/rust-lang/crates.io-index"
3736
+
checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
3737
dependencies = [
3738
"proc-macro2",
3739
"quote",
···
3742
3743
[[package]]
3744
name = "windows-interface"
3745
+
version = "0.59.2"
3746
source = "registry+https://github.com/rust-lang/crates.io-index"
3747
+
checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
3748
dependencies = [
3749
"proc-macro2",
3750
"quote",
···
3758
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
3759
3760
[[package]]
3761
+
name = "windows-link"
3762
version = "0.2.0"
3763
source = "registry+https://github.com/rust-lang/crates.io-index"
3764
+
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
3765
3766
[[package]]
3767
name = "windows-registry"
···
3769
source = "registry+https://github.com/rust-lang/crates.io-index"
3770
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
3771
dependencies = [
3772
+
"windows-link 0.1.3",
3773
+
"windows-result 0.3.4",
3774
+
"windows-strings 0.4.2",
3775
]
3776
3777
[[package]]
···
3780
source = "registry+https://github.com/rust-lang/crates.io-index"
3781
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
3782
dependencies = [
3783
+
"windows-link 0.1.3",
3784
+
]
3785
+
3786
+
[[package]]
3787
+
name = "windows-result"
3788
+
version = "0.4.0"
3789
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3790
+
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
3791
+
dependencies = [
3792
+
"windows-link 0.2.0",
3793
]
3794
3795
[[package]]
···
3798
source = "registry+https://github.com/rust-lang/crates.io-index"
3799
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
3800
dependencies = [
3801
+
"windows-link 0.1.3",
3802
+
]
3803
+
3804
+
[[package]]
3805
+
name = "windows-strings"
3806
+
version = "0.5.0"
3807
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3808
+
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
3809
+
dependencies = [
3810
+
"windows-link 0.2.0",
3811
]
3812
3813
[[package]]
···
3843
source = "registry+https://github.com/rust-lang/crates.io-index"
3844
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
3845
dependencies = [
3846
+
"windows-targets 0.53.4",
3847
+
]
3848
+
3849
+
[[package]]
3850
+
name = "windows-sys"
3851
+
version = "0.61.1"
3852
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3853
+
checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
3854
+
dependencies = [
3855
+
"windows-link 0.2.0",
3856
]
3857
3858
[[package]]
···
3888
3889
[[package]]
3890
name = "windows-targets"
3891
+
version = "0.53.4"
3892
source = "registry+https://github.com/rust-lang/crates.io-index"
3893
+
checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
3894
dependencies = [
3895
+
"windows-link 0.2.0",
3896
"windows_aarch64_gnullvm 0.53.0",
3897
"windows_aarch64_msvc 0.53.0",
3898
"windows_i686_gnu 0.53.0",
···
3901
"windows_x86_64_gnu 0.53.0",
3902
"windows_x86_64_gnullvm 0.53.0",
3903
"windows_x86_64_msvc 0.53.0",
3904
]
3905
3906
[[package]]
···
4052
]
4053
4054
[[package]]
4055
+
name = "wit-bindgen"
4056
+
version = "0.46.0"
4057
source = "registry+https://github.com/rust-lang/crates.io-index"
4058
+
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
4059
4060
[[package]]
4061
name = "writeable"
···
4089
4090
[[package]]
4091
name = "zerocopy"
4092
+
version = "0.8.27"
4093
source = "registry+https://github.com/rust-lang/crates.io-index"
4094
+
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
4095
dependencies = [
4096
"zerocopy-derive",
4097
]
4098
4099
[[package]]
4100
name = "zerocopy-derive"
4101
+
version = "0.8.27"
4102
source = "registry+https://github.com/rust-lang/crates.io-index"
4103
+
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
4104
dependencies = [
4105
"proc-macro2",
4106
"quote",
···
4187
4188
[[package]]
4189
name = "zstd-sys"
4190
+
version = "2.0.16+zstd.1.5.7"
4191
source = "registry+https://github.com/rust-lang/crates.io-index"
4192
+
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
4193
dependencies = [
4194
"cc",
4195
"pkg-config",
+7
-4
api/Cargo.toml
+7
-4
api/Cargo.toml
···
49
async-trait = "0.1"
50
51
# AT Protocol client
52
+
atproto-client = "0.12.0"
53
+
atproto-identity = "0.12.0"
54
+
atproto-oauth = "0.12.0"
55
+
atproto-jetstream = "0.12.0"
56
57
58
# Middleware for HTTP requests with retry logic
···
62
# Job queue
63
sqlxmq = "0.6"
64
regex = "1.11.2"
65
+
66
+
# Redis for caching
67
+
redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
+1
-1
api/flake.nix
+1
-1
api/flake.nix
+65
-1
api/src/actor_resolver.rs
+65
-1
api/src/actor_resolver.rs
···
5
web::query as web_query,
6
};
7
use thiserror::Error;
8
9
#[derive(Error, Debug)]
10
pub enum ActorResolverError {
···
18
InvalidSubject,
19
}
20
21
-
#[derive(Debug, Clone)]
22
pub struct ActorData {
23
pub did: String,
24
pub handle: Option<String>,
···
26
}
27
28
pub async fn resolve_actor_data(client: &Client, did: &str) -> Result<ActorData, ActorResolverError> {
29
let (pds_url, handle) = match parse_input(did) {
30
Ok(InputType::Plc(did_str)) => {
31
match plc_query(client, "plc.directory", &did_str).await {
···
5
web::query as web_query,
6
};
7
use thiserror::Error;
8
+
use std::sync::Arc;
9
+
use tokio::sync::Mutex;
10
+
use serde::{Serialize, Deserialize};
11
+
use crate::cache::SliceCache;
12
13
#[derive(Error, Debug)]
14
pub enum ActorResolverError {
···
22
InvalidSubject,
23
}
24
25
+
#[derive(Debug, Clone, Serialize, Deserialize)]
26
pub struct ActorData {
27
pub did: String,
28
pub handle: Option<String>,
···
30
}
31
32
pub async fn resolve_actor_data(client: &Client, did: &str) -> Result<ActorData, ActorResolverError> {
33
+
resolve_actor_data_cached(client, did, None).await
34
+
}
35
+
36
+
pub async fn resolve_actor_data_cached(
37
+
client: &Client,
38
+
did: &str,
39
+
cache: Option<Arc<Mutex<SliceCache>>>
40
+
) -> Result<ActorData, ActorResolverError> {
41
+
// Try cache first if provided
42
+
if let Some(cache) = &cache {
43
+
let cached_result = {
44
+
let mut cache_lock = cache.lock().await;
45
+
cache_lock.get_cached_did_resolution(did).await
46
+
};
47
+
48
+
if let Ok(Some(actor_data_value)) = cached_result
49
+
&& let Ok(actor_data) = serde_json::from_value::<ActorData>(actor_data_value) {
50
+
return Ok(actor_data);
51
+
}
52
+
}
53
+
54
+
// Cache miss - resolve from PLC/web
55
+
let actor_data = resolve_actor_data_impl(client, did).await?;
56
+
57
+
// Cache the result if cache is provided
58
+
if let Some(cache) = &cache
59
+
&& let Ok(actor_data_value) = serde_json::to_value(&actor_data) {
60
+
let mut cache_lock = cache.lock().await;
61
+
let _ = cache_lock.cache_did_resolution(did, &actor_data_value).await;
62
+
}
63
+
64
+
Ok(actor_data)
65
+
}
66
+
67
+
pub async fn resolve_actor_data_with_retry(
68
+
client: &Client,
69
+
did: &str,
70
+
cache: Option<Arc<Mutex<SliceCache>>>,
71
+
invalidate_cache_on_retry: bool
72
+
) -> Result<ActorData, ActorResolverError> {
73
+
match resolve_actor_data_cached(client, did, cache.clone()).await {
74
+
Ok(actor_data) => Ok(actor_data),
75
+
Err(e) => {
76
+
// If we should invalidate cache on retry and we have a cache
77
+
if invalidate_cache_on_retry {
78
+
if let Some(cache) = &cache {
79
+
let mut cache_lock = cache.lock().await;
80
+
let _ = cache_lock.invalidate_did_resolution(did).await;
81
+
}
82
+
83
+
// Retry once with fresh resolution
84
+
resolve_actor_data_cached(client, did, cache).await
85
+
} else {
86
+
Err(e)
87
+
}
88
+
}
89
+
}
90
+
}
91
+
92
+
async fn resolve_actor_data_impl(client: &Client, did: &str) -> Result<ActorData, ActorResolverError> {
93
let (pds_url, handle) = match parse_input(did) {
94
Ok(InputType::Plc(did_str)) => {
95
match plc_query(client, "plc.directory", &did_str).await {
+10
-10
api/src/api/xrpc_dynamic.rs
+10
-10
api/src/api/xrpc_dynamic.rs
···
11
use serde::Deserialize;
12
13
use crate::AppState;
14
-
use crate::auth::{extract_bearer_token, get_atproto_auth_for_user, verify_oauth_token};
15
use crate::models::{
16
IndexedRecord, Record, SliceRecordsOutput, SliceRecordsParams, SortField, WhereCondition,
17
};
···
526
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
527
// Extract and verify OAuth token
528
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
529
-
let user_info = verify_oauth_token(&token, &state.config.auth_base_url)
530
.await
531
.map_err(status_to_error_response)?;
532
533
-
// Get AT Protocol DPoP auth and PDS URL
534
-
let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url)
535
.await
536
.map_err(status_to_error_response)?;
537
···
644
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
645
// Extract and verify OAuth token
646
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
647
-
let user_info = verify_oauth_token(&token, &state.config.auth_base_url)
648
.await
649
.map_err(status_to_error_response)?;
650
651
-
// Get AT Protocol DPoP auth and PDS URL
652
-
let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url)
653
.await
654
.map_err(status_to_error_response)?;
655
···
762
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
763
// Extract and verify OAuth token
764
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
765
-
let user_info = verify_oauth_token(&token, &state.config.auth_base_url)
766
.await
767
.map_err(status_to_error_response)?;
768
769
-
// Get AT Protocol DPoP auth and PDS URL
770
-
let (dpop_auth, pds_url) = get_atproto_auth_for_user(&token, &state.config.auth_base_url)
771
.await
772
.map_err(status_to_error_response)?;
773
···
11
use serde::Deserialize;
12
13
use crate::AppState;
14
+
use crate::auth::{extract_bearer_token, get_atproto_auth_for_user_cached, verify_oauth_token_cached};
15
use crate::models::{
16
IndexedRecord, Record, SliceRecordsOutput, SliceRecordsParams, SortField, WhereCondition,
17
};
···
526
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
527
// Extract and verify OAuth token
528
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
529
+
let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
530
.await
531
.map_err(status_to_error_response)?;
532
533
+
// Get AT Protocol DPoP auth and PDS URL (with caching)
534
+
let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
535
.await
536
.map_err(status_to_error_response)?;
537
···
644
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
645
// Extract and verify OAuth token
646
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
647
+
let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
648
.await
649
.map_err(status_to_error_response)?;
650
651
+
// Get AT Protocol DPoP auth and PDS URL (with caching)
652
+
let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
653
.await
654
.map_err(status_to_error_response)?;
655
···
762
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
763
// Extract and verify OAuth token
764
let token = extract_bearer_token(&headers).map_err(status_to_error_response)?;
765
+
let user_info = verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
766
.await
767
.map_err(status_to_error_response)?;
768
769
+
// Get AT Protocol DPoP auth and PDS URL (with caching)
770
+
let (dpop_auth, pds_url) = get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone()))
771
.await
772
.map_err(status_to_error_response)?;
773
+92
-18
api/src/auth.rs
+92
-18
api/src/auth.rs
···
3
use atproto_client::client::DPoPAuth;
4
use atproto_identity::key::KeyData;
5
use atproto_oauth::jwk::WrappedJsonWebKey;
6
7
#[derive(Serialize, Deserialize, Debug)]
8
pub struct UserInfoResponse {
···
10
pub did: Option<String>,
11
}
12
13
// Extract bearer token from Authorization header
14
pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, StatusCode> {
15
let auth_header = headers
···
26
}
27
28
// Verify OAuth token with auth server
29
-
pub async fn verify_oauth_token(token: &str, auth_base_url: &str) -> Result<UserInfoResponse, StatusCode> {
30
let client = reqwest::Client::new();
31
let userinfo_url = format!("{}/oauth/userinfo", auth_base_url);
32
···
36
.send()
37
.await
38
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
39
-
40
41
if !response.status().is_success() {
42
return Err(StatusCode::UNAUTHORIZED);
···
47
.await
48
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
49
50
Ok(user_info)
51
}
52
53
// Get AT Protocol DPoP auth and PDS URL for the user
54
-
pub async fn get_atproto_auth_for_user(
55
token: &str,
56
auth_base_url: &str,
57
) -> Result<(DPoPAuth, String), StatusCode> {
58
-
// First get session info from auth server
59
let client = reqwest::Client::new();
60
let session_url = format!("{}/api/atprotocol/session", auth_base_url);
61
···
66
.await
67
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
68
69
-
70
if !session_response.status().is_success() {
71
return Err(StatusCode::UNAUTHORIZED);
72
}
···
75
.json()
76
.await
77
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
78
-
79
80
// Extract PDS URL from session
81
let pds_url = session_data["pds_endpoint"]
82
.as_str()
83
-
.ok_or({
84
-
StatusCode::INTERNAL_SERVER_ERROR
85
-
})?
86
.to_string();
87
-
88
89
// Extract AT Protocol access token from session data
90
let atproto_access_token = session_data["access_token"]
91
.as_str()
92
-
.ok_or({
93
-
StatusCode::INTERNAL_SERVER_ERROR
94
-
})?
95
.to_string();
96
-
97
98
// Extract DPoP private key from session data - convert JWK to KeyData
99
-
let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(session_data["dpop_jwk"].clone())
100
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
101
-
102
103
let dpop_private_key_data = KeyData::try_from(dpop_jwk)
104
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
105
106
-
107
let dpop_auth = DPoPAuth {
108
dpop_private_key_data,
109
-
oauth_access_token: atproto_access_token,
110
};
111
112
Ok((dpop_auth, pds_url))
113
}
···
3
use atproto_client::client::DPoPAuth;
4
use atproto_identity::key::KeyData;
5
use atproto_oauth::jwk::WrappedJsonWebKey;
6
+
use std::sync::Arc;
7
+
use tokio::sync::Mutex;
8
+
use crate::cache::SliceCache;
9
10
#[derive(Serialize, Deserialize, Debug)]
11
pub struct UserInfoResponse {
···
13
pub did: Option<String>,
14
}
15
16
+
#[derive(Serialize, Deserialize, Debug, Clone)]
17
+
struct CachedSession {
18
+
pds_url: String,
19
+
atproto_access_token: String,
20
+
dpop_jwk: serde_json::Value,
21
+
}
22
+
23
// Extract bearer token from Authorization header
24
pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, StatusCode> {
25
let auth_header = headers
···
36
}
37
38
// Verify OAuth token with auth server
39
+
40
+
// Verify OAuth token with auth server with optional caching
41
+
pub async fn verify_oauth_token_cached(
42
+
token: &str,
43
+
auth_base_url: &str,
44
+
cache: Option<Arc<Mutex<SliceCache>>>,
45
+
) -> Result<UserInfoResponse, StatusCode> {
46
+
47
+
// Try cache first if provided
48
+
if let Some(cache) = &cache {
49
+
let cached_result = {
50
+
let mut cache_lock = cache.lock().await;
51
+
cache_lock.get_cached_oauth_userinfo(token).await
52
+
};
53
+
54
+
if let Ok(Some(user_info_value)) = cached_result {
55
+
let user_info: UserInfoResponse = serde_json::from_value(user_info_value)
56
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
57
+
return Ok(user_info);
58
+
}
59
+
}
60
+
61
+
// Cache miss - verify with auth server
62
let client = reqwest::Client::new();
63
let userinfo_url = format!("{}/oauth/userinfo", auth_base_url);
64
···
68
.send()
69
.await
70
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
71
72
if !response.status().is_success() {
73
return Err(StatusCode::UNAUTHORIZED);
···
78
.await
79
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
80
81
+
// Cache the userinfo if cache is provided (5 minute TTL)
82
+
if let Some(cache) = &cache {
83
+
let user_info_value = serde_json::to_value(&user_info)
84
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
85
+
let mut cache_lock = cache.lock().await;
86
+
let _ = cache_lock.cache_oauth_userinfo(token, &user_info_value, 300).await;
87
+
}
88
+
89
Ok(user_info)
90
}
91
92
// Get AT Protocol DPoP auth and PDS URL for the user
93
+
94
+
// Get AT Protocol DPoP auth and PDS URL for the user with optional caching
95
+
pub async fn get_atproto_auth_for_user_cached(
96
token: &str,
97
auth_base_url: &str,
98
+
cache: Option<Arc<Mutex<SliceCache>>>,
99
) -> Result<(DPoPAuth, String), StatusCode> {
100
+
101
+
// Try cache first if provided
102
+
if let Some(cache) = &cache {
103
+
let cached_result = {
104
+
let mut cache_lock = cache.lock().await;
105
+
cache_lock.get_cached_atproto_session(token).await
106
+
};
107
+
108
+
if let Ok(Some(session_value)) = cached_result {
109
+
let cached_session: CachedSession = serde_json::from_value(session_value)
110
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
111
+
112
+
// Convert cached data back to DPoP auth
113
+
let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(cached_session.dpop_jwk)
114
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
115
+
116
+
let dpop_private_key_data = KeyData::try_from(dpop_jwk)
117
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
118
+
119
+
let dpop_auth = DPoPAuth {
120
+
dpop_private_key_data,
121
+
oauth_access_token: cached_session.atproto_access_token,
122
+
};
123
+
124
+
return Ok((dpop_auth, cached_session.pds_url));
125
+
}
126
+
}
127
+
128
+
// Cache miss - fetch from auth server
129
let client = reqwest::Client::new();
130
let session_url = format!("{}/api/atprotocol/session", auth_base_url);
131
···
136
.await
137
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
138
139
if !session_response.status().is_success() {
140
return Err(StatusCode::UNAUTHORIZED);
141
}
···
144
.json()
145
.await
146
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
147
148
// Extract PDS URL from session
149
let pds_url = session_data["pds_endpoint"]
150
.as_str()
151
+
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
152
.to_string();
153
154
// Extract AT Protocol access token from session data
155
let atproto_access_token = session_data["access_token"]
156
.as_str()
157
+
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
158
.to_string();
159
160
// Extract DPoP private key from session data - convert JWK to KeyData
161
+
let dpop_jwk_value = session_data["dpop_jwk"].clone();
162
+
let dpop_jwk: WrappedJsonWebKey = serde_json::from_value(dpop_jwk_value.clone())
163
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
164
165
let dpop_private_key_data = KeyData::try_from(dpop_jwk)
166
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
167
168
let dpop_auth = DPoPAuth {
169
dpop_private_key_data,
170
+
oauth_access_token: atproto_access_token.clone(),
171
};
172
+
173
+
// Cache the session data if cache is provided (5 minute TTL)
174
+
if let Some(cache) = &cache {
175
+
let cached_session = CachedSession {
176
+
pds_url: pds_url.clone(),
177
+
atproto_access_token,
178
+
dpop_jwk: dpop_jwk_value,
179
+
};
180
+
let session_value = serde_json::to_value(&cached_session)
181
+
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
182
+
let mut cache_lock = cache.lock().await;
183
+
let _ = cache_lock.cache_atproto_session(token, &session_value, 300).await;
184
+
}
185
186
Ok((dpop_auth, pds_url))
187
}
+455
api/src/cache.rs
+455
api/src/cache.rs
···
···
1
+
use async_trait::async_trait;
2
+
use anyhow::Result;
3
+
use serde::{Serialize, Deserialize};
4
+
use std::collections::{HashMap, HashSet};
5
+
use std::sync::Arc;
6
+
use tokio::sync::RwLock;
7
+
use tracing::{debug, info, warn};
8
+
use std::time::{Duration, Instant};
9
+
10
+
/// Generic cache trait for different backend implementations
11
+
#[async_trait]
12
+
pub trait Cache: Send + Sync {
13
+
/// Get a value from cache
14
+
async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
15
+
where
16
+
T: for<'de> Deserialize<'de> + Send;
17
+
18
+
/// Set a value in cache with optional TTL
19
+
async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
20
+
where
21
+
T: Serialize + Send + Sync;
22
+
23
+
24
+
/// Delete a key from cache
25
+
async fn delete(&mut self, key: &str) -> Result<()>;
26
+
27
+
/// Set multiple key-value pairs
28
+
async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
29
+
where
30
+
T: Serialize + Send + Sync;
31
+
32
+
/// Test cache connection/health
33
+
async fn ping(&mut self) -> Result<bool>;
34
+
35
+
/// Get cache info/statistics
36
+
async fn get_info(&mut self) -> Result<String>;
37
+
}
38
+
39
+
/// Cache entry type: (serialized_value, expiry)
40
+
type CacheEntry = (String, Option<Instant>);
41
+
42
+
/// In-memory cache implementation with TTL support
43
+
pub struct InMemoryCache {
44
+
data: Arc<RwLock<HashMap<String, CacheEntry>>>,
45
+
default_ttl_seconds: u64,
46
+
}
47
+
48
+
impl InMemoryCache {
49
+
pub fn new(default_ttl_seconds: Option<u64>) -> Self {
50
+
Self {
51
+
data: Arc::new(RwLock::new(HashMap::new())),
52
+
default_ttl_seconds: default_ttl_seconds.unwrap_or(3600),
53
+
}
54
+
}
55
+
56
+
}
57
+
58
+
#[async_trait]
59
+
impl Cache for InMemoryCache {
60
+
async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
61
+
where
62
+
T: for<'de> Deserialize<'de> + Send,
63
+
{
64
+
let data = self.data.read().await;
65
+
66
+
if let Some((serialized, expiry)) = data.get(key) {
67
+
// Check if expired
68
+
if let Some(exp) = expiry
69
+
&& *exp <= Instant::now() {
70
+
debug!(cache_key = %key, "Cache entry expired");
71
+
return Ok(None);
72
+
}
73
+
74
+
match serde_json::from_str::<T>(serialized) {
75
+
Ok(value) => {
76
+
// Cache hit - no logging needed
77
+
Ok(Some(value))
78
+
}
79
+
Err(e) => {
80
+
warn!(
81
+
error = ?e,
82
+
cache_key = %key,
83
+
"Failed to deserialize cached value"
84
+
);
85
+
Ok(None)
86
+
}
87
+
}
88
+
} else {
89
+
// Cache miss - no logging needed
90
+
Ok(None)
91
+
}
92
+
}
93
+
94
+
async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
95
+
where
96
+
T: Serialize + Send + Sync,
97
+
{
98
+
let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds);
99
+
100
+
match serde_json::to_string(value) {
101
+
Ok(serialized) => {
102
+
let expiry = if ttl > 0 {
103
+
Some(Instant::now() + Duration::from_secs(ttl))
104
+
} else {
105
+
None // No expiry
106
+
};
107
+
108
+
let mut data = self.data.write().await;
109
+
data.insert(key.to_string(), (serialized, expiry));
110
+
111
+
debug!(
112
+
cache_key = %key,
113
+
ttl_seconds = ttl,
114
+
"Cached value in memory"
115
+
);
116
+
Ok(())
117
+
}
118
+
Err(e) => {
119
+
warn!(
120
+
error = ?e,
121
+
cache_key = %key,
122
+
"Failed to serialize value for caching"
123
+
);
124
+
Ok(())
125
+
}
126
+
}
127
+
}
128
+
129
+
130
+
async fn delete(&mut self, key: &str) -> Result<()> {
131
+
let mut data = self.data.write().await;
132
+
data.remove(key);
133
+
debug!(cache_key = %key, "Deleted key from in-memory cache");
134
+
Ok(())
135
+
}
136
+
137
+
async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
138
+
where
139
+
T: Serialize + Send + Sync,
140
+
{
141
+
if items.is_empty() {
142
+
return Ok(());
143
+
}
144
+
145
+
let mut data = self.data.write().await;
146
+
let mut success_count = 0;
147
+
148
+
for (key, value, ttl) in &items {
149
+
match serde_json::to_string(value) {
150
+
Ok(serialized) => {
151
+
let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds);
152
+
let expiry = if ttl_to_use > 0 {
153
+
Some(Instant::now() + Duration::from_secs(ttl_to_use))
154
+
} else {
155
+
None
156
+
};
157
+
158
+
data.insert(key.to_string(), (serialized, expiry));
159
+
success_count += 1;
160
+
}
161
+
Err(e) => {
162
+
warn!(
163
+
error = ?e,
164
+
cache_key = %key,
165
+
"Failed to serialize value for bulk caching"
166
+
);
167
+
}
168
+
}
169
+
}
170
+
171
+
debug!(
172
+
items_count = success_count,
173
+
total_items = items.len(),
174
+
"Successfully bulk cached items in memory"
175
+
);
176
+
Ok(())
177
+
}
178
+
179
+
async fn ping(&mut self) -> Result<bool> {
180
+
// Always healthy for in-memory cache
181
+
Ok(true)
182
+
}
183
+
184
+
async fn get_info(&mut self) -> Result<String> {
185
+
let data = self.data.read().await;
186
+
let now = Instant::now();
187
+
188
+
let mut total_entries = 0;
189
+
let mut expired_entries = 0;
190
+
191
+
for (_, expiry) in data.values() {
192
+
total_entries += 1;
193
+
if let Some(exp) = expiry
194
+
&& *exp <= now {
195
+
expired_entries += 1;
196
+
}
197
+
}
198
+
199
+
Ok(format!(
200
+
"InMemoryCache: {} total entries, {} expired, {} active",
201
+
total_entries,
202
+
expired_entries,
203
+
total_entries - expired_entries
204
+
))
205
+
}
206
+
}
207
+
208
+
/// Cache backend enum to avoid dyn trait issues
209
+
pub enum CacheBackendImpl {
210
+
InMemory(InMemoryCache),
211
+
Redis(crate::redis_cache::RedisCache),
212
+
}
213
+
214
+
impl CacheBackendImpl {
215
+
pub async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
216
+
where
217
+
T: for<'de> Deserialize<'de> + Send,
218
+
{
219
+
match self {
220
+
CacheBackendImpl::InMemory(cache) => cache.get(key).await,
221
+
CacheBackendImpl::Redis(cache) => cache.get(key).await,
222
+
}
223
+
}
224
+
225
+
pub async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
226
+
where
227
+
T: Serialize + Send + Sync,
228
+
{
229
+
match self {
230
+
CacheBackendImpl::InMemory(cache) => cache.set(key, value, ttl_seconds).await,
231
+
CacheBackendImpl::Redis(cache) => cache.set(key, value, ttl_seconds).await,
232
+
}
233
+
}
234
+
235
+
pub async fn delete(&mut self, key: &str) -> Result<()> {
236
+
match self {
237
+
CacheBackendImpl::InMemory(cache) => cache.delete(key).await,
238
+
CacheBackendImpl::Redis(cache) => cache.delete(key).await,
239
+
}
240
+
}
241
+
242
+
pub async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
243
+
where
244
+
T: Serialize + Send + Sync,
245
+
{
246
+
match self {
247
+
CacheBackendImpl::InMemory(cache) => cache.set_multiple(items).await,
248
+
CacheBackendImpl::Redis(cache) => cache.set_multiple(items).await,
249
+
}
250
+
}
251
+
252
+
pub async fn ping(&mut self) -> Result<bool> {
253
+
match self {
254
+
CacheBackendImpl::InMemory(cache) => cache.ping().await,
255
+
CacheBackendImpl::Redis(cache) => cache.ping().await,
256
+
}
257
+
}
258
+
259
+
pub async fn get_info(&mut self) -> Result<String> {
260
+
match self {
261
+
CacheBackendImpl::InMemory(cache) => cache.get_info().await,
262
+
CacheBackendImpl::Redis(cache) => cache.get_info().await,
263
+
}
264
+
}
265
+
}
266
+
267
+
/// Cache-specific helper methods for slice operations
268
+
pub struct SliceCache {
269
+
cache: CacheBackendImpl,
270
+
}
271
+
272
+
impl SliceCache {
273
+
pub fn new(cache: CacheBackendImpl) -> Self {
274
+
Self { cache }
275
+
}
276
+
277
+
/// Actor cache methods
278
+
pub async fn is_actor(&mut self, did: &str, slice_uri: &str) -> Result<Option<bool>> {
279
+
let key = format!("actor:{}:{}", did, slice_uri);
280
+
self.cache.get::<bool>(&key).await
281
+
}
282
+
283
+
pub async fn cache_actor_exists(&mut self, did: &str, slice_uri: &str) -> Result<()> {
284
+
let key = format!("actor:{}:{}", did, slice_uri);
285
+
self.cache.set(&key, &true, None).await
286
+
}
287
+
288
+
pub async fn remove_actor(&mut self, did: &str, slice_uri: &str) -> Result<()> {
289
+
let key = format!("actor:{}:{}", did, slice_uri);
290
+
self.cache.delete(&key).await
291
+
}
292
+
293
+
pub async fn preload_actors(&mut self, actors: Vec<(String, String)>) -> Result<()> {
294
+
if actors.is_empty() {
295
+
return Ok(());
296
+
}
297
+
298
+
let items: Vec<(String, bool, Option<u64>)> = actors
299
+
.into_iter()
300
+
.map(|(did, slice_uri)| {
301
+
(format!("actor:{}:{}", did, slice_uri), true, None)
302
+
})
303
+
.collect();
304
+
305
+
let items_ref: Vec<(&str, &bool, Option<u64>)> = items
306
+
.iter()
307
+
.map(|(key, value, ttl)| (key.as_str(), value, *ttl))
308
+
.collect();
309
+
310
+
self.cache.set_multiple(items_ref).await
311
+
}
312
+
313
+
/// Lexicon cache methods
314
+
pub async fn cache_lexicons(&mut self, slice_uri: &str, lexicons: &Vec<serde_json::Value>) -> Result<()> {
315
+
let key = format!("lexicons:{}", slice_uri);
316
+
let lexicons_ttl = 7200; // 2 hours for lexicons
317
+
self.cache.set(&key, lexicons, Some(lexicons_ttl)).await
318
+
}
319
+
320
+
pub async fn get_lexicons(&mut self, slice_uri: &str) -> Result<Option<Vec<serde_json::Value>>> {
321
+
let key = format!("lexicons:{}", slice_uri);
322
+
self.cache.get::<Vec<serde_json::Value>>(&key).await
323
+
}
324
+
325
+
/// Domain cache methods
326
+
pub async fn cache_slice_domain(&mut self, slice_uri: &str, domain: &str) -> Result<()> {
327
+
let key = format!("domain:{}", slice_uri);
328
+
let domain_ttl = 14400; // 4 hours for domains
329
+
self.cache.set(&key, &domain.to_string(), Some(domain_ttl)).await
330
+
}
331
+
332
+
pub async fn get_slice_domain(&mut self, slice_uri: &str) -> Result<Option<String>> {
333
+
let key = format!("domain:{}", slice_uri);
334
+
self.cache.get::<String>(&key).await
335
+
}
336
+
337
+
/// Collections cache methods
338
+
pub async fn cache_slice_collections(&mut self, slice_uri: &str, collections: &HashSet<String>) -> Result<()> {
339
+
let key = format!("collections:{}", slice_uri);
340
+
let collections_ttl = 7200; // 2 hours for collections
341
+
self.cache.set(&key, collections, Some(collections_ttl)).await
342
+
}
343
+
344
+
pub async fn get_slice_collections(&mut self, slice_uri: &str) -> Result<Option<HashSet<String>>> {
345
+
let key = format!("collections:{}", slice_uri);
346
+
self.cache.get::<HashSet<String>>(&key).await
347
+
}
348
+
349
+
/// Utility methods
350
+
pub async fn ping(&mut self) -> Result<bool> {
351
+
self.cache.ping().await
352
+
}
353
+
354
+
pub async fn get_info(&mut self) -> Result<String> {
355
+
self.cache.get_info().await
356
+
}
357
+
358
+
/// Auth cache methods (5 minute TTL)
359
+
pub async fn get_cached_oauth_userinfo(&mut self, token: &str) -> Result<Option<serde_json::Value>> {
360
+
let key = format!("oauth_userinfo:{}", token);
361
+
self.cache.get(&key).await
362
+
}
363
+
364
+
pub async fn cache_oauth_userinfo(&mut self, token: &str, userinfo: &serde_json::Value, ttl_seconds: u64) -> Result<()> {
365
+
let key = format!("oauth_userinfo:{}", token);
366
+
self.cache.set(&key, userinfo, Some(ttl_seconds)).await
367
+
}
368
+
369
+
pub async fn get_cached_atproto_session(&mut self, token: &str) -> Result<Option<serde_json::Value>> {
370
+
let key = format!("atproto_session:{}", token);
371
+
self.cache.get(&key).await
372
+
}
373
+
374
+
pub async fn cache_atproto_session(&mut self, token: &str, session: &serde_json::Value, ttl_seconds: u64) -> Result<()> {
375
+
let key = format!("atproto_session:{}", token);
376
+
self.cache.set(&key, session, Some(ttl_seconds)).await
377
+
}
378
+
379
+
/// DID resolution cache methods (24 hour TTL - DIDs change infrequently)
380
+
pub async fn get_cached_did_resolution(&mut self, did: &str) -> Result<Option<serde_json::Value>> {
381
+
let key = format!("did_resolution:{}", did);
382
+
self.cache.get(&key).await
383
+
}
384
+
385
+
pub async fn cache_did_resolution(&mut self, did: &str, actor_data: &serde_json::Value) -> Result<()> {
386
+
let key = format!("did_resolution:{}", did);
387
+
let ttl_seconds = 24 * 60 * 60; // 24 hours
388
+
self.cache.set(&key, actor_data, Some(ttl_seconds)).await
389
+
}
390
+
391
+
pub async fn invalidate_did_resolution(&mut self, did: &str) -> Result<()> {
392
+
let key = format!("did_resolution:{}", did);
393
+
self.cache.delete(&key).await
394
+
}
395
+
396
+
/// Generic get/set for custom caching needs
397
+
pub async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
398
+
where
399
+
T: for<'de> Deserialize<'de> + Send,
400
+
{
401
+
self.cache.get(key).await
402
+
}
403
+
404
+
pub async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
405
+
where
406
+
T: Serialize + Send + Sync,
407
+
{
408
+
self.cache.set(key, value, ttl_seconds).await
409
+
}
410
+
}
411
+
412
+
/// Cache backend configuration
413
+
#[derive(Debug, Clone)]
414
+
pub enum CacheBackend {
415
+
InMemory { ttl_seconds: Option<u64> },
416
+
Redis { url: String, ttl_seconds: Option<u64> },
417
+
}
418
+
419
+
/// Cache factory for creating cache instances
420
+
pub struct CacheFactory;
421
+
422
+
impl CacheFactory {
423
+
/// Create a cache instance based on configuration
424
+
pub async fn create_cache(backend: CacheBackend) -> Result<CacheBackendImpl> {
425
+
match backend {
426
+
CacheBackend::InMemory { ttl_seconds } => {
427
+
let ttl_display = ttl_seconds.map(|t| format!("{}s", t)).unwrap_or_else(|| "default".to_string());
428
+
info!("Creating in-memory cache with TTL: {}", ttl_display);
429
+
Ok(CacheBackendImpl::InMemory(InMemoryCache::new(ttl_seconds)))
430
+
}
431
+
CacheBackend::Redis { url, ttl_seconds } => {
432
+
info!("Attempting to create Redis cache at: {}", url);
433
+
match crate::redis_cache::RedisCache::new(&url, ttl_seconds).await {
434
+
Ok(redis_cache) => {
435
+
info!("✓ Created Redis cache successfully");
436
+
Ok(CacheBackendImpl::Redis(redis_cache))
437
+
}
438
+
Err(e) => {
439
+
warn!(
440
+
error = ?e,
441
+
"Failed to create Redis cache, falling back to in-memory"
442
+
);
443
+
Ok(CacheBackendImpl::InMemory(InMemoryCache::new(ttl_seconds)))
444
+
}
445
+
}
446
+
}
447
+
}
448
+
}
449
+
450
+
/// Create a SliceCache with the specified backend
451
+
pub async fn create_slice_cache(backend: CacheBackend) -> Result<SliceCache> {
452
+
let cache = Self::create_cache(backend).await?;
453
+
Ok(SliceCache::new(cache))
454
+
}
455
+
}
+4
api/src/errors.rs
+4
api/src/errors.rs
···
64
65
#[error("error-slices-app-8 Forbidden: {0}")]
66
Forbidden(String),
67
}
68
69
impl From<StatusCode> for AppError {
···
99
AppError::DatabaseConnection(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
100
AppError::Migration(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
101
AppError::ServerBind(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
102
};
103
104
let body = Json(serde_json::json!({
···
64
65
#[error("error-slices-app-8 Forbidden: {0}")]
66
Forbidden(String),
67
+
68
+
#[error("error-slices-app-9 Cache error: {0}")]
69
+
Cache(#[from] anyhow::Error),
70
}
71
72
impl From<StatusCode> for AppError {
···
102
AppError::DatabaseConnection(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
103
AppError::Migration(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
104
AppError::ServerBind(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
105
+
AppError::Cache(e) => (StatusCode::INTERNAL_SERVER_ERROR, "InternalServerError", e.to_string()),
106
};
107
108
let body = Json(serde_json::json!({
+314
-254
api/src/jetstream.rs
+314
-254
api/src/jetstream.rs
···
2
use async_trait::async_trait;
3
use anyhow::Result;
4
use chrono::Utc;
5
-
use std::collections::{HashMap, HashSet};
6
use std::sync::Arc;
7
-
use tokio::sync::RwLock;
8
-
use tracing::{error, info};
9
use reqwest::Client;
10
11
use crate::actor_resolver::resolve_actor_data;
···
14
use crate::models::{Record, Actor};
15
use crate::errors::SliceError;
16
use crate::logging::{Logger, LogLevel};
17
18
pub struct JetstreamConsumer {
19
consumer: Consumer,
20
database: Database,
21
http_client: Client,
22
-
slice_collections: Arc<RwLock<HashMap<String, HashSet<String>>>>,
23
-
slice_domains: Arc<RwLock<HashMap<String, String>>>,
24
-
actor_cache: Arc<RwLock<HashMap<(String, String), bool>>>,
25
-
slice_lexicons: Arc<RwLock<HashMap<String, Vec<serde_json::Value>>>>,
26
pub event_count: Arc<std::sync::atomic::AtomicU64>,
27
cursor_handler: Option<Arc<PostgresCursorHandler>>,
28
}
29
30
// Event handler that implements the EventHandler trait
31
struct SliceEventHandler {
32
database: Database,
33
http_client: Client,
34
-
slice_collections: Arc<RwLock<HashMap<String, HashSet<String>>>>,
35
-
slice_domains: Arc<RwLock<HashMap<String, String>>>,
36
event_count: Arc<std::sync::atomic::AtomicU64>,
37
-
actor_cache: Arc<RwLock<HashMap<(String, String), bool>>>,
38
-
slice_lexicons: Arc<RwLock<HashMap<String, Vec<serde_json::Value>>>>,
39
cursor_handler: Option<Arc<PostgresCursorHandler>>,
40
}
41
42
#[async_trait]
···
101
}
102
103
impl SliceEventHandler {
104
async fn handle_commit_event(
105
&self,
106
did: &str,
107
commit: atproto_jetstream::JetstreamEventCommit,
108
) -> Result<()> {
109
-
let slice_collections = self.slice_collections.read().await;
110
-
let slice_domains = self.slice_domains.read().await;
111
-
let slice_lexicons = self.slice_lexicons.read().await;
112
-
113
-
for (slice_uri, collections) in slice_collections.iter() {
114
if collections.contains(&commit.collection) {
115
// Special handling for network.slices.lexicon records
116
// These should only be indexed to the slice specified in their JSON data
···
125
continue;
126
}
127
}
128
-
// Get the domain for this slice
129
-
let domain = match slice_domains.get(slice_uri) {
130
-
Some(d) => d,
131
-
None => continue, // No domain, skip
132
};
133
-
134
// Check if this is a primary collection (starts with slice domain)
135
-
let is_primary_collection = commit.collection.starts_with(domain);
136
-
137
// For external collections, check actor status BEFORE expensive validation
138
if !is_primary_collection {
139
-
let cache_key = (did.to_string(), slice_uri.clone());
140
-
let is_actor = {
141
-
let cache = self.actor_cache.read().await;
142
-
cache.get(&cache_key).copied()
143
-
};
144
-
145
-
let is_actor: Result<bool, anyhow::Error> = match is_actor {
146
-
Some(cached_result) => Ok(cached_result),
147
-
None => {
148
// Cache miss means this DID is not an actor we've synced
149
// For external collections, we only care about actors we've already added
150
-
// Don't cache negative results to avoid memory bloat
151
-
Ok(false)
152
-
}
153
-
};
154
-
155
-
match is_actor {
156
-
Ok(false) => {
157
-
// Not an actor - skip validation entirely for external collections
158
-
continue;
159
-
}
160
-
Ok(true) => {
161
-
// Actor found - continue to validation
162
}
163
Err(e) => {
164
error!("Error checking actor status: {}", e);
165
continue;
166
}
167
}
168
}
169
-
170
// Get lexicons for validation (after actor check for external collections)
171
-
let lexicons = match slice_lexicons.get(slice_uri) {
172
-
Some(lexicons) => lexicons.clone(),
173
-
None => {
174
-
// Fallback: Try to load fresh lexicons from database for this slice
175
-
info!("No cached lexicons for slice {} - attempting database fallback", slice_uri);
176
-
match self.database.get_lexicons_by_slice(slice_uri).await {
177
-
Ok(fresh_lexicons) if !fresh_lexicons.is_empty() => {
178
-
info!("✓ Loaded fresh lexicons for slice {} from database", slice_uri);
179
-
// Cache the fresh lexicons for future use
180
-
{
181
-
let mut lexicons_cache = self.slice_lexicons.write().await;
182
-
lexicons_cache.insert(slice_uri.clone(), fresh_lexicons.clone());
183
-
}
184
-
fresh_lexicons
185
-
}
186
-
_ => {
187
-
info!("No lexicons found for slice {} - skipping validation", slice_uri);
188
-
continue;
189
-
}
190
-
}
191
}
192
};
193
-
194
// Validate the record against the slice's lexicons
195
let validation_result = match slices_lexicon::validate_record(lexicons.clone(), &commit.collection, commit.record.clone()) {
196
Ok(_) => {
···
198
true
199
}
200
Err(e) => {
201
-
info!("Validation failed with cached validator for collection {} in slice {}: {} - trying database fallback",
202
-
commit.collection, slice_uri, e);
203
-
204
-
// Try database fallback in case lexicons were updated
205
-
match self.database.get_lexicons_by_slice(slice_uri).await {
206
-
Ok(fresh_lexicons) if !fresh_lexicons.is_empty() => {
207
-
match slices_lexicon::validate_record(fresh_lexicons.clone(), &commit.collection, commit.record.clone()) {
208
-
Ok(_) => {
209
-
info!("✓ Record validated with fresh lexicons for collection {} in slice {}",
210
-
commit.collection, slice_uri);
211
-
// Update cache with fresh lexicons
212
-
{
213
-
let mut lexicons_cache = self.slice_lexicons.write().await;
214
-
lexicons_cache.insert(slice_uri.clone(), fresh_lexicons);
215
-
}
216
-
true
217
-
}
218
-
Err(fresh_e) => {
219
-
let message = format!("Validation failed for collection {} in slice {}", commit.collection, slice_uri);
220
-
error!("✗ {}: {}", message, fresh_e);
221
-
Logger::global().log_jetstream_with_slice(LogLevel::Warn, &message, Some(serde_json::json!({
222
-
"collection": commit.collection,
223
-
"slice_uri": slice_uri,
224
-
"did": did
225
-
})), Some(slice_uri));
226
-
false
227
-
}
228
-
}
229
-
}
230
-
Ok(_) => {
231
-
// Empty lexicons - skip logging as this is expected for many slices
232
-
false
233
-
}
234
-
Err(_) => {
235
-
// Database error - skip logging for missing lexicons
236
-
false
237
-
}
238
-
}
239
}
240
};
241
-
242
if !validation_result {
243
continue; // Skip this slice if validation fails
244
}
245
-
246
if is_primary_collection {
247
// Primary collection - ensure actor exists and index ALL records
248
info!("✓ Primary collection {} for slice {} (domain: {}) - indexing record",
249
commit.collection, slice_uri, domain);
250
251
// Ensure actor exists for primary collections
252
-
let cache_key = (did.to_string(), slice_uri.clone());
253
-
let is_cached = {
254
-
let cache = self.actor_cache.read().await;
255
-
cache.contains_key(&cache_key)
256
-
};
257
258
if !is_cached {
259
// Actor not in cache - create it
···
274
error!("Failed to create actor {}: {}", did, e);
275
} else {
276
// Add to cache after successful database insert
277
-
let mut cache = self.actor_cache.write().await;
278
-
cache.insert(cache_key, true);
279
info!("✓ Created actor {} for slice {}", did, slice_uri);
280
}
281
}
···
284
}
285
}
286
}
287
-
288
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
289
-
290
let record = Record {
291
uri: uri.clone(),
292
cid: commit.cid.clone(),
···
296
indexed_at: Utc::now(),
297
slice_uri: Some(slice_uri.clone()),
298
};
299
-
300
match self.database.upsert_record(&record).await {
301
Ok(is_insert) => {
302
-
let message = if is_insert {
303
format!("Record inserted in {}", commit.collection)
304
-
} else {
305
format!("Record updated in {}", commit.collection)
306
};
307
let operation = if is_insert { "insert" } else { "update" };
···
311
"slice_uri": slice_uri,
312
"did": did,
313
"record_type": "primary"
314
-
})), Some(slice_uri));
315
}
316
Err(e) => {
317
let message = "Failed to insert/update record";
···
322
"did": did,
323
"error": e.to_string(),
324
"record_type": "primary"
325
-
})), Some(slice_uri));
326
return Err(anyhow::anyhow!("Database error: {}", e));
327
}
328
}
329
-
330
-
info!("✓ Successfully indexed {} record from primary collection: {}",
331
commit.operation, uri);
332
break;
333
} else {
334
// External collection - we already checked actor status, so just index
335
-
info!("✓ External collection {} - DID {} is actor in slice {} - indexing",
336
commit.collection, did, slice_uri);
337
-
338
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
339
-
340
let record = Record {
341
uri: uri.clone(),
342
cid: commit.cid.clone(),
···
346
indexed_at: Utc::now(),
347
slice_uri: Some(slice_uri.clone()),
348
};
349
-
350
match self.database.upsert_record(&record).await {
351
Ok(is_insert) => {
352
-
let message = if is_insert {
353
format!("Record inserted in {}", commit.collection)
354
-
} else {
355
format!("Record updated in {}", commit.collection)
356
};
357
let operation = if is_insert { "insert" } else { "update" };
···
361
"slice_uri": slice_uri,
362
"did": did,
363
"record_type": "external"
364
-
})), Some(slice_uri));
365
}
366
Err(e) => {
367
let message = "Failed to insert/update record";
···
372
"did": did,
373
"error": e.to_string(),
374
"record_type": "external"
375
-
})), Some(slice_uri));
376
return Err(anyhow::anyhow!("Database error: {}", e));
377
}
378
}
379
-
380
-
info!("✓ Successfully indexed {} record from external collection: {}",
381
commit.operation, uri);
382
break;
383
}
384
}
385
}
386
-
387
Ok(())
388
}
389
···
394
) -> Result<()> {
395
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
396
397
-
// Get slices that track this collection
398
-
let slice_collections = self.slice_collections.read().await;
399
-
let slice_domains = self.slice_domains.read().await;
400
-
let actor_cache = self.actor_cache.read().await;
401
402
let mut relevant_slices: Vec<String> = Vec::new();
403
404
-
for (slice_uri, collections) in slice_collections.iter() {
405
if !collections.contains(&commit.collection) {
406
continue;
407
}
408
409
-
// Get the domain for this slice
410
-
let domain = match slice_domains.get(slice_uri) {
411
-
Some(d) => d,
412
-
None => continue,
413
};
414
415
// Check if this is a primary collection (starts with slice domain)
416
-
let is_primary_collection = commit.collection.starts_with(domain);
417
418
if is_primary_collection {
419
// Primary collection - always process deletes
420
relevant_slices.push(slice_uri.clone());
421
} else {
422
// External collection - only process if DID is an actor in this slice
423
-
let cache_key = (did.to_string(), slice_uri.clone());
424
-
if actor_cache.get(&cache_key).copied().unwrap_or(false) {
425
relevant_slices.push(slice_uri.clone());
426
}
427
}
···
466
if deleted > 0 {
467
info!("✓ Cleaned up actor {} from slice {} (no records remaining)", did, slice_uri);
468
// Remove from cache
469
-
let cache_key = (did.to_string(), slice_uri.clone());
470
-
let mut cache = self.actor_cache.write().await;
471
-
cache.remove(&cache_key);
472
}
473
}
474
Err(e) => {
···
512
}
513
514
impl JetstreamConsumer {
515
-
/// Create a new Jetstream consumer with optional cursor support
516
///
517
/// # Arguments
518
/// * `database` - Database connection for slice configurations and record storage
519
/// * `jetstream_hostname` - Optional custom jetstream hostname
520
/// * `cursor_handler` - Optional cursor handler for resumable event processing
521
/// * `initial_cursor` - Optional starting cursor position (time_us) to resume from
522
pub async fn new(
523
database: Database,
524
jetstream_hostname: Option<String>,
525
cursor_handler: Option<Arc<PostgresCursorHandler>>,
526
initial_cursor: Option<i64>,
527
) -> Result<Self, SliceError> {
528
let config = ConsumerTaskConfig {
529
user_agent: "slice-server/1.0".to_string(),
···
541
let consumer = Consumer::new(config);
542
let http_client = Client::new();
543
544
Ok(Self {
545
consumer,
546
database,
547
http_client,
548
-
slice_collections: Arc::new(RwLock::new(HashMap::new())),
549
-
slice_domains: Arc::new(RwLock::new(HashMap::new())),
550
-
actor_cache: Arc::new(RwLock::new(HashMap::new())),
551
-
slice_lexicons: Arc::new(RwLock::new(HashMap::new())),
552
event_count: Arc::new(std::sync::atomic::AtomicU64::new(0)),
553
cursor_handler,
554
})
555
}
556
557
-
/// Load slice configurations to know which collections to index
558
pub async fn load_slice_configurations(&self) -> Result<(), SliceError> {
559
-
info!("Loading slice configurations for Jetstream indexing");
560
-
561
-
// Get all slices that have lexicon definitions
562
-
let slices = self.database.get_all_slices().await?;
563
-
info!("Found {} total slices in database", slices.len());
564
-
565
-
let mut collections_map = HashMap::new();
566
-
let mut domains_map = HashMap::new();
567
-
let mut total_collections = 0;
568
-
569
-
for slice_uri in &slices {
570
-
info!("Checking slice: {}", slice_uri);
571
-
572
-
// Get the domain for this slice
573
-
if let Ok(Some(domain)) = self.database.get_slice_domain(slice_uri).await {
574
-
info!("Slice {} has domain: {}", slice_uri, domain);
575
-
domains_map.insert(slice_uri.clone(), domain.clone());
576
-
577
-
// Get collections defined in this slice's lexicons
578
-
let collections = self.database.get_slice_collections_list(slice_uri).await?;
579
-
580
-
if !collections.is_empty() {
581
-
// Categorize collections as primary or external
582
-
let mut primary = Vec::new();
583
-
let mut external = Vec::new();
584
-
585
-
for collection in &collections {
586
-
if collection.starts_with(&domain) {
587
-
primary.push(collection.clone());
588
-
} else {
589
-
external.push(collection.clone());
590
-
}
591
-
}
592
-
593
-
info!("Slice {} has {} primary collections: {:?}", slice_uri, primary.len(), primary);
594
-
info!("Slice {} has {} external collections: {:?}", slice_uri, external.len(), external);
595
-
596
-
total_collections += collections.len();
597
-
collections_map.insert(slice_uri.clone(), collections.into_iter().collect());
598
-
} else {
599
-
info!("Slice {} has no collections defined (no lexicons or empty lexicons)", slice_uri);
600
-
}
601
-
} else {
602
-
info!("Slice {} has no domain defined - skipping", slice_uri);
603
-
}
604
-
}
605
-
606
-
let mut slice_collections = self.slice_collections.write().await;
607
-
*slice_collections = collections_map;
608
-
609
-
let mut slice_domains = self.slice_domains.write().await;
610
-
*slice_domains = domains_map;
611
-
612
-
// Load lexicons for each slice
613
-
let mut lexicons_map = HashMap::new();
614
-
for slice_uri in slice_collections.keys() {
615
-
info!("Loading lexicons for slice: {}", slice_uri);
616
-
617
-
// Get all lexicons for this slice
618
-
match self.database.get_lexicons_by_slice(slice_uri).await {
619
-
Ok(lexicons) if !lexicons.is_empty() => {
620
-
lexicons_map.insert(slice_uri.clone(), lexicons);
621
-
info!("✓ Loaded lexicons for slice {}", slice_uri);
622
-
}
623
-
Ok(_) => {
624
-
info!("No lexicons found for slice {}", slice_uri);
625
-
}
626
-
Err(e) => {
627
-
error!("Failed to load lexicons for slice {}: {}", slice_uri, e);
628
-
}
629
-
}
630
-
}
631
632
-
let mut slice_lexicons = self.slice_lexicons.write().await;
633
-
*slice_lexicons = lexicons_map;
634
635
-
info!("Jetstream consumer will monitor {} total collections across {} slices with {} lexicon sets loaded",
636
-
total_collections, slice_collections.len(), slice_lexicons.len());
637
-
638
Ok(())
639
}
640
641
/// Preload actor cache to avoid database hits during event processing
642
async fn preload_actor_cache(&self) -> Result<(), SliceError> {
643
info!("Preloading actor cache...");
644
-
645
let actors = self.database.get_all_actors().await?;
646
info!("Found {} actors to cache", actors.len());
647
-
648
-
let mut cache = self.actor_cache.write().await;
649
-
cache.clear(); // Clear existing cache
650
-
for (did, slice_uri) in actors {
651
-
cache.insert((did, slice_uri), true);
652
}
653
-
654
-
info!("Actor cache preloaded with {} entries", cache.len());
655
-
Ok(())
656
}
657
-
658
659
/// Start consuming events from Jetstream
660
pub async fn start_consuming(&self, cancellation_token: CancellationToken) -> Result<(), SliceError> {
661
info!("Starting Jetstream consumer");
662
-
663
// Load initial slice configurations
664
self.load_slice_configurations().await?;
665
-
666
// Preload actor cache
667
self.preload_actor_cache().await?;
668
-
669
// Create and register the event handler
670
let handler = Arc::new(SliceEventHandler {
671
database: self.database.clone(),
672
http_client: self.http_client.clone(),
673
-
slice_collections: self.slice_collections.clone(),
674
-
slice_domains: self.slice_domains.clone(),
675
event_count: self.event_count.clone(),
676
actor_cache: self.actor_cache.clone(),
677
-
slice_lexicons: self.slice_lexicons.clone(),
678
cursor_handler: self.cursor_handler.clone(),
679
});
680
-
681
self.consumer.register_handler(handler).await
682
.map_err(|e| SliceError::JetstreamError {
683
message: format!("Failed to register event handler: {}", e),
684
})?;
685
-
686
// Start periodic status reporting
687
let event_count_for_status = self.event_count.clone();
688
tokio::spawn(async move {
···
693
info!("Jetstream consumer status: {} total events processed", count);
694
}
695
});
696
-
697
// Start the consumer
698
info!("Starting Jetstream background consumer...");
699
let result = self.consumer.run_background(cancellation_token).await
···
718
pub fn start_configuration_reloader(consumer: Arc<Self>) {
719
tokio::spawn(async move {
720
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // Reload every 5 minutes
721
-
722
loop {
723
interval.tick().await;
724
-
725
if let Err(e) = consumer.load_slice_configurations().await {
726
error!("Failed to reload slice configurations: {}", e);
727
}
728
-
729
if let Err(e) = consumer.preload_actor_cache().await {
730
error!("Failed to reload actor cache: {}", e);
731
}
732
}
733
});
734
}
735
-
}
···
2
use async_trait::async_trait;
3
use anyhow::Result;
4
use chrono::Utc;
5
+
use std::collections::HashSet;
6
use std::sync::Arc;
7
+
use tokio::sync::{Mutex, RwLock};
8
+
use tracing::{error, info, warn};
9
use reqwest::Client;
10
11
use crate::actor_resolver::resolve_actor_data;
···
14
use crate::models::{Record, Actor};
15
use crate::errors::SliceError;
16
use crate::logging::{Logger, LogLevel};
17
+
use crate::cache::{SliceCache, CacheFactory, CacheBackend};
18
19
pub struct JetstreamConsumer {
20
consumer: Consumer,
21
database: Database,
22
http_client: Client,
23
+
actor_cache: Arc<Mutex<SliceCache>>,
24
+
lexicon_cache: Arc<Mutex<SliceCache>>,
25
+
domain_cache: Arc<Mutex<SliceCache>>,
26
+
collections_cache: Arc<Mutex<SliceCache>>,
27
pub event_count: Arc<std::sync::atomic::AtomicU64>,
28
cursor_handler: Option<Arc<PostgresCursorHandler>>,
29
+
slices_list: Arc<RwLock<Vec<String>>>,
30
}
31
32
// Event handler that implements the EventHandler trait
33
struct SliceEventHandler {
34
database: Database,
35
http_client: Client,
36
event_count: Arc<std::sync::atomic::AtomicU64>,
37
+
actor_cache: Arc<Mutex<SliceCache>>,
38
+
lexicon_cache: Arc<Mutex<SliceCache>>,
39
+
domain_cache: Arc<Mutex<SliceCache>>,
40
+
collections_cache: Arc<Mutex<SliceCache>>,
41
cursor_handler: Option<Arc<PostgresCursorHandler>>,
42
+
slices_list: Arc<RwLock<Vec<String>>>,
43
}
44
45
#[async_trait]
···
104
}
105
106
impl SliceEventHandler {
107
+
/// Check if DID is an actor for the given slice
108
+
async fn is_actor_cached(&self, did: &str, slice_uri: &str) -> Result<Option<bool>, anyhow::Error> {
109
+
match self.actor_cache.lock().await.is_actor(did, slice_uri).await {
110
+
Ok(result) => Ok(result),
111
+
Err(e) => {
112
+
warn!(
113
+
error = ?e,
114
+
did = did,
115
+
slice_uri = slice_uri,
116
+
"Actor cache error"
117
+
);
118
+
Ok(None)
119
+
}
120
+
}
121
+
}
122
+
123
+
/// Cache that an actor exists
124
+
async fn cache_actor_exists(&self, did: &str, slice_uri: &str) {
125
+
if let Err(e) = self.actor_cache.lock().await.cache_actor_exists(did, slice_uri).await {
126
+
warn!(
127
+
error = ?e,
128
+
did = did,
129
+
slice_uri = slice_uri,
130
+
"Failed to cache actor exists"
131
+
);
132
+
}
133
+
}
134
+
135
+
/// Remove actor from cache
136
+
async fn remove_actor_from_cache(&self, did: &str, slice_uri: &str) {
137
+
if let Err(e) = self.actor_cache.lock().await.remove_actor(did, slice_uri).await {
138
+
warn!(
139
+
error = ?e,
140
+
did = did,
141
+
slice_uri = slice_uri,
142
+
"Failed to remove actor from cache"
143
+
);
144
+
}
145
+
}
146
+
147
+
/// Get slice collections from cache with database fallback
148
+
async fn get_slice_collections(&self, slice_uri: &str) -> Result<Option<HashSet<String>>, anyhow::Error> {
149
+
// Try cache first
150
+
let cache_result = {
151
+
let mut cache = self.collections_cache.lock().await;
152
+
cache.get_slice_collections(slice_uri).await
153
+
};
154
+
155
+
match cache_result {
156
+
Ok(Some(collections)) => Ok(Some(collections)),
157
+
Ok(None) => {
158
+
// Cache miss - load from database
159
+
match self.database.get_slice_collections_list(slice_uri).await {
160
+
Ok(collections) => {
161
+
let collections_set: HashSet<String> = collections.into_iter().collect();
162
+
// Cache the result
163
+
let _ = self.collections_cache.lock().await.cache_slice_collections(slice_uri, &collections_set).await;
164
+
Ok(Some(collections_set))
165
+
}
166
+
Err(e) => Err(e.into())
167
+
}
168
+
}
169
+
Err(e) => Err(e)
170
+
}
171
+
}
172
+
173
+
/// Get slice domain from cache with database fallback
174
+
async fn get_slice_domain(&self, slice_uri: &str) -> Result<Option<String>, anyhow::Error> {
175
+
// Try cache first
176
+
let cache_result = {
177
+
let mut cache = self.domain_cache.lock().await;
178
+
cache.get_slice_domain(slice_uri).await
179
+
};
180
+
181
+
match cache_result {
182
+
Ok(Some(domain)) => Ok(Some(domain)),
183
+
Ok(None) => {
184
+
// Cache miss - load from database
185
+
match self.database.get_slice_domain(slice_uri).await {
186
+
Ok(Some(domain)) => {
187
+
// Cache the result
188
+
let _ = self.domain_cache.lock().await.cache_slice_domain(slice_uri, &domain).await;
189
+
Ok(Some(domain))
190
+
}
191
+
Ok(None) => Ok(None),
192
+
Err(e) => Err(e.into())
193
+
}
194
+
}
195
+
Err(e) => Err(e)
196
+
}
197
+
}
198
+
199
+
/// Get slice lexicons from cache with database fallback
200
+
async fn get_slice_lexicons(&self, slice_uri: &str) -> Result<Option<Vec<serde_json::Value>>, anyhow::Error> {
201
+
// Try cache first
202
+
let cache_result = {
203
+
let mut cache = self.lexicon_cache.lock().await;
204
+
cache.get_lexicons(slice_uri).await
205
+
};
206
+
207
+
match cache_result {
208
+
Ok(Some(lexicons)) => Ok(Some(lexicons)),
209
+
Ok(None) => {
210
+
// Cache miss - load from database
211
+
match self.database.get_lexicons_by_slice(slice_uri).await {
212
+
Ok(lexicons) if !lexicons.is_empty() => {
213
+
// Cache the result
214
+
let _ = self.lexicon_cache.lock().await.cache_lexicons(slice_uri, &lexicons).await;
215
+
Ok(Some(lexicons))
216
+
}
217
+
Ok(_) => Ok(None), // Empty lexicons
218
+
Err(e) => Err(e.into())
219
+
}
220
+
}
221
+
Err(e) => Err(e)
222
+
}
223
+
}
224
async fn handle_commit_event(
225
&self,
226
did: &str,
227
commit: atproto_jetstream::JetstreamEventCommit,
228
) -> Result<()> {
229
+
// Get all slices from cached list
230
+
let slices = self.slices_list.read().await.clone();
231
+
232
+
// Process each slice
233
+
for slice_uri in slices {
234
+
// Get collections for this slice (with caching)
235
+
let collections = match self.get_slice_collections(&slice_uri).await {
236
+
Ok(Some(collections)) => collections,
237
+
Ok(None) => continue, // No collections for this slice
238
+
Err(e) => {
239
+
error!("Failed to get collections for slice {}: {}", slice_uri, e);
240
+
continue;
241
+
}
242
+
};
243
+
244
if collections.contains(&commit.collection) {
245
// Special handling for network.slices.lexicon records
246
// These should only be indexed to the slice specified in their JSON data
···
255
continue;
256
}
257
}
258
+
// Get the domain for this slice (with caching)
259
+
let domain = match self.get_slice_domain(&slice_uri).await {
260
+
Ok(Some(domain)) => domain,
261
+
Ok(None) => continue, // No domain, skip
262
+
Err(e) => {
263
+
error!("Failed to get domain for slice {}: {}", slice_uri, e);
264
+
continue;
265
+
}
266
};
267
+
268
// Check if this is a primary collection (starts with slice domain)
269
+
let is_primary_collection = commit.collection.starts_with(&domain);
270
+
271
// For external collections, check actor status BEFORE expensive validation
272
if !is_primary_collection {
273
+
let is_actor = match self.is_actor_cached(did, &slice_uri).await {
274
+
Ok(Some(cached_result)) => cached_result,
275
+
Ok(None) => {
276
// Cache miss means this DID is not an actor we've synced
277
// For external collections, we only care about actors we've already added
278
+
false
279
}
280
Err(e) => {
281
error!("Error checking actor status: {}", e);
282
continue;
283
}
284
+
};
285
+
286
+
if !is_actor {
287
+
// Not an actor - skip validation entirely for external collections
288
+
continue;
289
}
290
}
291
+
292
// Get lexicons for validation (after actor check for external collections)
293
+
let lexicons = match self.get_slice_lexicons(&slice_uri).await {
294
+
Ok(Some(lexicons)) => lexicons,
295
+
Ok(None) => {
296
+
info!("No lexicons found for slice {} - skipping validation", slice_uri);
297
+
continue;
298
+
}
299
+
Err(e) => {
300
+
error!("Failed to get lexicons for slice {}: {}", slice_uri, e);
301
+
continue;
302
}
303
};
304
+
305
// Validate the record against the slice's lexicons
306
let validation_result = match slices_lexicon::validate_record(lexicons.clone(), &commit.collection, commit.record.clone()) {
307
Ok(_) => {
···
309
true
310
}
311
Err(e) => {
312
+
let message = format!("Validation failed for collection {} in slice {}", commit.collection, slice_uri);
313
+
error!("✗ {}: {}", message, e);
314
+
Logger::global().log_jetstream_with_slice(LogLevel::Warn, &message, Some(serde_json::json!({
315
+
"collection": commit.collection,
316
+
"slice_uri": slice_uri,
317
+
"did": did
318
+
})), Some(&slice_uri));
319
+
false
320
}
321
};
322
+
323
if !validation_result {
324
continue; // Skip this slice if validation fails
325
}
326
+
327
if is_primary_collection {
328
// Primary collection - ensure actor exists and index ALL records
329
info!("✓ Primary collection {} for slice {} (domain: {}) - indexing record",
330
commit.collection, slice_uri, domain);
331
332
// Ensure actor exists for primary collections
333
+
let is_cached = matches!(self.is_actor_cached(did, &slice_uri).await, Ok(Some(_)));
334
335
if !is_cached {
336
// Actor not in cache - create it
···
351
error!("Failed to create actor {}: {}", did, e);
352
} else {
353
// Add to cache after successful database insert
354
+
self.cache_actor_exists(did, &slice_uri).await;
355
info!("✓ Created actor {} for slice {}", did, slice_uri);
356
}
357
}
···
360
}
361
}
362
}
363
+
364
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
365
+
366
let record = Record {
367
uri: uri.clone(),
368
cid: commit.cid.clone(),
···
372
indexed_at: Utc::now(),
373
slice_uri: Some(slice_uri.clone()),
374
};
375
+
376
match self.database.upsert_record(&record).await {
377
Ok(is_insert) => {
378
+
let message = if is_insert {
379
format!("Record inserted in {}", commit.collection)
380
+
} else {
381
format!("Record updated in {}", commit.collection)
382
};
383
let operation = if is_insert { "insert" } else { "update" };
···
387
"slice_uri": slice_uri,
388
"did": did,
389
"record_type": "primary"
390
+
})), Some(&slice_uri));
391
}
392
Err(e) => {
393
let message = "Failed to insert/update record";
···
398
"did": did,
399
"error": e.to_string(),
400
"record_type": "primary"
401
+
})), Some(&slice_uri));
402
return Err(anyhow::anyhow!("Database error: {}", e));
403
}
404
}
405
+
406
+
info!("✓ Successfully indexed {} record from primary collection: {}",
407
commit.operation, uri);
408
break;
409
} else {
410
// External collection - we already checked actor status, so just index
411
+
info!("✓ External collection {} - DID {} is actor in slice {} - indexing",
412
commit.collection, did, slice_uri);
413
+
414
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
415
+
416
let record = Record {
417
uri: uri.clone(),
418
cid: commit.cid.clone(),
···
422
indexed_at: Utc::now(),
423
slice_uri: Some(slice_uri.clone()),
424
};
425
+
426
match self.database.upsert_record(&record).await {
427
Ok(is_insert) => {
428
+
let message = if is_insert {
429
format!("Record inserted in {}", commit.collection)
430
+
} else {
431
format!("Record updated in {}", commit.collection)
432
};
433
let operation = if is_insert { "insert" } else { "update" };
···
437
"slice_uri": slice_uri,
438
"did": did,
439
"record_type": "external"
440
+
})), Some(&slice_uri));
441
}
442
Err(e) => {
443
let message = "Failed to insert/update record";
···
448
"did": did,
449
"error": e.to_string(),
450
"record_type": "external"
451
+
})), Some(&slice_uri));
452
return Err(anyhow::anyhow!("Database error: {}", e));
453
}
454
}
455
+
456
+
info!("✓ Successfully indexed {} record from external collection: {}",
457
commit.operation, uri);
458
break;
459
}
460
}
461
}
462
+
463
Ok(())
464
}
465
···
470
) -> Result<()> {
471
let uri = format!("at://{}/{}/{}", did, commit.collection, commit.rkey);
472
473
+
// Get all slices from cached list
474
+
let slices = self.slices_list.read().await.clone();
475
476
let mut relevant_slices: Vec<String> = Vec::new();
477
478
+
for slice_uri in slices {
479
+
// Get collections for this slice (with caching)
480
+
let collections = match self.get_slice_collections(&slice_uri).await {
481
+
Ok(Some(collections)) => collections,
482
+
Ok(None) => continue, // No collections for this slice
483
+
Err(e) => {
484
+
error!("Failed to get collections for slice {}: {}", slice_uri, e);
485
+
continue;
486
+
}
487
+
};
488
+
489
if !collections.contains(&commit.collection) {
490
continue;
491
}
492
493
+
// Get the domain for this slice (with caching)
494
+
let domain = match self.get_slice_domain(&slice_uri).await {
495
+
Ok(Some(domain)) => domain,
496
+
Ok(None) => continue, // No domain, skip
497
+
Err(e) => {
498
+
error!("Failed to get domain for slice {}: {}", slice_uri, e);
499
+
continue;
500
+
}
501
};
502
503
// Check if this is a primary collection (starts with slice domain)
504
+
let is_primary_collection = commit.collection.starts_with(&domain);
505
506
if is_primary_collection {
507
// Primary collection - always process deletes
508
relevant_slices.push(slice_uri.clone());
509
} else {
510
// External collection - only process if DID is an actor in this slice
511
+
let is_actor = match self.is_actor_cached(did, &slice_uri).await {
512
+
Ok(Some(cached_result)) => cached_result,
513
+
_ => false,
514
+
};
515
+
if is_actor {
516
relevant_slices.push(slice_uri.clone());
517
}
518
}
···
557
if deleted > 0 {
558
info!("✓ Cleaned up actor {} from slice {} (no records remaining)", did, slice_uri);
559
// Remove from cache
560
+
self.remove_actor_from_cache(did, slice_uri).await;
561
}
562
}
563
Err(e) => {
···
601
}
602
603
impl JetstreamConsumer {
604
+
/// Create a new Jetstream consumer with optional cursor support and Redis cache
605
///
606
/// # Arguments
607
/// * `database` - Database connection for slice configurations and record storage
608
/// * `jetstream_hostname` - Optional custom jetstream hostname
609
/// * `cursor_handler` - Optional cursor handler for resumable event processing
610
/// * `initial_cursor` - Optional starting cursor position (time_us) to resume from
611
+
/// * `redis_url` - Optional Redis URL for caching (falls back to in-memory if not provided)
612
pub async fn new(
613
database: Database,
614
jetstream_hostname: Option<String>,
615
cursor_handler: Option<Arc<PostgresCursorHandler>>,
616
initial_cursor: Option<i64>,
617
+
redis_url: Option<String>,
618
) -> Result<Self, SliceError> {
619
let config = ConsumerTaskConfig {
620
user_agent: "slice-server/1.0".to_string(),
···
632
let consumer = Consumer::new(config);
633
let http_client = Client::new();
634
635
+
// Determine cache backend based on Redis URL
636
+
let cache_backend = if let Some(redis_url) = redis_url {
637
+
CacheBackend::Redis { url: redis_url, ttl_seconds: None }
638
+
} else {
639
+
CacheBackend::InMemory { ttl_seconds: None }
640
+
};
641
+
642
+
// Create cache instances
643
+
let actor_cache = Arc::new(Mutex::new(
644
+
CacheFactory::create_slice_cache(cache_backend.clone()).await
645
+
.map_err(|e| SliceError::JetstreamError {
646
+
message: format!("Failed to create actor cache: {}", e)
647
+
})?
648
+
));
649
+
650
+
let lexicon_cache = Arc::new(Mutex::new(
651
+
CacheFactory::create_slice_cache(cache_backend.clone()).await
652
+
.map_err(|e| SliceError::JetstreamError {
653
+
message: format!("Failed to create lexicon cache: {}", e)
654
+
})?
655
+
));
656
+
657
+
let domain_cache = Arc::new(Mutex::new(
658
+
CacheFactory::create_slice_cache(cache_backend.clone()).await
659
+
.map_err(|e| SliceError::JetstreamError {
660
+
message: format!("Failed to create domain cache: {}", e)
661
+
})?
662
+
));
663
+
664
+
let collections_cache = Arc::new(Mutex::new(
665
+
CacheFactory::create_slice_cache(cache_backend).await
666
+
.map_err(|e| SliceError::JetstreamError {
667
+
message: format!("Failed to create collections cache: {}", e)
668
+
})?
669
+
));
670
+
671
Ok(Self {
672
consumer,
673
database,
674
http_client,
675
+
actor_cache,
676
+
lexicon_cache,
677
+
domain_cache,
678
+
collections_cache,
679
event_count: Arc::new(std::sync::atomic::AtomicU64::new(0)),
680
cursor_handler,
681
+
slices_list: Arc::new(RwLock::new(Vec::new())),
682
})
683
}
684
685
+
/// Load slice configurations
686
pub async fn load_slice_configurations(&self) -> Result<(), SliceError> {
687
+
info!("Jetstream consumer now uses on-demand loading with caching");
688
689
+
// Get all slices and update cached list
690
+
let slices = self.database.get_all_slices().await?;
691
+
*self.slices_list.write().await = slices.clone();
692
+
info!("Found {} total slices in database - data will be loaded on-demand", slices.len());
693
694
Ok(())
695
}
696
697
/// Preload actor cache to avoid database hits during event processing
698
async fn preload_actor_cache(&self) -> Result<(), SliceError> {
699
info!("Preloading actor cache...");
700
+
701
let actors = self.database.get_all_actors().await?;
702
info!("Found {} actors to cache", actors.len());
703
+
704
+
match self.actor_cache.lock().await.preload_actors(actors).await {
705
+
Ok(_) => {
706
+
info!("✓ Actor cache preloaded successfully");
707
+
Ok(())
708
+
}
709
+
Err(e) => {
710
+
warn!(error = ?e, "Failed to preload actors to cache");
711
+
Ok(()) // Don't fail startup if preload fails
712
+
}
713
}
714
}
715
+
716
717
/// Start consuming events from Jetstream
718
pub async fn start_consuming(&self, cancellation_token: CancellationToken) -> Result<(), SliceError> {
719
info!("Starting Jetstream consumer");
720
+
721
// Load initial slice configurations
722
self.load_slice_configurations().await?;
723
+
724
// Preload actor cache
725
self.preload_actor_cache().await?;
726
+
727
// Create and register the event handler
728
let handler = Arc::new(SliceEventHandler {
729
database: self.database.clone(),
730
http_client: self.http_client.clone(),
731
event_count: self.event_count.clone(),
732
actor_cache: self.actor_cache.clone(),
733
+
lexicon_cache: self.lexicon_cache.clone(),
734
+
domain_cache: self.domain_cache.clone(),
735
+
collections_cache: self.collections_cache.clone(),
736
cursor_handler: self.cursor_handler.clone(),
737
+
slices_list: self.slices_list.clone(),
738
});
739
+
740
self.consumer.register_handler(handler).await
741
.map_err(|e| SliceError::JetstreamError {
742
message: format!("Failed to register event handler: {}", e),
743
})?;
744
+
745
// Start periodic status reporting
746
let event_count_for_status = self.event_count.clone();
747
tokio::spawn(async move {
···
752
info!("Jetstream consumer status: {} total events processed", count);
753
}
754
});
755
+
756
// Start the consumer
757
info!("Starting Jetstream background consumer...");
758
let result = self.consumer.run_background(cancellation_token).await
···
777
pub fn start_configuration_reloader(consumer: Arc<Self>) {
778
tokio::spawn(async move {
779
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // Reload every 5 minutes
780
+
interval.tick().await; // Skip first immediate tick
781
+
782
loop {
783
interval.tick().await;
784
+
785
if let Err(e) = consumer.load_slice_configurations().await {
786
error!("Failed to reload slice configurations: {}", e);
787
}
788
+
789
if let Err(e) = consumer.preload_actor_cache().await {
790
error!("Failed to reload actor cache: {}", e);
791
}
792
}
793
});
794
}
795
+
}
+18
-6
api/src/jobs.rs
+18
-6
api/src/jobs.rs
···
5
use crate::sync::SyncService;
6
use crate::models::BulkSyncParams;
7
use crate::logging::LogLevel;
8
use serde_json::json;
9
use tracing::{info, error};
10
11
/// Payload for sync jobs
12
#[derive(Debug, Clone, Serialize, Deserialize)]
···
65
}))
66
);
67
68
-
// Create sync service with logging
69
let database = crate::database::Database::from_pool(pool.clone());
70
let relay_endpoint = std::env::var("RELAY_ENDPOINT")
71
.unwrap_or_else(|_| "https://relay1.us-west.bsky.network".to_string());
72
-
let sync_service = SyncService::with_logging(
73
-
database.clone(),
74
-
relay_endpoint,
75
-
logger.clone(),
76
payload.job_id,
77
-
payload.user_did.clone()
78
);
79
80
// Track progress
···
5
use crate::sync::SyncService;
6
use crate::models::BulkSyncParams;
7
use crate::logging::LogLevel;
8
+
use crate::cache;
9
use serde_json::json;
10
use tracing::{info, error};
11
+
use std::sync::Arc;
12
+
use tokio::sync::Mutex;
13
14
/// Payload for sync jobs
15
#[derive(Debug, Clone, Serialize, Deserialize)]
···
68
}))
69
);
70
71
+
// Create sync service with logging and cache
72
let database = crate::database::Database::from_pool(pool.clone());
73
let relay_endpoint = std::env::var("RELAY_ENDPOINT")
74
.unwrap_or_else(|_| "https://relay1.us-west.bsky.network".to_string());
75
+
76
+
// Create cache for DID resolution (24 hour TTL)
77
+
let cache = Arc::new(Mutex::new(
78
+
cache::CacheFactory::create_slice_cache(
79
+
cache::CacheBackend::InMemory { ttl_seconds: Some(24 * 60 * 60) }
80
+
).await?
81
+
));
82
+
83
+
let sync_service = SyncService::with_logging_and_cache(
84
+
database.clone(),
85
+
relay_endpoint,
86
+
logger.clone(),
87
payload.job_id,
88
+
payload.user_did.clone(),
89
+
cache
90
);
91
92
// Track progress
+19
-1
api/src/main.rs
+19
-1
api/src/main.rs
···
2
mod api;
3
mod atproto_extensions;
4
mod auth;
5
mod database;
6
mod errors;
7
mod jetstream;
···
9
mod jobs;
10
mod logging;
11
mod models;
12
mod sync;
13
mod xrpc;
14
···
20
use std::env;
21
use std::sync::Arc;
22
use std::sync::atomic::AtomicBool;
23
use tower_http::{cors::CorsLayer, trace::TraceLayer};
24
use tracing::info;
25
···
42
database_pool: PgPool,
43
config: Config,
44
pub jetstream_connected: Arc<AtomicBool>,
45
}
46
47
#[tokio::main]
···
126
let jetstream_connected_clone = jetstream_connected.clone();
127
tokio::spawn(async move {
128
let jetstream_hostname = env::var("JETSTREAM_HOSTNAME").ok();
129
let cursor_write_interval = env::var("JETSTREAM_CURSOR_WRITE_INTERVAL_SECS")
130
.unwrap_or_else(|_| "5".to_string())
131
.parse::<u64>()
···
179
cursor_write_interval,
180
));
181
182
-
// Create consumer with cursor support
183
let consumer_result = JetstreamConsumer::new(
184
database_for_jetstream.clone(),
185
jetstream_hostname.clone(),
186
Some(cursor_handler.clone()),
187
initial_cursor,
188
).await;
189
190
let consumer_arc = match consumer_result {
···
231
}
232
});
233
234
let state = AppState {
235
database: database.clone(),
236
database_pool: pool,
237
config,
238
jetstream_connected,
239
};
240
241
// Build application with routes
···
2
mod api;
3
mod atproto_extensions;
4
mod auth;
5
+
mod cache;
6
mod database;
7
mod errors;
8
mod jetstream;
···
10
mod jobs;
11
mod logging;
12
mod models;
13
+
mod redis_cache;
14
mod sync;
15
mod xrpc;
16
···
22
use std::env;
23
use std::sync::Arc;
24
use std::sync::atomic::AtomicBool;
25
+
use tokio::sync::Mutex;
26
use tower_http::{cors::CorsLayer, trace::TraceLayer};
27
use tracing::info;
28
···
45
database_pool: PgPool,
46
config: Config,
47
pub jetstream_connected: Arc<AtomicBool>,
48
+
pub auth_cache: Arc<Mutex<cache::SliceCache>>,
49
}
50
51
#[tokio::main]
···
130
let jetstream_connected_clone = jetstream_connected.clone();
131
tokio::spawn(async move {
132
let jetstream_hostname = env::var("JETSTREAM_HOSTNAME").ok();
133
+
let redis_url = env::var("REDIS_URL").ok();
134
let cursor_write_interval = env::var("JETSTREAM_CURSOR_WRITE_INTERVAL_SECS")
135
.unwrap_or_else(|_| "5".to_string())
136
.parse::<u64>()
···
184
cursor_write_interval,
185
));
186
187
+
// Create consumer with cursor support and Redis cache
188
let consumer_result = JetstreamConsumer::new(
189
database_for_jetstream.clone(),
190
jetstream_hostname.clone(),
191
Some(cursor_handler.clone()),
192
initial_cursor,
193
+
redis_url.clone(),
194
).await;
195
196
let consumer_arc = match consumer_result {
···
237
}
238
});
239
240
+
// Create auth cache for token/session caching (5 minute TTL)
241
+
let redis_url = env::var("REDIS_URL").ok();
242
+
let auth_cache_backend = if let Some(redis_url) = redis_url {
243
+
cache::CacheBackend::Redis { url: redis_url, ttl_seconds: Some(300) }
244
+
} else {
245
+
cache::CacheBackend::InMemory { ttl_seconds: Some(300) }
246
+
};
247
+
let auth_cache = Arc::new(Mutex::new(
248
+
cache::CacheFactory::create_slice_cache(auth_cache_backend).await?
249
+
));
250
+
251
let state = AppState {
252
database: database.clone(),
253
database_pool: pool,
254
config,
255
jetstream_connected,
256
+
auth_cache,
257
};
258
259
// Build application with routes
+259
api/src/redis_cache.rs
+259
api/src/redis_cache.rs
···
···
1
+
use redis::{Client, AsyncCommands};
2
+
use redis::aio::ConnectionManager;
3
+
use anyhow::Result;
4
+
use tracing::{debug, error, warn};
5
+
use serde::{Serialize, Deserialize};
6
+
use async_trait::async_trait;
7
+
use crate::cache::Cache;
8
+
9
+
/// Generic Redis cache for scalable caching across multiple instances
10
+
pub struct RedisCache {
11
+
conn: ConnectionManager,
12
+
default_ttl_seconds: u64,
13
+
}
14
+
15
+
impl RedisCache {
16
+
/// Create a new Redis cache
17
+
///
18
+
/// # Arguments
19
+
/// * `redis_url` - Redis connection URL (e.g., "redis://localhost:6379")
20
+
/// * `default_ttl_seconds` - Default time-to-live for cache entries (default: 3600 = 1 hour)
21
+
pub async fn new(redis_url: &str, default_ttl_seconds: Option<u64>) -> Result<Self> {
22
+
let client = Client::open(redis_url)?;
23
+
let conn = ConnectionManager::new(client).await?;
24
+
25
+
Ok(Self {
26
+
conn,
27
+
default_ttl_seconds: default_ttl_seconds.unwrap_or(3600),
28
+
})
29
+
}
30
+
31
+
/// Get a value from cache
32
+
///
33
+
/// Returns:
34
+
/// - Some(T) if key exists and can be deserialized
35
+
/// - None if key doesn't exist or deserialization fails
36
+
pub async fn get_value<T>(&mut self, key: &str) -> Result<Option<T>>
37
+
where
38
+
T: for<'de> Deserialize<'de>,
39
+
{
40
+
match self.conn.get::<_, Option<String>>(key).await {
41
+
Ok(Some(value)) => {
42
+
match serde_json::from_str::<T>(&value) {
43
+
Ok(parsed) => {
44
+
// Cache hit - no logging needed
45
+
Ok(Some(parsed))
46
+
}
47
+
Err(e) => {
48
+
error!(
49
+
error = ?e,
50
+
cache_key = %key,
51
+
"Failed to deserialize cached value"
52
+
);
53
+
// Remove corrupted entry
54
+
let _ = self.conn.del::<_, ()>(key).await;
55
+
Ok(None)
56
+
}
57
+
}
58
+
}
59
+
Ok(None) => {
60
+
// Cache miss - no logging needed
61
+
Ok(None)
62
+
}
63
+
Err(e) => {
64
+
error!(
65
+
error = ?e,
66
+
cache_key = %key,
67
+
"Redis error during get"
68
+
);
69
+
// Return cache miss on Redis error
70
+
Ok(None)
71
+
}
72
+
}
73
+
}
74
+
75
+
/// Set a value in cache with optional TTL
76
+
pub async fn set_value<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
77
+
where
78
+
T: Serialize,
79
+
{
80
+
let ttl = ttl_seconds.unwrap_or(self.default_ttl_seconds);
81
+
82
+
match serde_json::to_string(value) {
83
+
Ok(serialized) => {
84
+
match self.conn.set_ex::<_, _, ()>(key, serialized, ttl).await {
85
+
Ok(_) => {
86
+
debug!(
87
+
cache_key = %key,
88
+
ttl_seconds = ttl,
89
+
"Cached value in Redis"
90
+
);
91
+
Ok(())
92
+
}
93
+
Err(e) => {
94
+
error!(
95
+
error = ?e,
96
+
cache_key = %key,
97
+
"Failed to cache value in Redis"
98
+
);
99
+
// Don't fail the operation if Redis is down
100
+
Ok(())
101
+
}
102
+
}
103
+
}
104
+
Err(e) => {
105
+
error!(
106
+
error = ?e,
107
+
cache_key = %key,
108
+
"Failed to serialize value for caching"
109
+
);
110
+
Ok(())
111
+
}
112
+
}
113
+
}
114
+
115
+
/// Check if a key exists in cache
116
+
pub async fn key_exists(&mut self, key: &str) -> Result<bool> {
117
+
match self.conn.exists(key).await {
118
+
Ok(exists) => {
119
+
debug!(cache_key = %key, exists = exists, "Redis exists check");
120
+
Ok(exists)
121
+
}
122
+
Err(e) => {
123
+
error!(
124
+
error = ?e,
125
+
cache_key = %key,
126
+
"Redis error during exists check"
127
+
);
128
+
Ok(false)
129
+
}
130
+
}
131
+
}
132
+
133
+
/// Delete a key from cache
134
+
pub async fn delete_key(&mut self, key: &str) -> Result<()> {
135
+
match self.conn.del::<_, ()>(key).await {
136
+
Ok(_) => {
137
+
debug!(cache_key = %key, "Deleted key from Redis cache");
138
+
Ok(())
139
+
}
140
+
Err(e) => {
141
+
error!(
142
+
error = ?e,
143
+
cache_key = %key,
144
+
"Failed to delete key from Redis cache"
145
+
);
146
+
Ok(())
147
+
}
148
+
}
149
+
}
150
+
151
+
/// Set multiple key-value pairs using pipeline for efficiency
152
+
pub async fn set_multiple_values<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
153
+
where
154
+
T: Serialize,
155
+
{
156
+
if items.is_empty() {
157
+
return Ok(());
158
+
}
159
+
160
+
let mut pipe = redis::pipe();
161
+
let mut serialization_errors = 0;
162
+
163
+
for (key, value, ttl) in &items {
164
+
match serde_json::to_string(value) {
165
+
Ok(serialized) => {
166
+
let ttl_to_use = ttl.unwrap_or(self.default_ttl_seconds);
167
+
pipe.set_ex(key, serialized, ttl_to_use);
168
+
}
169
+
Err(e) => {
170
+
error!(
171
+
error = ?e,
172
+
cache_key = %key,
173
+
"Failed to serialize value for bulk caching"
174
+
);
175
+
serialization_errors += 1;
176
+
}
177
+
}
178
+
}
179
+
180
+
match pipe.query_async::<()>(&mut self.conn).await {
181
+
Ok(_) => {
182
+
debug!(
183
+
items_count = items.len() - serialization_errors,
184
+
serialization_errors = serialization_errors,
185
+
"Successfully bulk cached items in Redis"
186
+
);
187
+
Ok(())
188
+
}
189
+
Err(e) => {
190
+
error!(
191
+
error = ?e,
192
+
items_count = items.len(),
193
+
"Failed to bulk cache items in Redis"
194
+
);
195
+
Ok(())
196
+
}
197
+
}
198
+
}
199
+
200
+
/// Test Redis connection
201
+
pub async fn ping(&mut self) -> Result<bool> {
202
+
match self.conn.ping::<String>().await {
203
+
Ok(response) => Ok(response == "PONG"),
204
+
Err(e) => {
205
+
error!(error = ?e, "Redis ping failed");
206
+
Ok(false)
207
+
}
208
+
}
209
+
}
210
+
211
+
/// Get cache statistics (for monitoring)
212
+
pub async fn get_info(&mut self) -> Result<String> {
213
+
match redis::cmd("INFO").arg("memory").query_async::<String>(&mut self.conn).await {
214
+
Ok(info) => Ok(info),
215
+
Err(e) => {
216
+
warn!(error = ?e, "Failed to get Redis info");
217
+
Ok("Redis info unavailable".to_string())
218
+
}
219
+
}
220
+
}
221
+
}
222
+
223
+
#[async_trait]
224
+
impl Cache for RedisCache {
225
+
async fn get<T>(&mut self, key: &str) -> Result<Option<T>>
226
+
where
227
+
T: for<'de> Deserialize<'de> + Send,
228
+
{
229
+
self.get_value(key).await
230
+
}
231
+
232
+
async fn set<T>(&mut self, key: &str, value: &T, ttl_seconds: Option<u64>) -> Result<()>
233
+
where
234
+
T: Serialize + Send + Sync,
235
+
{
236
+
self.set_value(key, value, ttl_seconds).await
237
+
}
238
+
239
+
240
+
async fn delete(&mut self, key: &str) -> Result<()> {
241
+
self.delete_key(key).await
242
+
}
243
+
244
+
async fn set_multiple<T>(&mut self, items: Vec<(&str, &T, Option<u64>)>) -> Result<()>
245
+
where
246
+
T: Serialize + Send + Sync,
247
+
{
248
+
self.set_multiple_values(items).await
249
+
}
250
+
251
+
async fn ping(&mut self) -> Result<bool> {
252
+
RedisCache::ping(self).await
253
+
}
254
+
255
+
async fn get_info(&mut self) -> Result<String> {
256
+
RedisCache::get_info(self).await
257
+
}
258
+
}
259
+
+34
-29
api/src/sync.rs
+34
-29
api/src/sync.rs
···
8
resolve::{resolve_subject, HickoryDnsResolver},
9
};
10
11
-
use crate::actor_resolver::resolve_actor_data;
12
use crate::database::Database;
13
use crate::errors::SyncError;
14
use crate::models::{Actor, Record};
15
use crate::logging::LogLevel;
16
use crate::logging::Logger;
17
use serde_json::json;
18
use uuid::Uuid;
19
20
···
66
client: Client,
67
database: Database,
68
relay_endpoint: String,
69
-
atp_cache: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<String, AtpData>>>,
70
logger: Option<Logger>,
71
job_id: Option<Uuid>,
72
user_did: Option<String>,
73
}
74
75
impl SyncService {
76
-
pub fn new(database: Database, relay_endpoint: String) -> Self {
77
Self {
78
client: Client::new(),
79
database,
80
relay_endpoint,
81
-
atp_cache: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
82
logger: None,
83
job_id: None,
84
user_did: None,
85
}
86
}
87
88
-
/// Create a new SyncService with logging enabled for a specific job
89
-
pub fn with_logging(database: Database, relay_endpoint: String, logger: Logger, job_id: Uuid, user_did: String) -> Self {
90
Self {
91
client: Client::new(),
92
database,
93
relay_endpoint,
94
-
atp_cache: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
95
logger: Some(logger),
96
job_id: Some(job_id),
97
user_did: Some(user_did),
···
126
127
let all_collections = [&primary_collections[..], &external_collections[..]].concat();
128
129
-
// Clear cache at start of each backfill operation
130
-
self.clear_atp_cache();
131
132
let all_repos = if let Some(provided_repos) = repos {
133
info!("📋 Using {} provided repositories", provided_repos.len());
···
474
475
async fn fetch_records_for_repo_collection_with_atp_map(&self, repo: &str, collection: &str, atp_map: &std::collections::HashMap<String, AtpData>, slice_uri: &str) -> Result<Vec<Record>, SyncError> {
476
let atp_data = atp_map.get(repo).ok_or_else(|| SyncError::Generic(format!("No ATP data found for repo: {}", repo)))?;
477
-
self.fetch_records_for_repo_collection(repo, collection, &atp_data.pds, slice_uri).await
478
}
479
480
async fn fetch_records_for_repo_collection(&self, repo: &str, collection: &str, pds_url: &str, slice_uri: &str) -> Result<Vec<Record>, SyncError> {
···
605
async fn resolve_atp_data(&self, did: &str) -> Result<AtpData, SyncError> {
606
debug!("Resolving ATP data for DID: {}", did);
607
608
-
{
609
-
let cache = self.atp_cache.lock().unwrap();
610
-
if let Some(cached_data) = cache.get(did) {
611
-
debug!("Using cached ATP data for DID: {}", did);
612
-
return Ok(cached_data.clone());
613
-
}
614
-
}
615
-
616
let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
617
618
match resolve_subject(&self.client, &dns_resolver, did).await {
619
Ok(resolved_did) => {
620
debug!("Successfully resolved subject: {}", resolved_did);
621
622
-
let actor_data = resolve_actor_data(&self.client, &resolved_did).await
623
.map_err(|e| SyncError::Generic(e.to_string()))?;
624
625
let atp_data = AtpData {
···
628
handle: actor_data.handle,
629
};
630
631
-
// Cache the result
632
-
{
633
-
let mut cache = self.atp_cache.lock().unwrap();
634
-
cache.insert(did.to_string(), atp_data.clone());
635
-
}
636
-
637
Ok(atp_data)
638
}
639
Err(e) => {
···
664
Ok(())
665
}
666
667
-
pub fn clear_atp_cache(&self) {
668
-
let mut cache = self.atp_cache.lock().unwrap();
669
-
cache.clear();
670
-
}
671
672
/// Get external collections for a slice (collections that don't start with the slice's domain)
673
async fn get_external_collections_for_slice(&self, slice_uri: &str) -> Result<Vec<String>, SyncError> {
···
8
resolve::{resolve_subject, HickoryDnsResolver},
9
};
10
11
+
use crate::actor_resolver::{resolve_actor_data_cached, resolve_actor_data_with_retry};
12
+
use crate::cache::SliceCache;
13
use crate::database::Database;
14
use crate::errors::SyncError;
15
use crate::models::{Actor, Record};
16
use crate::logging::LogLevel;
17
use crate::logging::Logger;
18
use serde_json::json;
19
+
use std::sync::Arc;
20
+
use tokio::sync::Mutex;
21
use uuid::Uuid;
22
23
···
69
client: Client,
70
database: Database,
71
relay_endpoint: String,
72
+
cache: Option<Arc<Mutex<SliceCache>>>,
73
logger: Option<Logger>,
74
job_id: Option<Uuid>,
75
user_did: Option<String>,
76
}
77
78
impl SyncService {
79
+
80
+
pub fn with_cache(database: Database, relay_endpoint: String, cache: Arc<Mutex<SliceCache>>) -> Self {
81
Self {
82
client: Client::new(),
83
database,
84
relay_endpoint,
85
+
cache: Some(cache),
86
logger: None,
87
job_id: None,
88
user_did: None,
89
}
90
}
91
92
+
93
+
/// Create a new SyncService with logging and cache enabled for a specific job
94
+
pub fn with_logging_and_cache(database: Database, relay_endpoint: String, logger: Logger, job_id: Uuid, user_did: String, cache: Arc<Mutex<SliceCache>>) -> Self {
95
Self {
96
client: Client::new(),
97
database,
98
relay_endpoint,
99
+
cache: Some(cache),
100
logger: Some(logger),
101
job_id: Some(job_id),
102
user_did: Some(user_did),
···
131
132
let all_collections = [&primary_collections[..], &external_collections[..]].concat();
133
134
+
// DID resolution cache is now handled by SliceCache
135
136
let all_repos = if let Some(provided_repos) = repos {
137
info!("📋 Using {} provided repositories", provided_repos.len());
···
478
479
async fn fetch_records_for_repo_collection_with_atp_map(&self, repo: &str, collection: &str, atp_map: &std::collections::HashMap<String, AtpData>, slice_uri: &str) -> Result<Vec<Record>, SyncError> {
480
let atp_data = atp_map.get(repo).ok_or_else(|| SyncError::Generic(format!("No ATP data found for repo: {}", repo)))?;
481
+
482
+
match self.fetch_records_for_repo_collection(repo, collection, &atp_data.pds, slice_uri).await {
483
+
Ok(records) => Ok(records),
484
+
Err(SyncError::ListRecords { status }) if (400..600).contains(&status) => {
485
+
// 4xx/5xx error from PDS - try invalidating cache and retrying once
486
+
debug!("PDS error {} for repo {}, attempting cache invalidation and retry", status, repo);
487
+
488
+
match resolve_actor_data_with_retry(&self.client, repo, self.cache.clone(), true).await {
489
+
Ok(fresh_actor_data) => {
490
+
debug!("Successfully re-resolved actor data for {}, retrying with PDS: {}", repo, fresh_actor_data.pds);
491
+
self.fetch_records_for_repo_collection(repo, collection, &fresh_actor_data.pds, slice_uri).await
492
+
}
493
+
Err(e) => {
494
+
debug!("Failed to re-resolve actor data for {}: {:?}", repo, e);
495
+
Err(SyncError::ListRecords { status }) // Return original error
496
+
}
497
+
}
498
+
}
499
+
Err(e) => Err(e), // Other errors (network, etc.) - don't retry
500
+
}
501
}
502
503
async fn fetch_records_for_repo_collection(&self, repo: &str, collection: &str, pds_url: &str, slice_uri: &str) -> Result<Vec<Record>, SyncError> {
···
628
async fn resolve_atp_data(&self, did: &str) -> Result<AtpData, SyncError> {
629
debug!("Resolving ATP data for DID: {}", did);
630
631
let dns_resolver = HickoryDnsResolver::create_resolver(&[]);
632
633
match resolve_subject(&self.client, &dns_resolver, did).await {
634
Ok(resolved_did) => {
635
debug!("Successfully resolved subject: {}", resolved_did);
636
637
+
let actor_data = resolve_actor_data_cached(&self.client, &resolved_did, self.cache.clone()).await
638
.map_err(|e| SyncError::Generic(e.to_string()))?;
639
640
let atp_data = AtpData {
···
643
handle: actor_data.handle,
644
};
645
646
Ok(atp_data)
647
}
648
Err(e) => {
···
673
Ok(())
674
}
675
676
677
/// Get external collections for a slice (collections that don't start with the slice's domain)
678
async fn get_external_collections_for_slice(&self, slice_uri: &str) -> Result<Vec<String>, SyncError> {
+2
-2
api/src/xrpc/com/atproto/repo/upload_blob.rs
+2
-2
api/src/xrpc/com/atproto/repo/upload_blob.rs
···
9
let headers = request.headers().clone();
10
11
let token = auth::extract_bearer_token(&headers)?;
12
-
let _user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
13
14
let (dpop_auth, pds_url) =
15
-
auth::get_atproto_auth_for_user(&token, &state.config.auth_base_url).await?;
16
17
let mime_type = headers
18
.get("content-type")
···
9
let headers = request.headers().clone();
10
11
let token = auth::extract_bearer_token(&headers)?;
12
+
let _user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?;
13
14
let (dpop_auth, pds_url) =
15
+
auth::get_atproto_auth_for_user_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?;
16
17
let mime_type = headers
18
.get("content-type")
+1
-1
api/src/xrpc/network/slices/slice/create_oauth_client.rs
+1
-1
api/src/xrpc/network/slices/slice/create_oauth_client.rs
+1
-1
api/src/xrpc/network/slices/slice/delete_oauth_client.rs
+1
-1
api/src/xrpc/network/slices/slice/delete_oauth_client.rs
+1
-1
api/src/xrpc/network/slices/slice/get_oauth_clients.rs
+1
-1
api/src/xrpc/network/slices/slice/get_oauth_clients.rs
+1
-1
api/src/xrpc/network/slices/slice/start_sync.rs
+1
-1
api/src/xrpc/network/slices/slice/start_sync.rs
···
24
Json(params): Json<Params>,
25
) -> Result<Json<Output>, AppError> {
26
let token = auth::extract_bearer_token(&headers)?;
27
+
let user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?;
28
29
let user_did = user_info.sub;
30
let slice_uri = params.slice;
+2
-2
api/src/xrpc/network/slices/slice/sync_user_collections.rs
+2
-2
api/src/xrpc/network/slices/slice/sync_user_collections.rs
···
20
Json(params): Json<Params>,
21
) -> Result<Json<crate::sync::SyncUserCollectionsResult>, AppError> {
22
let token = auth::extract_bearer_token(&headers)?;
23
-
let user_info = auth::verify_oauth_token(&token, &state.config.auth_base_url).await?;
24
25
let user_did = user_info.did.unwrap_or(user_info.sub);
26
···
38
);
39
40
let sync_service =
41
-
crate::sync::SyncService::new(state.database.clone(), state.config.relay_endpoint.clone());
42
43
let result = sync_service
44
.sync_user_collections(&user_did, ¶ms.slice, params.timeout_seconds)
···
20
Json(params): Json<Params>,
21
) -> Result<Json<crate::sync::SyncUserCollectionsResult>, AppError> {
22
let token = auth::extract_bearer_token(&headers)?;
23
+
let user_info = auth::verify_oauth_token_cached(&token, &state.config.auth_base_url, Some(state.auth_cache.clone())).await?;
24
25
let user_did = user_info.did.unwrap_or(user_info.sub);
26
···
38
);
39
40
let sync_service =
41
+
crate::sync::SyncService::with_cache(state.database.clone(), state.config.relay_endpoint.clone(), state.auth_cache.clone());
42
43
let result = sync_service
44
.sync_user_collections(&user_did, ¶ms.slice, params.timeout_seconds)
+1
-1
api/src/xrpc/network/slices/slice/update_oauth_client.rs
+1
-1
api/src/xrpc/network/slices/slice/update_oauth_client.rs
+13
docker-compose.yml
+13
docker-compose.yml
···
28
networks:
29
- default
30
31
+
redis:
32
+
image: redis:7-alpine
33
+
ports:
34
+
- "6379:6379"
35
+
volumes:
36
+
- redis_data:/data
37
+
healthcheck:
38
+
test: ["CMD", "redis-cli", "ping"]
39
+
interval: 5s
40
+
timeout: 3s
41
+
retries: 5
42
+
43
aip:
44
image: ghcr.io/bigmoves/aip/aip-sqlite:main-e445b82
45
environment:
···
66
67
volumes:
68
postgres_data:
69
+
redis_data:
70
aip_data: