diff --git a/Workouts/ExerciseFigure/ExerciseInfo.swift b/Workouts/ExerciseFigure/ExerciseInfo.swift index 9a5925a..bb279f9 100644 --- a/Workouts/ExerciseFigure/ExerciseInfo.swift +++ b/Workouts/ExerciseFigure/ExerciseInfo.swift @@ -215,12 +215,28 @@ extension ExerciseInfo { /// Finds and parses the bundled `.info.md` exported next to the /// motion rigs by `render.py --export`. enum ExerciseInfoLibrary { + /// Every bundled `info.md`, parsed once and keyed by exercise name. `info(for:)` is + /// fanned out per exercise per keystroke by `ExerciseCatalog.sections(matching:)`, so + /// re-doing the `Bundle.main` lookup, file read, and markdown parse on every call was + /// wasteful; ~60-odd files parsed once is trivial. Enumerates the bundle directly + /// (mirroring `ExerciseMotionLibrary.exerciseNames`'s compound-suffix filter) rather + /// than keying off that list, so info lookup doesn't depend on a motion rig existing. + private static let cache: [String: ExerciseInfo] = { + let urls = Bundle.main.urls(forResourcesWithExtension: "md", subdirectory: nil) ?? [] + var result: [String: ExerciseInfo] = [:] + for url in urls { + let filename = url.lastPathComponent + guard filename.hasSuffix(".info.md"), + let markdown = try? String(contentsOf: url, encoding: .utf8) + else { continue } + let exerciseName = String(filename.dropLast(".info.md".count)) + result[exerciseName] = ExerciseInfo.parse(markdown: markdown) + } + return result + }() + /// The parsed info for `exerciseName`, or `nil` when none is bundled. static func info(for exerciseName: String) -> ExerciseInfo? { - guard - let url = Bundle.main.url(forResource: exerciseName, withExtension: "info.md"), - let markdown = try? String(contentsOf: url, encoding: .utf8) - else { return nil } - return ExerciseInfo.parse(markdown: markdown) + cache[exerciseName] } } diff --git a/Workouts/Sync/SyncEngine.swift b/Workouts/Sync/SyncEngine.swift index d701d1e..5075923 100644 --- a/Workouts/Sync/SyncEngine.swift +++ b/Workouts/Sync/SyncEngine.swift @@ -407,7 +407,7 @@ final class SyncEngine { /// when the engine isn't connected. @discardableResult func duplicate(routine: Routine) async -> String? { - guard store != nil else { return nil } + guard store != nil, routine.isLive else { return nil } var doc = RoutineDocument(from: routine) doc.id = ULID.make() @@ -767,6 +767,10 @@ final class SyncEngine { /// changes accumulated while the app was closed are picked up. private func reconcile() async { guard let store, let tombstones else { return } + // Drop a reconcile that overlaps a running one — redundant, since the + // running pass covers the same tree and the metadata observer picks up + // anything newer. + guard !isSyncing else { return } isSyncing = true defer { isSyncing = false }