Client-side, single user live journal backed by AT Protocol
0

Configure Feed

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

bunnylog / migration.js
4.6 kB 158 lines
1import { agent } from "#app/oauth"; 2import { makeXRPC, getATURIParts } from "#app"; 3 4/** 5 * @template T 6 * @typedef {Object} ListRecordsResponse 7 * @prop {string} [cursor] 8 * @prop {T[]} records 9 */ 10 11/** 12 * Split an array by `n` partitions 13 * @template {unknown[]} T 14 * @param {T} array 15 * @param {number} n - Must be greater than 1 16 * @returns {T[]} 17 */ 18function partition(array, n) { 19 let local_idx = 0 20 let initial = true 21 return Object.values(array.reduce((acc, val, idx) => { 22 if (idx % n === 0 && !initial) local_idx++ 23 if (initial) { initial = false } // hack 24 if (!acc[local_idx]) acc[local_idx] = [] 25 acc[local_idx].push(val) 26 27 return acc 28 }, {})) 29} 30 31/** 32 * Check if target is eligible for the lexicon namespace migration (one-time) 33 * @param {string} target 34 * @returns {Promise<boolean>} 35 */ 36export async function shouldMigrate(target) { 37 /** @type {{ collections: string[], [string]: any}} */ 38 const { collections } = await fetch(makeXRPC("com.atproto.repo.describeRepo", { 39 repo: target 40 })).then(res => res.json()) 41 42 return collections.includes("space.bunniesin.micro.log") 43} 44 45/** 46 * Retry with exponential backoff 47 * @template {() => unknown} T 48 * @param {T} fn - Function to call on retry 49 * @param {{initialDelay: number, maxAttempts: number}} [options] 50 * @async 51 * @returns {ReturnType<T>} 52 */ 53async function retry(fn, options = { initialDelay: 200, maxAttempts: 10 }) { 54 const { initialDelay, maxAttempts } = options; 55 let attempt = 1 56 while (attempt <= maxAttempts) { 57 try { 58 return await fn() 59 } catch (err) { 60 if (attempt === maxAttempts) throw err 61 62 const t = initialDelay * Math.pow(2, attempt - 1); 63 const j = Math.random() * initialDelay 64 const d = t + j; 65 66 await new Promise((r) => setTimeout(() => r(), d)) 67 attempt++ 68 } 69 } 70} 71 72export async function queryAllRecordsToBeMigrated(target) { 73 /** @type {string?} */ 74 let cursor = ""; 75 /** @type {(import("./app.js").BunnyLogEntry & { rkey: string })[]} */ 76 let records = []; 77 78 /** 79 * @returns {Promise<ListRecordsResponse<import("./app.js").BunnyLogEntry>>} 80 */ 81 async function next() { 82 const res = await fetch( 83 makeXRPC("com.atproto.repo.listRecords", { 84 repo: target, 85 collection: "space.bunniesin.micro.log", 86 cursor, 87 }), 88 ); 89 90 if (!res.ok) { 91 return retry(next); // /technically/ recursive, but it isn't 92 } 93 94 return await res.json(); 95 } 96 97 while (cursor != null) { 98 let p = await next(); 99 cursor = p.cursor ?? null; 100 records.push(...p.records.map(r => ({ value: r.value, rkey: getATURIParts(r.uri).rkey }))) 101 } 102 103 return records 104} 105 106/** 107 * @asnyc 108 * @returns {boolean} 109 */ 110export async function migrateRecordsToNewLexicon(target, batchSize = 50) { 111 /** @type {(import("./app.js").BunnyLogEntry & { rkey: string })[]}*/ 112 const records = await queryAllRecordsToBeMigrated(target); 113 114 console.info("[MIGRATION]", "Records to be migrated:", records.length); 115 116 let currentBatch = 1; 117 // Part 2 --- Migrate them to the new namespace & lexicon using com.atproto.repo.applyWrites 118 for (const rx of partition(records, batchSize)) { 119 /** @type {any[]} */ 120 const creates = rx.reduce((acc, r) => { 121 const { rkey, value } = {...r, value: {...r.value, $type: "space.bunniesin.log.entry" }} // this is stupid 122 return [...acc, 123 { $type: "com.atproto.repo.applyWrites#create", collection: "space.bunniesin.log.entry", rkey, value }]; 124 }, []) 125 126 const deletes = rx.reduce((acc, r) => { 127 return [...acc, { $type: "com.atproto.repo.applyWrites#delete", collection: "space.bunniesin.micro.log", rkey: r.rkey}] 128 }, []) 129 130 let res = await agent.com.atproto.repo.applyWrites({ 131 repo: agent.did, 132 writes: creates, 133 validate: false // Most, if not all PDSes do not include our lexicons, so validation is not possible 134 }) 135 136 if (!res.success) throw new Error(`FAILED TO CREATE RECORDS!!!!! FUCK!!!!!\n${JSON.stringify(res)}`); 137 138 console.info("[MIGRATION]", `Created ${creates.length} records.`) 139 140 141 res = await agent.com.atproto.repo.applyWrites({ 142 repo: agent.did, 143 writes: deletes, 144 validate: false 145 }) 146 147 if (!res.success) throw new Error(`FAILED TO DELETE OLD RECORDS!!!!! FUCK!!!!!\n${JSON.stringify(res)}`); 148 149 console.info("[MIGRATION]", `Deleted ${deletes.length} old records.`) 150 151 console.info("[MIGRATION]", `Batch ${currentBatch} was migrated without issue.`) 152 currentBatch++ 153 } 154 155 console.info("[MIGRATION]", "All batches were migrated successfully.") 156 157 return true 158}