| 249 | extensions: [ |
| 250 | new Database({ |
| 251 | async fetch({ documentName }) { |
| 252 | const boardUuid = extractBoardUuid(documentName) |
| 253 | if (!boardUuid) return null |
| 254 | |
| 255 | // 1. Try Redis first |
| 256 | try { |
| 257 | const r = getRedis() |
| 258 | const cached = await r.getBuffer(redisYdocKey(boardUuid)) |
| 259 | if (cached && cached.byteLength > 0) { |
| 260 | console.log( |
| 261 | `[collab] Redis hit for ${boardUuid}: ${cached.byteLength} bytes`, |
| 262 | ) |
| 263 | return new Uint8Array(cached) |
| 264 | } |
| 265 | } catch (err) { |
| 266 | console.error( |
| 267 | `[collab] Redis fetch error for ${boardUuid}:`, |
| 268 | err, |
| 269 | ) |
| 270 | } |
| 271 | |
| 272 | // 2. Fall back to database (with timeout) |
| 273 | try { |
| 274 | const url = `${API_URL}/api/v1/boards/${boardUuid}/ydoc` |
| 275 | console.log(`[collab] Fetching ydoc from DB for ${boardUuid}`) |
| 276 | const response = await fetchWithTimeout(url, { |
| 277 | headers: { |
| 278 | 'X-Internal-Key': INTERNAL_KEY, |
| 279 | }, |
| 280 | }) |
| 281 | |
| 282 | if (!response.ok) { |
| 283 | console.error( |
| 284 | `[collab] Failed to fetch ydoc for ${boardUuid}: ${response.status} ${response.statusText}`, |
| 285 | ) |
| 286 | return null |
| 287 | } |
| 288 | |
| 289 | const buffer = await response.arrayBuffer() |
| 290 | if (buffer.byteLength === 0) return null |
| 291 | |
| 292 | const state = new Uint8Array(buffer) |
| 293 | console.log( |
| 294 | `[collab] Fetched ydoc from DB for ${boardUuid}: ${buffer.byteLength} bytes`, |
| 295 | ) |
| 296 | |
| 297 | // Warm the Redis cache |
| 298 | try { |
| 299 | const r = getRedis() |
| 300 | await r.setex( |
| 301 | redisYdocKey(boardUuid), |
| 302 | REDIS_YDOC_TTL, |
| 303 | Buffer.from(state), |
| 304 | ) |
| 305 | } catch (err) { |
| 306 | console.error( |
| 307 | `[collab] Redis warm error for ${boardUuid}:`, |
| 308 | err, |