Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.7.3"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"),
Expand All @@ -55,7 +54,6 @@ let package = Package(
traits: ["CasePaths"]
)
),
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(
name: "CustomDump",
package: "swift-custom-dump",
Expand Down
2 changes: 0 additions & 2 deletions Package@swift-5.9.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"),
Expand All @@ -31,7 +30,6 @@ let package = Package(
dependencies: [
"Sharing1",
"Sharing2",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
Expand Down
2 changes: 0 additions & 2 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.1"),
.package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "1.0.0"),
Expand All @@ -32,7 +31,6 @@ let package = Package(
dependencies: [
"Sharing1",
"Sharing2",
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
.product(name: "CustomDump", package: "swift-custom-dump"),
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
Expand Down
16 changes: 16 additions & 0 deletions Sources/Sharing/Internal/LockIsolated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import class Foundation.NSLock

package final class LockIsolated<Value>: @unchecked Sendable {
private var value: Value
private let lock = NSLock()
package init(_ value: sending Value) {
self.value = value
}
package func withLock<R, F: Error>(
_ operation: (inout sending Value) throws(F) -> sending R
) throws(F) -> sending R {
lock.lock()
defer { lock.unlock() }
return try operation(&value)
}
}
1 change: 0 additions & 1 deletion Sources/Sharing/SharedKeys/AppStorageKey.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit)
import ConcurrencyExtras
public import Dependencies
@preconcurrency public import Foundation
import IssueReporting
Expand Down
19 changes: 9 additions & 10 deletions Sources/Sharing/SharedKeys/FileStorageKey.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit)
import Combine
public import ConcurrencyExtras
public import Dependencies
@preconcurrency import Dispatch

Expand Down Expand Up @@ -143,7 +142,7 @@
setUpSources()
}
}
if state.withValue({ $0.workItem == nil }) {
if state.withLock({ $0.workItem == nil }) {
if fileExists {
subscriber.yield(with: Result { try self.decode(self.storage.load(self.url)) })
} else {
Expand All @@ -161,7 +160,7 @@
? (try? self.storage.attributesOfItemAtPath(self.url.path)[.modificationDate]
as? Date)
: nil
let shouldYield = state.withValue { state in
let shouldYield = state.withLock { state in
guard fileExists
else {
state.cancelWorkItem()
Expand Down Expand Up @@ -210,7 +209,7 @@

public func save(_ value: Value, context: SaveContext, continuation: SaveContinuation) {
do {
let workItem: DispatchWorkItem? = try state.withValue { state in
let workItem: DispatchWorkItem? = try state.withLock { state in
let data = try encode(value)
switch context {
case .didSet:
Expand All @@ -224,7 +223,7 @@
continuation.resume()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.state.withValue { state in
self.state.withLock { state in
defer {
state.value = nil
state.workItem = nil
Expand Down Expand Up @@ -334,7 +333,7 @@
@Sendable (URL, DispatchSource.FileSystemEvent, @escaping @Sendable () -> Void) throws ->
SharedSubscription
let load: @Sendable (URL) throws -> Data
@_spi(Internals) public let save: @Sendable (Data, URL) throws -> Void
public let save: @Sendable (Data, URL) throws -> Void

/// File storage that interacts directly with the file system for saving, loading and listening
/// for file changes.
Expand Down Expand Up @@ -391,7 +390,7 @@
inMemory(fileSystem: LockIsolated([:]))
}

@_spi(Internals) public static func inMemory(
package static func inMemory(
fileSystem: LockIsolated<[URL: Data]>,
async: @escaping @Sendable (DispatchWorkItem) -> Void = { $0.perform() },
asyncAfter: @escaping @Sendable (DispatchTimeInterval, DispatchWorkItem) -> Void = {
Expand All @@ -405,22 +404,22 @@
asyncAfter: asyncAfter,
attributesOfItemAtPath: { _ in [:] },
createDirectory: { _, _ in },
fileExists: { fileSystem.keys.contains($0) },
fileExists: { url in fileSystem.withLock { $0.keys.contains(url) } },
fileSystemSource: { url, event, handler in
guard event.contains(.write)
else { return SharedSubscription {} }
return SharedSubscription {}
},
load: {
guard let data = fileSystem[$0]
guard let data = fileSystem.withLock(\.[$0])
else {
struct LoadError: Error {}
throw LoadError()
}
return data
},
save: { data, url in
fileSystem.withValue { $0[url] = data }
fileSystem.withLock { $0[url] = data }
}
)
}
Expand Down
47 changes: 23 additions & 24 deletions Tests/SharingTests/FileStorageTests.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit)
import Combine
import CombineSchedulers
import ConcurrencyExtras
import CustomDump
import Dependencies
import DependenciesTestSupport
import Foundation
@_spi(Internals) import Sharing
import Sharing
import Testing

@Suite struct FileStorageTests {
let fileSystem = LockIsolated<[URL: Data]>([:])
let fileSystem = Sharing.LockIsolated<[URL: Data]>([:])
let testScheduler = DispatchQueue.test

@Test func basics() throws {
Expand All @@ -20,11 +19,11 @@
@Shared(.fileStorage(.fileURL)) var users = [User]()
#expect($users.loadError == nil)
expectNoDifference(
fileSystem.value,
fileSystem.withLock(\.self),
[.fileURL: Data()]
)
$users.withLock { $0.append(.blob) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])
}
}

Expand All @@ -35,12 +34,12 @@
@Shared(.utf8String) var string = ""
#expect($string.loadError == nil)
expectNoDifference(
fileSystem.value,
fileSystem.withLock(\.self),
[.utf8StringURL: Data()]
)
$string.withLock { $0 = "hello" }
expectNoDifference(
fileSystem.value[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) },
fileSystem.withLock(\.self)[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) },
"hello"
)
}
Expand All @@ -51,26 +50,26 @@
$0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: testScheduler)
} operation: {
@Shared(.fileStorage(.fileURL)) var users = [User]()
try expectNoDifference(fileSystem.value.users(for: .fileURL), nil)
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), nil)

$users.withLock { $0.append(.blob) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])

$users.withLock { $0.append(.blobJr) }
testScheduler.advance(by: .seconds(1) - .milliseconds(1))
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])

$users.withLock { $0.append(.blobSr) }
testScheduler.advance(by: .milliseconds(1))
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr, .blobSr])

testScheduler.advance(by: .seconds(1))
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr, .blobSr])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr, .blobSr])

testScheduler.advance(by: .seconds(0.5))
$users.withLock { $0.append(.blobEsq) }
try expectNoDifference(
fileSystem.value.users(for: .fileURL),
fileSystem.withLock(\.self).users(for: .fileURL),
[
.blob,
.blobJr,
Expand All @@ -86,14 +85,14 @@
$0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: testScheduler)
} operation: {
@Shared(.fileStorage(.fileURL)) var users = [User]()
try expectNoDifference(fileSystem.value.users(for: .fileURL), nil)
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), nil)

$users.withLock { $0.append(.blob) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])

testScheduler.advance(by: .seconds(2))
$users.withLock { $0.append(.blobJr) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob, .blobJr])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob, .blobJr])
}
}

Expand All @@ -114,17 +113,17 @@
@Shared(.fileStorage(.anotherFileURL)) var otherUsers = [User]()

$users.withLock { $0.append(.blob) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), nil)
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .anotherFileURL), nil)

$otherUsers.withLock { $0.append(.blobJr) }
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.value.users(for: .anotherFileURL), [.blobJr])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .anotherFileURL), [.blobJr])
}
}

@Test func initialValue() async throws {
let fileSystem = try LockIsolated<[URL: Data]>(
let fileSystem = Sharing.LockIsolated<[URL: Data]>(
[.fileURL: try JSONEncoder().encode([User.blob])]
)
try await withDependencies {
Expand All @@ -133,12 +132,12 @@
@Shared(.fileStorage(.fileURL)) var users = [User]()
_ = users
await Task.yield()
try expectNoDifference(fileSystem.value.users(for: .fileURL), [.blob])
try expectNoDifference(fileSystem.withLock(\.self).users(for: .fileURL), [.blob])
}
}

@Test func decodeFailure() async throws {
let fileSystem = LockIsolated<[URL: Data]>(
let fileSystem = Sharing.LockIsolated<[URL: Data]>(
[.fileURL: Data("corrupt".utf8)]
)
try withDependencies {
Expand Down Expand Up @@ -488,7 +487,7 @@

extension FileStorage {
fileprivate static func inMemory<S: Scheduler & Sendable>(
fileSystem: LockIsolated<[URL: Data]>,
fileSystem: Sharing.LockIsolated<[URL: Data]>,
scheduler: S
) -> Self where S.SchedulerTimeType == DispatchQueue.SchedulerTimeType {
.inMemory(
Expand Down
Loading