From 501e6e14b1f95a2b059a6d791901e28e52a31cef Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 18:46:40 +0330 Subject: [PATCH 1/6] full sendability --- Package.swift | 2 +- Sources/LeafKit/Exports.swift | 12 ++- Sources/LeafKit/LeafAST.swift | 2 +- .../LeafKit/LeafCache/DefaultLeafCache.swift | 30 ++++++- Sources/LeafKit/LeafCache/LeafCache.swift | 3 +- Sources/LeafKit/LeafConfiguration.swift | 80 ++++++++++--------- Sources/LeafKit/LeafData/LeafData.swift | 9 ++- .../LeafKit/LeafData/LeafDataStorage.swift | 5 +- Sources/LeafKit/LeafError.swift | 4 +- .../LeafLexer/LeafParameterTypes.swift | 8 +- Sources/LeafKit/LeafLexer/LeafToken.swift | 2 +- .../LeafKit/LeafParser/LeafParameter.swift | 2 +- Sources/LeafKit/LeafRenderer.swift | 16 ++-- .../LeafSerialize/LeafSerializer.swift | 2 +- Sources/LeafKit/LeafSource/LeafSource.swift | 2 +- Sources/LeafKit/LeafSource/LeafSources.swift | 17 ++-- Sources/LeafKit/LeafSource/NIOLeafFiles.swift | 6 +- Sources/LeafKit/LeafSyntax/LeafSyntax.swift | 18 ++--- Sources/LeafKit/LeafSyntax/LeafTag.swift | 8 +- Sources/LeafKit/SendableBox.swift | 24 ++++++ Tests/LeafKitTests/TestHelpers.swift | 12 +-- 21 files changed, 168 insertions(+), 96 deletions(-) create mode 100644 Sources/LeafKit/SendableBox.swift diff --git a/Package.swift b/Package.swift index 6122d97a..dcd6ff02 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.6 import PackageDescription let package = Package( diff --git a/Sources/LeafKit/Exports.swift b/Sources/LeafKit/Exports.swift index b1503b3c..72243804 100644 --- a/Sources/LeafKit/Exports.swift +++ b/Sources/LeafKit/Exports.swift @@ -2,8 +2,16 @@ extension Character { // MARK: - LeafToken specific identities (Internal) - static var tagIndicator: Character = .octothorpe - + private static let _tagIndicator = SendableBox(Character.octothorpe) + static var tagIndicator: Character { + get { + _tagIndicator.value + } + set(newValue) { + _tagIndicator.value = newValue + } + } + var isValidInTagName: Bool { return self.isLowercaseLetter || self.isUppercaseLetter diff --git a/Sources/LeafKit/LeafAST.swift b/Sources/LeafKit/LeafAST.swift index c0b98266..1a25c8d5 100644 --- a/Sources/LeafKit/LeafAST.swift +++ b/Sources/LeafKit/LeafAST.swift @@ -1,7 +1,7 @@ import NIO /// `LeafAST` represents a "compiled," grammatically valid Leaf template (which may or may not be fully resolvable or erroring) -public struct LeafAST: Hashable { +public struct LeafAST: Hashable, Sendable { // MARK: - Public public func hash(into hasher: inout Hasher) { hasher.combine(name) } diff --git a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift index 9eef26bc..52bc53cd 100644 --- a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift +++ b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift @@ -1,11 +1,33 @@ import NIOConcurrencyHelpers import NIO -public final class DefaultLeafCache: SynchronousLeafCache { +/// `@unchecked Sendable` because uses locks to guarantee Sendability. +public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable { // MARK: - Public - `LeafCache` Protocol Conformance - + + var __isEnabled = true /// Global setting for enabling or disabling the cache - public var isEnabled: Bool = true + public var _isEnabled: Bool { + get { + self.lock.withLock { + self.__isEnabled + } + } + set(newValue) { + self.lock.withLock { + self.__isEnabled = newValue + } + } + } + /// Global setting for enabling or disabling the cache + public var isEnabled: Bool { + get { + self._isEnabled + } + set(newValue) { + self._isEnabled = newValue + } + } /// Current count of cached documents public var count: Int { self.lock.withLock { cache.count } } @@ -73,7 +95,7 @@ public final class DefaultLeafCache: SynchronousLeafCache { // MARK: - Internal Only - internal let lock: Lock + internal let lock: NIOLock internal var cache: [String: LeafAST] /// Blocking file load behavior diff --git a/Sources/LeafKit/LeafCache/LeafCache.swift b/Sources/LeafKit/LeafCache/LeafCache.swift index 7ab2f54e..b69a343e 100644 --- a/Sources/LeafKit/LeafCache/LeafCache.swift +++ b/Sources/LeafKit/LeafCache/LeafCache.swift @@ -12,7 +12,8 @@ import NIO /// adherents where the cache store itself is not a bottleneck. /// /// `LeafAST.name` is to be used in all cases as the key for retrieving cached documents. -public protocol LeafCache { +@preconcurrency +public protocol LeafCache: Sendable { /// Global setting for enabling or disabling the cache var isEnabled : Bool { get set } /// Current count of cached documents diff --git a/Sources/LeafKit/LeafConfiguration.swift b/Sources/LeafKit/LeafConfiguration.swift index 5c0c7f38..1953bddb 100644 --- a/Sources/LeafKit/LeafConfiguration.swift +++ b/Sources/LeafKit/LeafConfiguration.swift @@ -3,7 +3,7 @@ import Foundation /// General configuration of Leaf /// - Sets the default View directory where templates will be looked for /// - Guards setting the global tagIndicator (default `#`). -public struct LeafConfiguration { +public struct LeafConfiguration: Sendable { /// Initialize Leaf with the default tagIndicator `#` and unfound imports throwing an exception /// - Parameter rootDirectory: Default directory where templates will be found @@ -23,9 +23,9 @@ public struct LeafConfiguration { /// - Parameter tagIndicator: Unique tagIndicator - may only be set once. /// - Parameter ignoreUnfoundImports: Ignore unfound imports - may only be set once. public init(rootDirectory: String, tagIndicator: Character, ignoreUnfoundImports: Bool) { - if !Self.started { + if !Self.started.value { Character.tagIndicator = tagIndicator - Self.started = true + Self.started.value = true } self._rootDirectory = rootDirectory self._ignoreUnfoundImports = ignoreUnfoundImports @@ -42,53 +42,53 @@ public struct LeafConfiguration { } public static var encoding: String.Encoding { - get { _encoding } - set { if !Self.running { _encoding = newValue } } + get { _encoding.value } + set { if !Self.running { _encoding.value = newValue } } } public static var boolFormatter: (Bool) -> String { - get { _boolFormatter } - set { if !Self.running { _boolFormatter = newValue } } + get { _boolFormatter.value } + set { if !Self.running { _boolFormatter.value = newValue } } } public static var intFormatter: (Int) -> String { - get { _intFormatter } - set { if !Self.running { _intFormatter = newValue } } + get { _intFormatter.value } + set { if !Self.running { _intFormatter.value = newValue } } } public static var doubleFormatter: (Double) -> String { - get { _doubleFormatter } - set { if !Self.running { _doubleFormatter = newValue } } + get { _doubleFormatter.value } + set { if !Self.running { _doubleFormatter.value = newValue } } } public static var nilFormatter: () -> String { - get { _nilFormatter } - set { if !Self.running { _nilFormatter = newValue } } + get { _nilFormatter.value } + set { if !Self.running { _nilFormatter.value = newValue } } } public static var voidFormatter: () -> String { - get { _voidFormatter } - set { if !Self.running { _voidFormatter = newValue } } + get { _voidFormatter.value } + set { if !Self.running { _voidFormatter.value = newValue } } } public static var stringFormatter: (String) -> String { - get { _stringFormatter } - set { if !Self.running { _stringFormatter = newValue } } + get { _stringFormatter.value } + set { if !Self.running { _stringFormatter.value = newValue } } } public static var arrayFormatter: ([String]) -> String { - get { _arrayFormatter } - set { if !Self.running { _arrayFormatter = newValue } } + get { _arrayFormatter.value } + set { if !Self.running { _arrayFormatter.value = newValue } } } public static var dictFormatter: ([String: String]) -> String { - get { _dictFormatter } - set { if !Self.running { _dictFormatter = newValue } } + get { _dictFormatter.value } + set { if !Self.running { _dictFormatter.value = newValue } } } public static var dataFormatter: (Data) -> String? { - get { _dataFormatter } - set { if !Self.running { _dataFormatter = newValue } } + get { _dataFormatter.value } + set { if !Self.running { _dataFormatter.value = newValue } } } // MARK: - Internal/Private Only @@ -99,27 +99,29 @@ public struct LeafConfiguration { internal var _ignoreUnfoundImports: Bool { willSet { assert(!accessed, "Changing property after LeafConfiguration has been read has no effect") } } - - internal static var _encoding: String.Encoding = .utf8 - internal static var _boolFormatter: (Bool) -> String = { $0.description } - internal static var _intFormatter: (Int) -> String = { $0.description } - internal static var _doubleFormatter: (Double) -> String = { $0.description } - internal static var _nilFormatter: () -> String = { "" } - internal static var _voidFormatter: () -> String = { "" } - internal static var _stringFormatter: (String) -> String = { $0 } - internal static var _arrayFormatter: ([String]) -> String = + + internal static let _encoding = SendableBox(.utf8) + internal static let _boolFormatter = SendableBox<(Bool) -> String>({ $0.description }) + internal static let _intFormatter = SendableBox<(Int) -> String>({ $0.description }) + internal static let _doubleFormatter = SendableBox<(Double) -> String>({ $0.description }) + internal static let _nilFormatter = SendableBox<(() -> String)>({ "" }) + internal static let _voidFormatter = SendableBox<(() -> String)>({ "" }) + internal static let _stringFormatter = SendableBox<((String) -> String)>({ $0 }) + internal static let _arrayFormatter = SendableBox<(([String]) -> String)>( { "[\($0.map {"\"\($0)\""}.joined(separator: ", "))]" } - internal static var _dictFormatter: ([String: String]) -> String = + ) + internal static let _dictFormatter = SendableBox<(([String: String]) -> String)>( { "[\($0.map { "\($0): \"\($1)\"" }.joined(separator: ", "))]" } - internal static var _dataFormatter: (Data) -> String? = - { String(data: $0, encoding: Self._encoding) } - + ) + internal static let _dataFormatter = SendableBox<((Data) -> String?)>( + { String(data: $0, encoding: Self._encoding.value) } + ) /// Convenience flag for global write-once - private static var started = false + private static let started = SendableBox(false) private static var running: Bool { - assert(!Self.started, "LeafKit can only be configured prior to instantiating any LeafRenderer") - return Self.started + assert(!Self.started.value, "LeafKit can only be configured prior to instantiating any LeafRenderer") + return Self.started.value } /// Convenience flag for local lock-after-access diff --git a/Sources/LeafKit/LeafData/LeafData.swift b/Sources/LeafKit/LeafData/LeafData.swift index 53018a44..3eaee312 100644 --- a/Sources/LeafKit/LeafData/LeafData.swift +++ b/Sources/LeafKit/LeafData/LeafData.swift @@ -15,10 +15,11 @@ public struct LeafData: CustomStringConvertible, ExpressibleByBooleanLiteral, ExpressibleByArrayLiteral, ExpressibleByFloatLiteral, - ExpressibleByNilLiteral { - + ExpressibleByNilLiteral, + Sendable { + /// The concrete instantiable object types for a `LeafData` - public enum NaturalType: String, CaseIterable, Hashable { + public enum NaturalType: String, CaseIterable, Hashable, Sendable { case bool case string case int @@ -250,7 +251,7 @@ public struct LeafData: CustomStringConvertible, /// Creates a new `LeafData` from `() -> LeafData` if possible or `nil` if not possible. /// `returns` must specify a `NaturalType` that the function will return - internal static func lazy(_ lambda: @escaping () -> LeafData, + internal static func lazy(_ lambda: @Sendable @escaping () -> LeafData, returns type: LeafData.NaturalType, invariant sideEffects: Bool) throws -> LeafData { LeafData(.lazy(f: lambda, returns: type, invariant: sideEffects)) diff --git a/Sources/LeafKit/LeafData/LeafDataStorage.swift b/Sources/LeafKit/LeafData/LeafDataStorage.swift index 6d7f9637..6a3e09bc 100644 --- a/Sources/LeafKit/LeafData/LeafDataStorage.swift +++ b/Sources/LeafKit/LeafData/LeafDataStorage.swift @@ -1,7 +1,7 @@ import NIO import Foundation -internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible { +internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible, Sendable { // MARK: - Cases // Static values @@ -20,7 +20,8 @@ internal indirect enum LeafDataStorage: Equatable, CustomStringConvertible { // Lazy resolvable function // Must specify return tuple giving (returnType, invariance) - case lazy(f: () -> (LeafData), + @preconcurrency + case lazy(f: @Sendable () -> (LeafData), returns: LeafData.NaturalType, invariant: Bool) diff --git a/Sources/LeafKit/LeafError.swift b/Sources/LeafKit/LeafError.swift index a03f0f81..43c08402 100644 --- a/Sources/LeafKit/LeafError.swift +++ b/Sources/LeafKit/LeafError.swift @@ -5,7 +5,7 @@ /// public struct LeafError: Error { /// Possible cases of a LeafError.Reason, with applicable stored values where useful for the type - public enum Reason { + public enum Reason: Sendable { // MARK: Errors related to loading raw templates /// Attempted to access a template blocked for security reasons case illegalAccess(String) @@ -109,7 +109,7 @@ public struct LeafError: Error { public struct LexerError: Error { // MARK: - Public - public enum Reason { + public enum Reason: Sendable { // MARK: Errors occuring during Lexing /// A character not usable in parameters is present when Lexer is not expecting it case invalidParameterToken(Character) diff --git a/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift b/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift index 5a44f5e0..ab055492 100644 --- a/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift +++ b/Sources/LeafKit/LeafLexer/LeafParameterTypes.swift @@ -1,7 +1,7 @@ // MARK: - `Parameter` Token Type /// An associated value enum holding data, objects or values usable as parameters to a `.tag` -public enum Parameter: Equatable, CustomStringConvertible { +public enum Parameter: Equatable, CustomStringConvertible, Sendable { case stringLiteral(String) case constant(Constant) case variable(name: String) @@ -44,7 +44,7 @@ public enum Parameter: Equatable, CustomStringConvertible { /// `Keyword`s are identifiers which take precedence over syntax/variable names - may potentially have /// representable state themselves as value when used with operators (eg, `true`, `false` when /// used with logical operators, `nil` when used with equality operators, and so forth) -public enum LeafKeyword: String, Equatable { +public enum LeafKeyword: String, Equatable, Sendable { // MARK: Public - Cases // Eval -> Bool / Other @@ -79,7 +79,7 @@ extension LeafKeyword { // MARK: - Operator Symbols /// Mathematical and Logical operators -public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterable { +public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterable, Sendable { // MARK: Public - Cases // Operator types: Logic Exist. UnPre Scope @@ -157,7 +157,7 @@ public enum LeafOperator: String, Equatable, CustomStringConvertible, CaseIterab } /// An integer or double constant value parameter (eg `1_000`, `-42.0`) -public enum Constant: CustomStringConvertible, Equatable { +public enum Constant: CustomStringConvertible, Equatable, Sendable { case int(Int) case double(Double) diff --git a/Sources/LeafKit/LeafLexer/LeafToken.swift b/Sources/LeafKit/LeafLexer/LeafToken.swift index c4cab323..1b3ab101 100644 --- a/Sources/LeafKit/LeafLexer/LeafToken.swift +++ b/Sources/LeafKit/LeafLexer/LeafToken.swift @@ -22,7 +22,7 @@ /// - `.whitespace`: Only generated when not at top-level, and unclear why maintaining it is useful /// -internal enum LeafToken: CustomStringConvertible, Equatable { +internal enum LeafToken: CustomStringConvertible, Equatable, Sendable { /// Holds a variable-length string of data that will be passed through with no processing case raw(String) diff --git a/Sources/LeafKit/LeafParser/LeafParameter.swift b/Sources/LeafKit/LeafParser/LeafParameter.swift index 9edd7470..84be13c2 100644 --- a/Sources/LeafKit/LeafParser/LeafParameter.swift +++ b/Sources/LeafKit/LeafParser/LeafParameter.swift @@ -1,6 +1,6 @@ import NIO -public indirect enum ParameterDeclaration: CustomStringConvertible { +public indirect enum ParameterDeclaration: CustomStringConvertible, Sendable { case parameter(Parameter) case expression([ParameterDeclaration]) case tag(Syntax.CustomTagDeclaration) diff --git a/Sources/LeafKit/LeafRenderer.swift b/Sources/LeafKit/LeafRenderer.swift index b831c704..60e2dc4e 100644 --- a/Sources/LeafKit/LeafRenderer.swift +++ b/Sources/LeafKit/LeafRenderer.swift @@ -10,7 +10,7 @@ import NIO /// /// Additional instances of LeafRenderer can then be created using these shared modules to allow /// concurrent rendering, potentially with unique per-instance scoped data via `userInfo`. -public final class LeafRenderer { +public final class LeafRenderer: Sendable { // MARK: - Public Only /// An initialized `LeafConfiguration` specificying default directory and tagIndicator @@ -25,13 +25,16 @@ public final class LeafRenderer { public let sources: LeafSources /// The NIO `EventLoop` on which this instance of `LeafRenderer` will operate public let eventLoop: EventLoop + let _userInfo: SendableBox<[AnyHashable: Any]> /// Any custom instance data to use (eg, in Vapor, the `Application` and/or `Request` data) - public let userInfo: [AnyHashable: Any] - + public var userInfo: [AnyHashable: Any] { + _userInfo.value + } + /// Initial configuration of LeafRenderer. public init( configuration: LeafConfiguration, - tags: [String: LeafTag] = defaultTags, + tags: [String: LeafTag] = defaultLeafTags, cache: LeafCache = DefaultLeafCache(), sources: LeafSources, eventLoop: EventLoop, @@ -42,7 +45,7 @@ public final class LeafRenderer { self.cache = cache self.sources = sources self.eventLoop = eventLoop - self.userInfo = userInfo + self._userInfo = .init(userInfo) } /// The public interface to `LeafRenderer` @@ -164,6 +167,7 @@ public final class LeafRenderer { let fetchRequests = ast.unresolvedRefs.map { self.fetch(template: $0, chain: chain) } + let constantChain = chain let results = EventLoopFuture.whenAllComplete(fetchRequests, on: self.eventLoop) return results.flatMap { results in let results = results @@ -182,7 +186,7 @@ public final class LeafRenderer { // Check new AST's unresolved refs to see if extension introduced new refs if !new.unresolvedRefs.subtracting(ast.unresolvedRefs).isEmpty { // AST has new references - try to resolve again recursively - return self.resolve(ast: new, chain: chain) + return self.resolve(ast: new, chain: constantChain) } else { // Cache extended AST & return - AST is either flat or unresolvable return self.cache.insert(new, on: self.eventLoop, replace: true) diff --git a/Sources/LeafKit/LeafSerialize/LeafSerializer.swift b/Sources/LeafKit/LeafSerialize/LeafSerializer.swift index a0ab6579..3ed48ec4 100644 --- a/Sources/LeafKit/LeafSerialize/LeafSerializer.swift +++ b/Sources/LeafKit/LeafSerialize/LeafSerializer.swift @@ -5,7 +5,7 @@ internal struct LeafSerializer { init( ast: [Syntax], - tags: [String: LeafTag] = defaultTags, + tags: [String: LeafTag] = defaultLeafTags, userInfo: [AnyHashable: Any] = [:], ignoreUnfoundImports: Bool diff --git a/Sources/LeafKit/LeafSource/LeafSource.swift b/Sources/LeafKit/LeafSource/LeafSource.swift index aa83a465..3785cefc 100644 --- a/Sources/LeafKit/LeafSource/LeafSource.swift +++ b/Sources/LeafKit/LeafSource/LeafSource.swift @@ -1,7 +1,7 @@ import NIO /// Public protocol to adhere to in order to provide template source originators to `LeafRenderer` -public protocol LeafSource { +public protocol LeafSource: Sendable { /// Given a path name, return an EventLoopFuture holding a ByteBuffer /// - Parameters: /// - template: Relative template name (eg: `"path/to/template"`) diff --git a/Sources/LeafKit/LeafSource/LeafSources.swift b/Sources/LeafKit/LeafSource/LeafSources.swift index 531512bb..a66d389d 100644 --- a/Sources/LeafKit/LeafSource/LeafSources.swift +++ b/Sources/LeafKit/LeafSource/LeafSources.swift @@ -11,7 +11,9 @@ import NIOConcurrencyHelpers /// prior to use by `LeafRenderer`. /// - `.all` provides a `Set` of the `String`keys for all sources registered with the instance /// - `.searchOrder` provides the keys of sources that an unspecified template request will search. -public final class LeafSources { +/// +/// `@unchecked Sendable` because uses locks to guarantee Sendability. +public final class LeafSources: @unchecked Sendable { // MARK: - Public /// All available `LeafSource`s of templates @@ -52,8 +54,8 @@ public final class LeafSources { // MARK: - Internal Only internal private(set) var sources: [String: LeafSource] private var order: [String] - private let lock: Lock = .init() - + private let lock: NIOLock = .init() + /// Locate a template from the sources; if a specific source is named, only try to read from it. Otherwise, use the specified search order internal func find(template: String, in source: String? = nil, on eventLoop: EventLoop) throws -> EventLoopFuture<(String, ByteBuffer)> { var keys: [String] @@ -71,12 +73,13 @@ public final class LeafSources { private func searchSources(t: String, on eL: EventLoop, s: [String]) -> EventLoopFuture<(String, ByteBuffer)> { guard !s.isEmpty else { return eL.makeFailedFuture(LeafError(.noTemplateExists(t))) } - var more = s - let key = more.removeFirst() + var _more = s + let key = _more.removeFirst() lock.lock() - let source = sources[key]! + let source = sources[key]! lock.unlock() - + let more = _more + do { let file = try source.file(template: t, escape: true, on: eL) // Hit the file - return the combined tuple diff --git a/Sources/LeafKit/LeafSource/NIOLeafFiles.swift b/Sources/LeafKit/LeafSource/NIOLeafFiles.swift index 53af91e7..33242416 100644 --- a/Sources/LeafKit/LeafSource/NIOLeafFiles.swift +++ b/Sources/LeafKit/LeafSource/NIOLeafFiles.swift @@ -1,11 +1,11 @@ import Foundation -import NIO +@preconcurrency import NIO /// Reference and default implementation of `LeafSource` adhering object that provides a non-blocking /// file reader for `LeafRenderer` /// /// Default initializer will -public struct NIOLeafFiles: LeafSource { +public struct NIOLeafFiles: LeafSource, Sendable { // MARK: - Public /// Various options for configuring an instance of `NIOLeafFiles` @@ -17,7 +17,7 @@ public struct NIOLeafFiles: LeafSource { /// inside a directory starting with `.`) /// /// A new `NIOLeafFiles` defaults to [.toSandbox, .toVisibleFiles, .requireExtensions] - public struct Limit: OptionSet { + public struct Limit: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue diff --git a/Sources/LeafKit/LeafSyntax/LeafSyntax.swift b/Sources/LeafKit/LeafSyntax/LeafSyntax.swift index 29b0488c..a070c8af 100644 --- a/Sources/LeafKit/LeafSyntax/LeafSyntax.swift +++ b/Sources/LeafKit/LeafSyntax/LeafSyntax.swift @@ -1,6 +1,6 @@ import NIO -public indirect enum Syntax { +public indirect enum Syntax: Sendable { // MARK: .raw - Makeable, Entirely Readable case raw(ByteBuffer) // MARK: `case variable(Variable)` removed @@ -23,7 +23,7 @@ public indirect enum Syntax { case export(Export) } -public enum ConditionalSyntax { +public enum ConditionalSyntax: Sendable { case `if`([ParameterDeclaration]) case `elseif`([ParameterDeclaration]) case `else` @@ -166,7 +166,7 @@ func indent(_ depth: Int) -> String { } extension Syntax { - public struct Import { + public struct Import: Sendable { public let key: String public init(_ params: [ParameterDeclaration]) throws { guard params.count == 1 else { throw "import only supports single param \(params)" } @@ -180,7 +180,7 @@ extension Syntax { } } - public struct Extend: BodiedSyntax { + public struct Extend: BodiedSyntax, Sendable { public let key: String public private(set) var exports: [String: Export] public private(set) var context: [ParameterDeclaration]? @@ -307,7 +307,7 @@ extension Syntax { } } - public struct Export: BodiedSyntax { + public struct Export: BodiedSyntax, Sendable { public let key: String public internal(set) var body: [Syntax] private var externalsSet: Set @@ -363,7 +363,7 @@ extension Syntax { } } - public struct Conditional: BodiedSyntax { + public struct Conditional: BodiedSyntax, Sendable { public internal(set) var chain: [( condition: ConditionalSyntax, body: [Syntax] @@ -464,7 +464,7 @@ extension Syntax { } } - public struct With: BodiedSyntax { + public struct With: BodiedSyntax, Sendable { public internal(set) var body: [Syntax] public internal(set) var context: [ParameterDeclaration] @@ -528,7 +528,7 @@ extension Syntax { } } - public struct Loop: BodiedSyntax { + public struct Loop: BodiedSyntax, Sendable { /// the key to use when accessing items public let item: String /// the key to use to access the array @@ -614,7 +614,7 @@ extension Syntax { } } - public struct CustomTagDeclaration: BodiedSyntax { + public struct CustomTagDeclaration: BodiedSyntax, Sendable { public let name: String public let params: [ParameterDeclaration] public internal(set) var body: [Syntax]? diff --git a/Sources/LeafKit/LeafSyntax/LeafTag.swift b/Sources/LeafKit/LeafSyntax/LeafTag.swift index c8fa1055..11556908 100644 --- a/Sources/LeafKit/LeafSyntax/LeafTag.swift +++ b/Sources/LeafKit/LeafSyntax/LeafTag.swift @@ -1,14 +1,15 @@ import Foundation /// Create custom tags by conforming to this protocol and registering them. -public protocol LeafTag { +@preconcurrency +public protocol LeafTag: Sendable { func render(_ ctx: LeafContext) throws -> LeafData } /// Tags conforming to this protocol do not get their contents HTML-escaped. public protocol UnsafeUnescapedLeafTag: LeafTag {} -public var defaultTags: [String: LeafTag] = [ +public let defaultLeafTags: [String: LeafTag] = [ "unsafeHTML": UnsafeHTML(), "lowercased": Lowercased(), "uppercased": Uppercased(), @@ -21,6 +22,9 @@ public var defaultTags: [String: LeafTag] = [ "dumpContext": DumpContext() ] +@available(*, deprecated, renamed: "defaultRendererTags", message: "This variable is concurrently unsafe and is deprecated") +public var defaultTags: [String: LeafTag] = defaultLeafTags + struct UnsafeHTML: UnsafeUnescapedLeafTag { func render(_ ctx: LeafContext) throws -> LeafData { guard let str = ctx.parameters.first?.string else { diff --git a/Sources/LeafKit/SendableBox.swift b/Sources/LeafKit/SendableBox.swift new file mode 100644 index 00000000..d42c2194 --- /dev/null +++ b/Sources/LeafKit/SendableBox.swift @@ -0,0 +1,24 @@ +import NIOConcurrencyHelpers + +/// Uses a locking mechanism to ensure Sendability. +internal final class SendableBox: @unchecked Sendable { + private var _value: Value + private let lock = NIOLock() + + internal var value: Value { + get { + lock.withLock { + self._value + } + } + set(newValue) { + lock.withLock { + self._value = newValue + } + } + } + + internal init(_ value: Value) { + self._value = value + } +} diff --git a/Tests/LeafKitTests/TestHelpers.swift b/Tests/LeafKitTests/TestHelpers.swift index 7cf3fa54..8801d6e1 100644 --- a/Tests/LeafKitTests/TestHelpers.swift +++ b/Tests/LeafKitTests/TestHelpers.swift @@ -47,13 +47,15 @@ internal func render(name: String = "test-render", _ template: String, _ context // MARK: - Helper Structs and Classes /// Helper wrapping` LeafRenderer` to preconfigure for simplicity & allow eliding context -internal class TestRenderer { +/// +/// `@unchecked Sendable` because uses locks to guarantee Sendability. +internal class TestRenderer: @unchecked Sendable { var r: LeafRenderer - private let lock: Lock + private let lock: NIOLock private var counter: Int = 0 init(configuration: LeafConfiguration = .init(rootDirectory: "/"), - tags: [String : LeafTag] = defaultTags, + tags: [String : LeafTag] = defaultLeafTags, cache: LeafCache = DefaultLeafCache(), sources: LeafSources = .singleSource(TestFiles()), eventLoop: EventLoop = EmbeddedEventLoop(), @@ -88,8 +90,8 @@ internal class TestRenderer { /// Helper `LeafFiles` struct providing an in-memory thread-safe map of "file names" to "file data" internal struct TestFiles: LeafSource { var files: [String: String] - var lock: Lock - + var lock: NIOLock + init() { files = [:] lock = .init() From eebb46cb30a03a206bec0fea5235fcd2278e6023 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 18:50:46 +0330 Subject: [PATCH 2/6] changes --- Sources/LeafKit/LeafRenderer.swift | 2 +- Sources/LeafKit/LeafSerialize/LeafSerializer.swift | 2 +- Sources/LeafKit/LeafSource/NIOLeafFiles.swift | 2 +- Sources/LeafKit/LeafSyntax/LeafTag.swift | 14 ++++++++++---- Tests/LeafKitTests/TestHelpers.swift | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Sources/LeafKit/LeafRenderer.swift b/Sources/LeafKit/LeafRenderer.swift index 60e2dc4e..70a7bbf3 100644 --- a/Sources/LeafKit/LeafRenderer.swift +++ b/Sources/LeafKit/LeafRenderer.swift @@ -34,7 +34,7 @@ public final class LeafRenderer: Sendable { /// Initial configuration of LeafRenderer. public init( configuration: LeafConfiguration, - tags: [String: LeafTag] = defaultLeafTags, + tags: [String: LeafTag] = defaultTags, cache: LeafCache = DefaultLeafCache(), sources: LeafSources, eventLoop: EventLoop, diff --git a/Sources/LeafKit/LeafSerialize/LeafSerializer.swift b/Sources/LeafKit/LeafSerialize/LeafSerializer.swift index 3ed48ec4..a0ab6579 100644 --- a/Sources/LeafKit/LeafSerialize/LeafSerializer.swift +++ b/Sources/LeafKit/LeafSerialize/LeafSerializer.swift @@ -5,7 +5,7 @@ internal struct LeafSerializer { init( ast: [Syntax], - tags: [String: LeafTag] = defaultLeafTags, + tags: [String: LeafTag] = defaultTags, userInfo: [AnyHashable: Any] = [:], ignoreUnfoundImports: Bool diff --git a/Sources/LeafKit/LeafSource/NIOLeafFiles.swift b/Sources/LeafKit/LeafSource/NIOLeafFiles.swift index 33242416..1eb65806 100644 --- a/Sources/LeafKit/LeafSource/NIOLeafFiles.swift +++ b/Sources/LeafKit/LeafSource/NIOLeafFiles.swift @@ -1,5 +1,5 @@ import Foundation -@preconcurrency import NIO +import NIO /// Reference and default implementation of `LeafSource` adhering object that provides a non-blocking /// file reader for `LeafRenderer` diff --git a/Sources/LeafKit/LeafSyntax/LeafTag.swift b/Sources/LeafKit/LeafSyntax/LeafTag.swift index 11556908..4692314d 100644 --- a/Sources/LeafKit/LeafSyntax/LeafTag.swift +++ b/Sources/LeafKit/LeafSyntax/LeafTag.swift @@ -9,7 +9,7 @@ public protocol LeafTag: Sendable { /// Tags conforming to this protocol do not get their contents HTML-escaped. public protocol UnsafeUnescapedLeafTag: LeafTag {} -public let defaultLeafTags: [String: LeafTag] = [ +let _defaultTags = SendableBox<[String: LeafTag]>([ "unsafeHTML": UnsafeHTML(), "lowercased": Lowercased(), "uppercased": Uppercased(), @@ -20,10 +20,16 @@ public let defaultLeafTags: [String: LeafTag] = [ "count": Count(), "comment": Comment(), "dumpContext": DumpContext() -] +]) -@available(*, deprecated, renamed: "defaultRendererTags", message: "This variable is concurrently unsafe and is deprecated") -public var defaultTags: [String: LeafTag] = defaultLeafTags +public var defaultTags: [String: LeafTag] { + get { + _defaultTags.value + } + set(newValue) { + _defaultTags.value = newValue + } +} struct UnsafeHTML: UnsafeUnescapedLeafTag { func render(_ ctx: LeafContext) throws -> LeafData { diff --git a/Tests/LeafKitTests/TestHelpers.swift b/Tests/LeafKitTests/TestHelpers.swift index 8801d6e1..fb146774 100644 --- a/Tests/LeafKitTests/TestHelpers.swift +++ b/Tests/LeafKitTests/TestHelpers.swift @@ -55,7 +55,7 @@ internal class TestRenderer: @unchecked Sendable { private var counter: Int = 0 init(configuration: LeafConfiguration = .init(rootDirectory: "/"), - tags: [String : LeafTag] = defaultLeafTags, + tags: [String : LeafTag] = defaultTags, cache: LeafCache = DefaultLeafCache(), sources: LeafSources = .singleSource(TestFiles()), eventLoop: EventLoop = EmbeddedEventLoop(), From aeb7356748ba63cbb680fd46279b509d237e34c4 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 20:14:54 +0330 Subject: [PATCH 3/6] Sendable box to require sendable values --- Sources/LeafKit/SendableBox.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/LeafKit/SendableBox.swift b/Sources/LeafKit/SendableBox.swift index d42c2194..2b64d1c7 100644 --- a/Sources/LeafKit/SendableBox.swift +++ b/Sources/LeafKit/SendableBox.swift @@ -1,7 +1,7 @@ import NIOConcurrencyHelpers /// Uses a locking mechanism to ensure Sendability. -internal final class SendableBox: @unchecked Sendable { +internal final class SendableBox: @unchecked Sendable { private var _value: Value private let lock = NIOLock() From cdc40d52523bef2a8837fea454de4fc2b516baac Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 20:46:43 +0330 Subject: [PATCH 4/6] improvements --- Sources/LeafKit/Exports.swift | 11 +-- Sources/LeafKit/LeafConfiguration.swift | 75 ++++++++++--------- Sources/LeafKit/LeafLexer/LeafLexer.swift | 11 +-- Sources/LeafKit/LeafRenderer.swift | 5 +- Sources/LeafKit/LeafSyntax/LeafTag.swift | 7 +- Sources/LeafKit/SendableBox.swift | 24 ------ Tests/LeafKitTests/GHTests/VaporLeafKit.swift | 2 +- 7 files changed, 54 insertions(+), 81 deletions(-) delete mode 100644 Sources/LeafKit/SendableBox.swift diff --git a/Sources/LeafKit/Exports.swift b/Sources/LeafKit/Exports.swift index 72243804..66352676 100644 --- a/Sources/LeafKit/Exports.swift +++ b/Sources/LeafKit/Exports.swift @@ -1,16 +1,9 @@ +import NIOConcurrencyHelpers /// Various helper identities for convenience extension Character { // MARK: - LeafToken specific identities (Internal) - private static let _tagIndicator = SendableBox(Character.octothorpe) - static var tagIndicator: Character { - get { - _tagIndicator.value - } - set(newValue) { - _tagIndicator.value = newValue - } - } + static let tagIndicator = NIOLockedValueBox(Character.octothorpe) var isValidInTagName: Bool { return self.isLowercaseLetter diff --git a/Sources/LeafKit/LeafConfiguration.swift b/Sources/LeafKit/LeafConfiguration.swift index 1953bddb..50cede97 100644 --- a/Sources/LeafKit/LeafConfiguration.swift +++ b/Sources/LeafKit/LeafConfiguration.swift @@ -1,4 +1,5 @@ import Foundation +import NIOConcurrencyHelpers /// General configuration of Leaf /// - Sets the default View directory where templates will be looked for @@ -23,9 +24,9 @@ public struct LeafConfiguration: Sendable { /// - Parameter tagIndicator: Unique tagIndicator - may only be set once. /// - Parameter ignoreUnfoundImports: Ignore unfound imports - may only be set once. public init(rootDirectory: String, tagIndicator: Character, ignoreUnfoundImports: Bool) { - if !Self.started.value { - Character.tagIndicator = tagIndicator - Self.started.value = true + if !Self.started.withLockedValue({ $0 }) { + Character.tagIndicator.withLockedValue { $0 = tagIndicator } + Self.started.withLockedValue { $0 = true } } self._rootDirectory = rootDirectory self._ignoreUnfoundImports = ignoreUnfoundImports @@ -42,53 +43,53 @@ public struct LeafConfiguration: Sendable { } public static var encoding: String.Encoding { - get { _encoding.value } - set { if !Self.running { _encoding.value = newValue } } + get { _encoding.withLockedValue { $0 } } + set { if !Self.running { _encoding.withLockedValue { $0 = newValue } } } } public static var boolFormatter: (Bool) -> String { - get { _boolFormatter.value } - set { if !Self.running { _boolFormatter.value = newValue } } + get { _boolFormatter.withLockedValue { $0 } } + set { if !Self.running { _boolFormatter.withLockedValue { $0 = newValue } } } } public static var intFormatter: (Int) -> String { - get { _intFormatter.value } - set { if !Self.running { _intFormatter.value = newValue } } + get { _intFormatter.withLockedValue { $0 } } + set { if !Self.running { _intFormatter.withLockedValue { $0 = newValue } } } } public static var doubleFormatter: (Double) -> String { - get { _doubleFormatter.value } - set { if !Self.running { _doubleFormatter.value = newValue } } + get { _doubleFormatter.withLockedValue { $0 } } + set { if !Self.running { _doubleFormatter.withLockedValue { $0 = newValue } } } } public static var nilFormatter: () -> String { - get { _nilFormatter.value } - set { if !Self.running { _nilFormatter.value = newValue } } + get { _nilFormatter.withLockedValue { $0 } } + set { if !Self.running { _nilFormatter.withLockedValue { $0 = newValue } } } } public static var voidFormatter: () -> String { - get { _voidFormatter.value } - set { if !Self.running { _voidFormatter.value = newValue } } + get { _voidFormatter.withLockedValue { $0 } } + set { if !Self.running { _voidFormatter.withLockedValue { $0 = newValue } } } } public static var stringFormatter: (String) -> String { - get { _stringFormatter.value } - set { if !Self.running { _stringFormatter.value = newValue } } + get { _stringFormatter.withLockedValue { $0 } } + set { if !Self.running { _stringFormatter.withLockedValue { $0 = newValue } } } } public static var arrayFormatter: ([String]) -> String { - get { _arrayFormatter.value } - set { if !Self.running { _arrayFormatter.value = newValue } } + get { _arrayFormatter.withLockedValue { $0 } } + set { if !Self.running { _arrayFormatter.withLockedValue { $0 = newValue } } } } public static var dictFormatter: ([String: String]) -> String { - get { _dictFormatter.value } - set { if !Self.running { _dictFormatter.value = newValue } } + get { _dictFormatter.withLockedValue { $0 } } + set { if !Self.running { _dictFormatter.withLockedValue { $0 = newValue } } } } public static var dataFormatter: (Data) -> String? { - get { _dataFormatter.value } - set { if !Self.running { _dataFormatter.value = newValue } } + get { _dataFormatter.withLockedValue { $0 } } + set { if !Self.running { _dataFormatter.withLockedValue { $0 = newValue } } } } // MARK: - Internal/Private Only @@ -100,28 +101,28 @@ public struct LeafConfiguration: Sendable { willSet { assert(!accessed, "Changing property after LeafConfiguration has been read has no effect") } } - internal static let _encoding = SendableBox(.utf8) - internal static let _boolFormatter = SendableBox<(Bool) -> String>({ $0.description }) - internal static let _intFormatter = SendableBox<(Int) -> String>({ $0.description }) - internal static let _doubleFormatter = SendableBox<(Double) -> String>({ $0.description }) - internal static let _nilFormatter = SendableBox<(() -> String)>({ "" }) - internal static let _voidFormatter = SendableBox<(() -> String)>({ "" }) - internal static let _stringFormatter = SendableBox<((String) -> String)>({ $0 }) - internal static let _arrayFormatter = SendableBox<(([String]) -> String)>( + internal static let _encoding = NIOLockedValueBox(.utf8) + internal static let _boolFormatter = NIOLockedValueBox<(Bool) -> String>({ $0.description }) + internal static let _intFormatter = NIOLockedValueBox<(Int) -> String>({ $0.description }) + internal static let _doubleFormatter = NIOLockedValueBox<(Double) -> String>({ $0.description }) + internal static let _nilFormatter = NIOLockedValueBox<(() -> String)>({ "" }) + internal static let _voidFormatter = NIOLockedValueBox<(() -> String)>({ "" }) + internal static let _stringFormatter = NIOLockedValueBox<((String) -> String)>({ $0 }) + internal static let _arrayFormatter = NIOLockedValueBox<(([String]) -> String)>( { "[\($0.map {"\"\($0)\""}.joined(separator: ", "))]" } ) - internal static let _dictFormatter = SendableBox<(([String: String]) -> String)>( + internal static let _dictFormatter = NIOLockedValueBox<(([String: String]) -> String)>( { "[\($0.map { "\($0): \"\($1)\"" }.joined(separator: ", "))]" } ) - internal static let _dataFormatter = SendableBox<((Data) -> String?)>( - { String(data: $0, encoding: Self._encoding.value) } + internal static let _dataFormatter = NIOLockedValueBox<((Data) -> String?)>( + { String(data: $0, encoding: Self._encoding.withLockedValue { $0 }) } ) /// Convenience flag for global write-once - private static let started = SendableBox(false) + private static let started = NIOLockedValueBox(false) private static var running: Bool { - assert(!Self.started.value, "LeafKit can only be configured prior to instantiating any LeafRenderer") - return Self.started.value + assert(!Self.started.withLockedValue { $0 }, "LeafKit can only be configured prior to instantiating any LeafRenderer") + return Self.started.withLockedValue { $0 } } /// Convenience flag for local lock-after-access diff --git a/Sources/LeafKit/LeafLexer/LeafLexer.swift b/Sources/LeafKit/LeafLexer/LeafLexer.swift index 09228fd0..096cca91 100644 --- a/Sources/LeafKit/LeafLexer/LeafLexer.swift +++ b/Sources/LeafKit/LeafLexer/LeafLexer.swift @@ -63,7 +63,7 @@ internal struct LeafLexer { private mutating func nextToken() throws -> LeafToken? { // if EOF, return nil - no more to read guard let current = src.peek() else { return nil } - let isTagID = current == .tagIndicator + let isTagID = current == .tagIndicator.withLockedValue { $0 } let isTagVal = current.isValidInTagName let isCol = current == .colon let next = src.peek(aheadBy: 1) @@ -107,10 +107,11 @@ internal struct LeafLexer { /// Consume all data until hitting an unescaped `tagIndicator` and return a `.raw` token private mutating func lexRaw() -> LeafToken { var slice = "" - while let current = src.peek(), current != .tagIndicator { - slice += src.readWhile { $0 != .tagIndicator && $0 != .backSlash } + let tagIndicator = Character.tagIndicator.withLockedValue({ $0 }) + while let current = src.peek(), current != tagIndicator { + slice += src.readWhile { $0 != tagIndicator && $0 != .backSlash } guard let newCurrent = src.peek(), newCurrent == .backSlash else { break } - if let next = src.peek(aheadBy: 1), next == .tagIndicator { + if let next = src.peek(aheadBy: 1), next == tagIndicator { src.pop() } slice += src.pop()!.description @@ -129,7 +130,7 @@ internal struct LeafLexer { return .tagIndicator } else { state = .raw - return .raw(Character.tagIndicator.description) + return .raw((Character.tagIndicator.withLockedValue { $0 }).description) } } diff --git a/Sources/LeafKit/LeafRenderer.swift b/Sources/LeafKit/LeafRenderer.swift index 70a7bbf3..21328043 100644 --- a/Sources/LeafKit/LeafRenderer.swift +++ b/Sources/LeafKit/LeafRenderer.swift @@ -1,4 +1,5 @@ import NIO +import NIOConcurrencyHelpers // MARK: - `LeafRenderer` Summary @@ -25,7 +26,7 @@ public final class LeafRenderer: Sendable { public let sources: LeafSources /// The NIO `EventLoop` on which this instance of `LeafRenderer` will operate public let eventLoop: EventLoop - let _userInfo: SendableBox<[AnyHashable: Any]> + let _userInfo: NIOLoopBound<[AnyHashable: Any]> /// Any custom instance data to use (eg, in Vapor, the `Application` and/or `Request` data) public var userInfo: [AnyHashable: Any] { _userInfo.value @@ -45,7 +46,7 @@ public final class LeafRenderer: Sendable { self.cache = cache self.sources = sources self.eventLoop = eventLoop - self._userInfo = .init(userInfo) + self._userInfo = .init(userInfo, eventLoop: eventLoop) } /// The public interface to `LeafRenderer` diff --git a/Sources/LeafKit/LeafSyntax/LeafTag.swift b/Sources/LeafKit/LeafSyntax/LeafTag.swift index 4692314d..5b23c711 100644 --- a/Sources/LeafKit/LeafSyntax/LeafTag.swift +++ b/Sources/LeafKit/LeafSyntax/LeafTag.swift @@ -1,3 +1,4 @@ +import NIOConcurrencyHelpers import Foundation /// Create custom tags by conforming to this protocol and registering them. @@ -9,7 +10,7 @@ public protocol LeafTag: Sendable { /// Tags conforming to this protocol do not get their contents HTML-escaped. public protocol UnsafeUnescapedLeafTag: LeafTag {} -let _defaultTags = SendableBox<[String: LeafTag]>([ +let _defaultTags = NIOLockedValueBox<[String: LeafTag]>([ "unsafeHTML": UnsafeHTML(), "lowercased": Lowercased(), "uppercased": Uppercased(), @@ -24,10 +25,10 @@ let _defaultTags = SendableBox<[String: LeafTag]>([ public var defaultTags: [String: LeafTag] { get { - _defaultTags.value + _defaultTags.withLockedValue { $0 } } set(newValue) { - _defaultTags.value = newValue + _defaultTags.withLockedValue { $0 = newValue } } } diff --git a/Sources/LeafKit/SendableBox.swift b/Sources/LeafKit/SendableBox.swift deleted file mode 100644 index 2b64d1c7..00000000 --- a/Sources/LeafKit/SendableBox.swift +++ /dev/null @@ -1,24 +0,0 @@ -import NIOConcurrencyHelpers - -/// Uses a locking mechanism to ensure Sendability. -internal final class SendableBox: @unchecked Sendable { - private var _value: Value - private let lock = NIOLock() - - internal var value: Value { - get { - lock.withLock { - self._value - } - } - set(newValue) { - lock.withLock { - self._value = newValue - } - } - } - - internal init(_ value: Value) { - self._value = value - } -} diff --git a/Tests/LeafKitTests/GHTests/VaporLeafKit.swift b/Tests/LeafKitTests/GHTests/VaporLeafKit.swift index 50b0d498..405c44df 100644 --- a/Tests/LeafKitTests/GHTests/VaporLeafKit.swift +++ b/Tests/LeafKitTests/GHTests/VaporLeafKit.swift @@ -118,7 +118,7 @@ final class GHLeafKitIssuesTest: XCTestCase { XCTAssertEqual(page.string, expected) // Page rendering throws expected error - let config = LeafConfiguration(rootDirectory: "/", tagIndicator: Character.tagIndicator, ignoreUnfoundImports: false) + let config = LeafConfiguration(rootDirectory: "/", tagIndicator: Character.tagIndicator.withLockedValue { $0 }, ignoreUnfoundImports: false) XCTAssertThrowsError(try TestRenderer(configuration: config, sources: .singleSource(test)).render(path: "page").wait()) { error in XCTAssertEqual("\(error)", "import(\"body\") should have been resolved BEFORE serialization") } From e791f5cf751753fbaf66f2c843698b86b9c6e647 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 20:50:44 +0330 Subject: [PATCH 5/6] Update DefaultLeafCache.swift --- .../LeafKit/LeafCache/DefaultLeafCache.swift | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift index 52bc53cd..e2fb36c2 100644 --- a/Sources/LeafKit/LeafCache/DefaultLeafCache.swift +++ b/Sources/LeafKit/LeafCache/DefaultLeafCache.swift @@ -47,17 +47,16 @@ public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable { on loop: EventLoop, replace: Bool = false ) -> EventLoopFuture { - // future fails if caching is enabled - guard isEnabled else { return loop.makeSucceededFuture(document) } - - self.lock.lock() - defer { self.lock.unlock() } - // return an error if replace is false and the document name is already in cache - switch (self.cache.keys.contains(document.name),replace) { + self.lock.withLock { + // future fails if caching is enabled + guard __isEnabled else { return loop.makeSucceededFuture(document) } + // return an error if replace is false and the document name is already in cache + switch (self.cache.keys.contains(document.name),replace) { case (true, false): return loop.makeFailedFuture(LeafError(.keyExists(document.name))) default: self.cache[document.name] = document + } + return loop.makeSucceededFuture(document) } - return loop.makeSucceededFuture(document) } /// - Parameters: @@ -68,10 +67,10 @@ public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable { documentName: String, on loop: EventLoop ) -> EventLoopFuture { - guard isEnabled == true else { return loop.makeSucceededFuture(nil) } - self.lock.lock() - defer { self.lock.unlock() } - return loop.makeSucceededFuture(self.cache[documentName]) + self.lock.withLock { + guard __isEnabled == true else { return loop.makeSucceededFuture(nil) } + return loop.makeSucceededFuture(self.cache[documentName]) + } } /// - Parameters: @@ -83,14 +82,12 @@ public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable { _ documentName: String, on loop: EventLoop ) -> EventLoopFuture { - guard isEnabled == true else { return loop.makeFailedFuture(LeafError(.cachingDisabled)) } - - self.lock.lock() - defer { self.lock.unlock() } - - guard self.cache[documentName] != nil else { return loop.makeSucceededFuture(nil) } - self.cache[documentName] = nil - return loop.makeSucceededFuture(true) + self.lock.withLock { + guard __isEnabled == true else { return loop.makeFailedFuture(LeafError(.cachingDisabled)) } + guard self.cache[documentName] != nil else { return loop.makeSucceededFuture(nil) } + self.cache[documentName] = nil + return loop.makeSucceededFuture(true) + } } // MARK: - Internal Only @@ -100,9 +97,9 @@ public final class DefaultLeafCache: SynchronousLeafCache, @unchecked Sendable { /// Blocking file load behavior internal func retrieve(documentName: String) throws -> LeafAST? { - guard isEnabled == true else { throw LeafError(.cachingDisabled) } self.lock.lock() defer { self.lock.unlock() } + guard __isEnabled == true else { throw LeafError(.cachingDisabled) } let result = self.cache[documentName] guard result != nil else { throw LeafError(.noValueForKey(documentName)) } return result From 423abf5825aa5aa930a948f4e52d7195fcead7f8 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 28 Jul 2023 20:52:42 +0330 Subject: [PATCH 6/6] Update LeafSources.swift --- Sources/LeafKit/LeafSource/LeafSources.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/LeafKit/LeafSource/LeafSources.swift b/Sources/LeafKit/LeafSource/LeafSources.swift index a66d389d..86465718 100644 --- a/Sources/LeafKit/LeafSource/LeafSources.swift +++ b/Sources/LeafKit/LeafSource/LeafSources.swift @@ -75,9 +75,7 @@ public final class LeafSources: @unchecked Sendable { guard !s.isEmpty else { return eL.makeFailedFuture(LeafError(.noTemplateExists(t))) } var _more = s let key = _more.removeFirst() - lock.lock() - let source = sources[key]! - lock.unlock() + let source = self.lock.withLock { sources[key]! } let more = _more do {