From 5ad544affd4b3dcaca6fcd10d1d7753e96be49d5 Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Thu, 22 Jan 2026 17:30:03 -1000 Subject: [PATCH 1/3] refactor: Guarantee import operations are stateless by making them static functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we’ve been seeing some failures when running imports in parallel, it makes sense to refactor the code a little to guarantee that import operations are stateless. Thankfully, this didn’t require any change to the importers themselves, so we can now have a little more confidence in their implementation. --- Sources/InContextCore/Handler.swift | 2 +- .../InContextCore/Importers/CopyImporter.swift | 6 +++--- .../InContextCore/Importers/IgnoreImporter.swift | 6 +++--- .../InContextCore/Importers/ImageImporter.swift | 6 +++--- Sources/InContextCore/Importers/Importer.swift | 6 +++--- .../Importers/MarkdownImporter.swift | 14 +++++++------- .../InContextCore/Importers/VideoImporter.swift | 1 - .../Importers/VideoImporterLinux.swift | 6 +++--- .../Importers/VideoImporterMac.swift | 16 ++++++++-------- 9 files changed, 31 insertions(+), 32 deletions(-) diff --git a/Sources/InContextCore/Handler.swift b/Sources/InContextCore/Handler.swift index ce96956..995110e 100644 --- a/Sources/InContextCore/Handler.swift +++ b/Sources/InContextCore/Handler.swift @@ -63,7 +63,7 @@ extension Handler: Fingerprintable { } func process(file: File, outputURL: URL) async throws -> ImporterResult { - return try await importer.process(file: file, settings: settings, outputURL: outputURL) + return try await T.process(file: file, settings: settings, outputURL: outputURL) } } diff --git a/Sources/InContextCore/Importers/CopyImporter.swift b/Sources/InContextCore/Importers/CopyImporter.swift index f1a715f..464dc16 100644 --- a/Sources/InContextCore/Importers/CopyImporter.swift +++ b/Sources/InContextCore/Importers/CopyImporter.swift @@ -36,9 +36,9 @@ class CopyImporter: Importer { return EmptySettings() } - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { // TODO: Consider whether these actually get a tracking context that lets them add to the site instead of // returning documents. That feels like it might be cleaner and more flexible? // That approach would have the benefit of meaning that we don't really need to do significant path diff --git a/Sources/InContextCore/Importers/IgnoreImporter.swift b/Sources/InContextCore/Importers/IgnoreImporter.swift index bf1b393..2b24571 100644 --- a/Sources/InContextCore/Importers/IgnoreImporter.swift +++ b/Sources/InContextCore/Importers/IgnoreImporter.swift @@ -36,9 +36,9 @@ class IgnoreImporter: Importer { return EmptySettings() } - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { return ImporterResult() } diff --git a/Sources/InContextCore/Importers/ImageImporter.swift b/Sources/InContextCore/Importers/ImageImporter.swift index a8fa516..b509796 100644 --- a/Sources/InContextCore/Importers/ImageImporter.swift +++ b/Sources/InContextCore/Importers/ImageImporter.swift @@ -364,9 +364,9 @@ extension ImageImporter: Importer { extension ImageImporter: Importer { - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { let fileURL = file.url diff --git a/Sources/InContextCore/Importers/Importer.swift b/Sources/InContextCore/Importers/Importer.swift index 556575c..c87eea6 100644 --- a/Sources/InContextCore/Importers/Importer.swift +++ b/Sources/InContextCore/Importers/Importer.swift @@ -46,9 +46,9 @@ protocol Importer { var version: Int { get } func settings(for configuration: [String: Any]) throws -> Settings - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult } diff --git a/Sources/InContextCore/Importers/MarkdownImporter.swift b/Sources/InContextCore/Importers/MarkdownImporter.swift index 578fd93..2c573cd 100644 --- a/Sources/InContextCore/Importers/MarkdownImporter.swift +++ b/Sources/InContextCore/Importers/MarkdownImporter.swift @@ -44,21 +44,21 @@ class MarkdownImporter: Importer { defaultTemplate: try configuration.requiredValue(for: "defaultTemplate")) } - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { - + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { + let fileURL = file.url let details = fileURL.basenameDetails() let frontmatter = try FrontmatterDocument(contentsOf: fileURL, generateHTML: true) - + // Merge the details and metadata. var metadata = [AnyHashable: Any]() metadata["title"] = details.title metadata.merge(frontmatter.metadata) { $1 } - + let category: String = try metadata.value(for: "category", default: settings.defaultCategory) - + let document = try Document(url: fileURL.siteURL, parent: fileURL.parentURL, category: category, diff --git a/Sources/InContextCore/Importers/VideoImporter.swift b/Sources/InContextCore/Importers/VideoImporter.swift index 90bf570..1dab761 100644 --- a/Sources/InContextCore/Importers/VideoImporter.swift +++ b/Sources/InContextCore/Importers/VideoImporter.swift @@ -49,4 +49,3 @@ class VideoImporter { } } - diff --git a/Sources/InContextCore/Importers/VideoImporterLinux.swift b/Sources/InContextCore/Importers/VideoImporterLinux.swift index d5e5bdf..444b9d7 100644 --- a/Sources/InContextCore/Importers/VideoImporterLinux.swift +++ b/Sources/InContextCore/Importers/VideoImporterLinux.swift @@ -26,9 +26,9 @@ import Foundation extension VideoImporter: Importer { - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { throw InContextError.internalInconsistency("Unsupported") } diff --git a/Sources/InContextCore/Importers/VideoImporterMac.swift b/Sources/InContextCore/Importers/VideoImporterMac.swift index 09bd4a8..1006e80 100644 --- a/Sources/InContextCore/Importers/VideoImporterMac.swift +++ b/Sources/InContextCore/Importers/VideoImporterMac.swift @@ -27,9 +27,9 @@ import Foundation extension VideoImporter: Importer { - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { let fileURL = file.url @@ -134,7 +134,7 @@ extension VideoImporter: Importer { return ImporterResult(document: document, assets: [Asset(fileURL: videoURL), Asset(fileURL: thumbnailURL)]) } - func thumbnail(asset: AVAsset, destinationURL: URL) async throws { + static func thumbnail(asset: AVAsset, destinationURL: URL) async throws { let generator = AVAssetImageGenerator(asset: asset) generator.appliesPreferredTrackTransform = true let time = CMTime(seconds: 1, preferredTimescale: 1) @@ -154,10 +154,10 @@ extension VideoImporter: Importer { } // https://developer.apple.com/documentation/avfoundation/media_reading_and_writing/exporting_video_to_alternative_formats - func export(video: AVAsset, - withPreset preset: String = AVAssetExportPresetHighestQuality, - toFileType outputFileType: AVFileType = .mov, - atURL outputURL: URL) async throws { + static func export(video: AVAsset, + withPreset preset: String = AVAssetExportPresetHighestQuality, + toFileType outputFileType: AVFileType = .mov, + atURL outputURL: URL) async throws { // Check the compatibility of the preset to export the video to the output file type. guard await AVAssetExportSession.compatibility(ofExportPreset: preset, From 7ad99600fe321584d5ac753a6696c3d3ccb59a0e Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Thu, 22 Jan 2026 18:39:55 -1000 Subject: [PATCH 2/3] =?UTF-8?q?Don=E2=80=99t=20forget=20the=20Linux=20Imag?= =?UTF-8?q?eImporter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/InContextCore/Importers/ImageImporter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/InContextCore/Importers/ImageImporter.swift b/Sources/InContextCore/Importers/ImageImporter.swift index b509796..7384319 100644 --- a/Sources/InContextCore/Importers/ImageImporter.swift +++ b/Sources/InContextCore/Importers/ImageImporter.swift @@ -350,9 +350,9 @@ class ImageImporter { extension ImageImporter: Importer { - func process(file: File, - settings: Settings, - outputURL: URL) async throws -> ImporterResult { + static func process(file: File, + settings: Settings, + outputURL: URL) async throws -> ImporterResult { throw InContextError.internalInconsistency("Unsupported") From 975880f828dc7b0b86374a20dfa28c929533b02c Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Thu, 22 Jan 2026 18:45:56 -1000 Subject: [PATCH 3/3] Update the tests --- .../Tests/MarkdownImporterTests.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Tests/InContextTests/Tests/MarkdownImporterTests.swift b/Tests/InContextTests/Tests/MarkdownImporterTests.swift index 26b796d..bc15a0f 100644 --- a/Tests/InContextTests/Tests/MarkdownImporterTests.swift +++ b/Tests/InContextTests/Tests/MarkdownImporterTests.swift @@ -46,11 +46,10 @@ title: Fromage These are the contents of the file. """) - let importer = MarkdownImporter() - let result = try await importer.process(file: file, - settings: .init(defaultCategory: "general", - defaultTemplate: "posts.html"), - outputURL: defaultSourceDirectory.site.filesURL) + let result = try await MarkdownImporter.process(file: file, + settings: .init(defaultCategory: "general", + defaultTemplate: "posts.html"), + outputURL: defaultSourceDirectory.site.filesURL) XCTAssertNotNil(result.document) XCTAssertEqual(result.document!.metadata["title"] as? String, "Fromage") } @@ -68,11 +67,10 @@ steps: defaultTemplate: posts.html """) let file = try defaultSourceDirectory.add("cheese/index.markdown", location: .content, contents: "Contents!") - let importer = MarkdownImporter() - let result = try await importer.process(file: file, - settings: .init(defaultCategory: "general", - defaultTemplate: "posts.html"), - outputURL: defaultSourceDirectory.site.filesURL) + let result = try await MarkdownImporter.process(file: file, + settings: .init(defaultCategory: "general", + defaultTemplate: "posts.html"), + outputURL: defaultSourceDirectory.site.filesURL) XCTAssertNotNil(result.document) XCTAssertEqual(result.document!.metadata["title"] as? String, "Cheese") }