mirror of
https://github.com/kean/Pulse.git
synced 2026-05-30 21:07:33 +00:00
158 lines
5.3 KiB
Swift
158 lines
5.3 KiB
Swift
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
|
|
|
|
import Foundation
|
|
import Pulse
|
|
import CoreData
|
|
import SwiftUI
|
|
import Pulse
|
|
|
|
final class ShareStoreTask {
|
|
private var isCancelled = false
|
|
private var objectIDs: [NSManagedObjectID]
|
|
private let renderer = TextRenderer(options: .sharing)
|
|
private let store: LoggerStore
|
|
private let output: ShareOutput
|
|
private let context: NSManagedObjectContext
|
|
private var completion: ((ShareItems?) -> Void)?
|
|
|
|
init(entities: [NSManagedObject], store: LoggerStore, output: ShareOutput, completion: @escaping (ShareItems?) -> Void) {
|
|
self.objectIDs = entities.map(\.objectID)
|
|
self.store = store
|
|
self.output = output
|
|
self.context = store.backgroundContext
|
|
self.completion = completion
|
|
}
|
|
|
|
func cancel() { // Not yet implemented
|
|
isCancelled = true
|
|
completion?(nil)
|
|
completion = nil
|
|
}
|
|
|
|
func start() {
|
|
context.perform {
|
|
self.prepareForSharing()
|
|
}
|
|
}
|
|
|
|
/// - warning: For testing purposes only.
|
|
func share() -> ShareItems {
|
|
ShareService.share(renderAsAttributedString(), as: output)
|
|
}
|
|
|
|
private func prepareForSharing() {
|
|
let string = renderAsAttributedString()
|
|
|
|
if output == .pdf { // Can only be used on the main thread
|
|
DispatchQueue.main.async {
|
|
let items = ShareService.share(string, as: self.output)
|
|
self.didComplete(with: items)
|
|
}
|
|
} else {
|
|
let items = ShareService.share(string, as: output)
|
|
self.didComplete(with: items)
|
|
}
|
|
}
|
|
|
|
private func didComplete(with items: ShareItems) {
|
|
DispatchQueue.main.async {
|
|
self.completion?(items)
|
|
self.completion = nil
|
|
}
|
|
}
|
|
|
|
private func renderAsAttributedString() -> NSAttributedString {
|
|
switch output {
|
|
case .har:
|
|
guard let file = try? HARDocument(store: store),
|
|
let encoded = try? JSONEncoder().encode(file),
|
|
let string = try? NSAttributedString(data: encoded, documentAttributes: nil) else {
|
|
return NSAttributedString(string: "")
|
|
}
|
|
return string
|
|
default:
|
|
prerenderResponseBodies()
|
|
|
|
let content = contentForSharing(count: objectIDs.count)
|
|
for index in objectIDs.indices {
|
|
guard let entity = try? context.existingObject(with: objectIDs[index]) else {
|
|
continue
|
|
}
|
|
switch LoggerEntity(entity) {
|
|
case .message(let message):
|
|
renderer.render(message)
|
|
case .task(let task):
|
|
renderer.render(task, content: content, store: store)
|
|
}
|
|
if index < objectIDs.endIndex - 1 {
|
|
renderer.addSpacer()
|
|
}
|
|
}
|
|
return renderer.make()
|
|
}
|
|
}
|
|
|
|
// Unlike the rest of the processing it's easy to parallelize and it
|
|
// usually takes up at least 90% of processing.
|
|
private func prerenderResponseBodies() {
|
|
var jobs: [NSManagedObjectID: RenderBodyJob] = [:]
|
|
|
|
func enqueueJob(for blob: LoggerBlobHandleEntity, error: NetworkLogger.DecodingError?) {
|
|
jobs[blob.objectID] = RenderBodyJob(
|
|
data: LoggerBlobHandleEntity.getData(for: blob, store: store),
|
|
contentType: blob.contentType,
|
|
error: error
|
|
)
|
|
}
|
|
|
|
for objectID in objectIDs {
|
|
if let object = try? context.existingObject(with: objectID),let task = LoggerEntity(object).task {
|
|
if let blob = task.responseBody, jobs[blob.objectID] == nil {
|
|
enqueueJob(for: blob, error: task.decodingError)
|
|
}
|
|
if let blob = task.requestBody, jobs[blob.objectID] == nil {
|
|
enqueueJob(for: blob, error: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
let queue = Array(jobs)
|
|
let indices = queue.indices
|
|
// "To get the maximum benefit of this function, configure the number of
|
|
// iterations to be at least three times the number of available cores."
|
|
let iterations = indices.count >= 32 ? 32 : (indices.count >= 8 ? 8 : 1)
|
|
let lock = NSLock()
|
|
DispatchQueue.concurrentPerform(iterations: iterations) { index in
|
|
let start = index * indices.count / iterations
|
|
let end = (index + 1) * indices.count / iterations
|
|
|
|
for index in start..<end {
|
|
let job = queue[index]
|
|
guard let data = job.value.data() else { continue }
|
|
let string = TextRenderer(options: .sharing).render(data, contentType: job.value.contentType, error: job.value.error)
|
|
|
|
lock.lock()
|
|
renderer.renderedBodies[job.key] = string
|
|
lock.unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct RenderBodyJob {
|
|
let data: () -> Data?
|
|
let contentType: NetworkLogger.ContentType?
|
|
let error: NetworkLogger.DecodingError?
|
|
}
|
|
}
|
|
|
|
private func contentForSharing(count: Int) -> NetworkContent {
|
|
var content = NetworkContent.sharing
|
|
if count > 1 {
|
|
content.remove(.largeHeader)
|
|
content.insert(.header)
|
|
}
|
|
return content
|
|
}
|