diff --git a/Sources/Haystack/API/HisItem.swift b/Sources/Haystack/API/HisItem.swift index 5f7741c..f542b2b 100644 --- a/Sources/Haystack/API/HisItem.swift +++ b/Sources/Haystack/API/HisItem.swift @@ -1,5 +1,5 @@ /// A timestamp/value pair. -public struct HisItem { +public struct HisItem: Sendable { public let ts: DateTime public let val: any Val diff --git a/Sources/Haystack/API/HisReadRange.swift b/Sources/Haystack/API/HisReadRange.swift index 6814515..f35c3a6 100644 --- a/Sources/Haystack/API/HisReadRange.swift +++ b/Sources/Haystack/API/HisReadRange.swift @@ -1,7 +1,7 @@ import Foundation /// Query-able DateTime ranges, which support relative and absolute values. -public enum HisReadRange { +public enum HisReadRange: Sendable { case today case yesterday case date(Haystack.Date) diff --git a/Sources/Haystack/DateTime.swift b/Sources/Haystack/DateTime.swift index 0d78428..cfa2303 100644 --- a/Sources/Haystack/DateTime.swift +++ b/Sources/Haystack/DateTime.swift @@ -191,7 +191,7 @@ public struct DateTime: Val { } } -var calendar = Calendar(identifier: .gregorian) +let calendar = Calendar(identifier: .gregorian) // DateTime + Codable extension DateTime { diff --git a/Sources/Haystack/IO/ZincTokenizer.swift b/Sources/Haystack/IO/ZincTokenizer.swift index 2b225c9..312da10 100644 --- a/Sources/Haystack/IO/ZincTokenizer.swift +++ b/Sources/Haystack/IO/ZincTokenizer.swift @@ -18,7 +18,7 @@ class ZincTokenizer { try consume() } - public convenience init(_ string: String) throws { + convenience init(_ string: String) throws { guard let data = string.data(using: .utf8) else { throw ZincTokenizerError.inputIsNotUtf8 } diff --git a/Sources/Haystack/Val.swift b/Sources/Haystack/Val.swift index e5d1ab6..6359983 100644 --- a/Sources/Haystack/Val.swift +++ b/Sources/Haystack/Val.swift @@ -24,7 +24,7 @@ extension Val { } } -public enum ValType: String, CaseIterable { +public enum ValType: String, CaseIterable, Sendable { case Bool case Coord case Date diff --git a/Sources/HaystackClient/Client.swift b/Sources/HaystackClient/Client.swift index 509d766..d04e085 100644 --- a/Sources/HaystackClient/Client.swift +++ b/Sources/HaystackClient/Client.swift @@ -22,7 +22,7 @@ public class Client: API { let fetcher: Fetcher /// Set when `open` is called. - private var authToken: String? = nil + private var authToken: String? private let jsonEncoder = JSONEncoder() private let jsonDecoder = JSONDecoder() diff --git a/Sources/HaystackServer/HaystackServer.swift b/Sources/HaystackServer/HaystackServer.swift index 69f4db9..8152d03 100644 --- a/Sources/HaystackServer/HaystackServer.swift +++ b/Sources/HaystackServer/HaystackServer.swift @@ -3,22 +3,22 @@ import Haystack /// A HaystackServer is a server that implements the Haystack API. /// It translates API calls into operations on the underlying data stores. -public class HaystackServer: API { +public final class HaystackServer: API, Sendable { let recordStore: RecordStore let historyStore: HistoryStore let watchStore: WatchStore - let onInvokeAction: (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid - let onEval: (String) async throws -> Haystack.Grid + let onInvokeAction: @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid + let onEval: @Sendable (String) async throws -> Haystack.Grid public init( recordStore: RecordStore, historyStore: HistoryStore, watchStore: WatchStore, - onInvokeAction: @escaping (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in + onInvokeAction: @escaping @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in GridBuilder().toGrid() }, - onEval: @escaping (String) async throws -> Haystack.Grid = { _ in + onEval: @escaping @Sendable (String) async throws -> Haystack.Grid = { _ in GridBuilder().toGrid() } ) { diff --git a/Sources/HaystackServer/Stores/HistoryStore.swift b/Sources/HaystackServer/Stores/HistoryStore.swift index 2f2fea4..7dbb47a 100644 --- a/Sources/HaystackServer/Stores/HistoryStore.swift +++ b/Sources/HaystackServer/Stores/HistoryStore.swift @@ -1,7 +1,7 @@ import Haystack /// Defines a storage system that allows reading and writing of Haystack history data. -public protocol HistoryStore { +public protocol HistoryStore: Sendable { /// Reads history data for a given ID and time range. func hisRead(id: Haystack.Ref, range: Haystack.HisReadRange) async throws -> [Haystack.Dict] diff --git a/Sources/HaystackServer/Stores/RecordStore.swift b/Sources/HaystackServer/Stores/RecordStore.swift index fad76d4..24d63c3 100644 --- a/Sources/HaystackServer/Stores/RecordStore.swift +++ b/Sources/HaystackServer/Stores/RecordStore.swift @@ -1,7 +1,7 @@ import Haystack /// Defines a storage system that allows reading and writing of Haystack records. -public protocol RecordStore { +public protocol RecordStore: Sendable { /// Reads records from the store based on a list of IDs. func read(ids: [Haystack.Ref]) async throws -> [Haystack.Dict] @@ -12,7 +12,7 @@ public protocol RecordStore { func commitAll(diffs: [RecordDiff]) async throws -> [RecordDiff] } -public struct RecordDiff { +public struct RecordDiff: Sendable { public init( id: Haystack.Ref, old: Haystack.Dict?, diff --git a/Sources/HaystackServer/Stores/WatchStore.swift b/Sources/HaystackServer/Stores/WatchStore.swift index 08f4df3..753eab5 100644 --- a/Sources/HaystackServer/Stores/WatchStore.swift +++ b/Sources/HaystackServer/Stores/WatchStore.swift @@ -2,7 +2,7 @@ import Foundation import Haystack /// Defines a storage system that allows stateful storage of system watches. -public protocol WatchStore { +public protocol WatchStore: Sendable { func read(watchId: String) async throws -> WatchResponse func create(ids: [Haystack.Ref], lease: Haystack.Number?) async throws -> String func addIds(watchId: String, ids: [Haystack.Ref]) async throws @@ -11,7 +11,7 @@ public protocol WatchStore { func delete(watchId: String) async throws } -public struct WatchResponse { +public struct WatchResponse: Sendable { public let ids: [Haystack.Ref] public let lease: Haystack.Number public let lastReported: Foundation.Date? diff --git a/Sources/HaystackServerVapor/Application+Haystack.swift b/Sources/HaystackServerVapor/Application+Haystack.swift new file mode 100644 index 0000000..18a2777 --- /dev/null +++ b/Sources/HaystackServerVapor/Application+Haystack.swift @@ -0,0 +1,26 @@ +import Haystack +import Vapor + +extension Application { + struct HaystackServerKey: StorageKey { + typealias Value = API & Sendable + } + + public var haystack: (API & Sendable)? { + get { + storage[HaystackServerKey.self] + } + set { + storage[HaystackServerKey.self] = newValue + } + } +} + +extension Request { + func haystack() throws -> any API { + guard let haystack = application.haystack else { + fatalError("HaystackServer is not configured in the Vapor application.") + } + return haystack + } +} diff --git a/Sources/HaystackServerVapor/HaystackRouteCollection.swift b/Sources/HaystackServerVapor/HaystackRouteCollection.swift index 4f2498e..2f52068 100644 --- a/Sources/HaystackServerVapor/HaystackRouteCollection.swift +++ b/Sources/HaystackServerVapor/HaystackRouteCollection.swift @@ -3,19 +3,14 @@ import Vapor /// A route collection that exposes Haystack API endpoints. public struct HaystackRouteCollection: RouteCollection { - /// This instance defines all Haystack API processing that is done server-side. - let delegate: any API - - public init(delegate: any API) { - self.delegate = delegate - } + public init() {} public func boot(routes: any Vapor.RoutesBuilder) throws { /// Closes the current authentication session. /// /// https://project-haystack.org/doc/docHaystack/Ops#close - routes.post("close") { _ in - try await delegate.close() + routes.post("close") { request in + try await request.haystack().close() return "" } @@ -23,14 +18,14 @@ public struct HaystackRouteCollection: RouteCollection { /// /// https://project-haystack.org/doc/docHaystack/Ops#about routes.get("about") { request in - try await request.respond(with: delegate.about()) + try await request.respond(with: request.haystack().about()) } /// Queries basic information about the server /// /// https://project-haystack.org/doc/docHaystack/Ops#about routes.post("about") { request in - try await request.respond(with: delegate.about()) + try await request.respond(with: request.haystack().about()) } /// Queries def dicts from the current namespace @@ -48,7 +43,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.defs(filter: filter, limit: limit) + with: request.haystack().defs(filter: filter, limit: limit) ) } @@ -67,7 +62,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.defs(filter: filter, limit: limit) + with: request.haystack().defs(filter: filter, limit: limit) ) } @@ -86,7 +81,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -105,7 +100,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -124,7 +119,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.libs(filter: filter, limit: limit) + with: request.haystack().libs(filter: filter, limit: limit) ) } @@ -143,7 +138,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.ops(filter: filter, limit: limit) + with: request.haystack().ops(filter: filter, limit: limit) ) } @@ -162,7 +157,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.filetypes(filter: filter, limit: limit) + with: request.haystack().filetypes(filter: filter, limit: limit) ) } @@ -181,7 +176,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.filetypes(filter: filter, limit: limit) + with: request.haystack().filetypes(filter: filter, limit: limit) ) } @@ -208,10 +203,10 @@ public struct HaystackRouteCollection: RouteCollection { } if let ids = ids { - return try await request.respond(with: delegate.read(ids: ids)) + return try await request.respond(with: request.haystack().read(ids: ids)) } else if let filter = filter { return try await request.respond( - with: delegate.read(filter: filter, limit: limit) + with: request.haystack().read(filter: filter, limit: limit) ) } else { throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'") @@ -241,10 +236,10 @@ public struct HaystackRouteCollection: RouteCollection { } if let ids = ids { - return try await request.respond(with: delegate.read(ids: ids)) + return try await request.respond(with: request.haystack().read(ids: ids)) } else if let filter = filter { return try await request.respond( - with: delegate.read(filter: filter, limit: limit) + with: request.haystack().read(filter: filter, limit: limit) ) } else { throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'") @@ -264,7 +259,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.nav(navId: navId) + with: request.haystack().nav(navId: navId) ) } @@ -281,7 +276,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.nav(navId: navId) + with: request.haystack().nav(navId: navId) ) } @@ -300,7 +295,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisRead( + with: request.haystack().hisRead( id: id, range: range ) @@ -325,7 +320,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisRead( + with: request.haystack().hisRead( id: id, range: range ) @@ -351,7 +346,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.hisWrite(id: id, items: items) + with: request.haystack().hisWrite(id: id, items: items) ) } @@ -375,7 +370,7 @@ public struct HaystackRouteCollection: RouteCollection { } guard let level = level else { return try await request.respond( - with: delegate.pointWriteStatus(id: id) + with: request.haystack().pointWriteStatus(id: id) ) } @@ -391,7 +386,7 @@ public struct HaystackRouteCollection: RouteCollection { throw Abort(.badRequest, reason: error.localizedDescription) } return try await request.respond( - with: delegate.pointWrite( + with: request.haystack().pointWrite( id: id, level: level, val: val, @@ -424,7 +419,7 @@ public struct HaystackRouteCollection: RouteCollection { if let watchDis = watchDis { return try await request.respond( - with: delegate.watchSubCreate( + with: request.haystack().watchSubCreate( watchDis: watchDis, lease: lease, ids: ids @@ -434,7 +429,7 @@ public struct HaystackRouteCollection: RouteCollection { if let watchId = watchId { return try await request.respond( - with: delegate.watchSubAdd( + with: request.haystack().watchSubAdd( watchId: watchId, lease: lease, ids: ids @@ -460,7 +455,7 @@ public struct HaystackRouteCollection: RouteCollection { if grid.meta.has("close") { return try await request.respond( - with: delegate.watchUnsubDelete( + with: request.haystack().watchUnsubDelete( watchId: watchId ) ) @@ -475,7 +470,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.watchUnsubRemove( + with: request.haystack().watchUnsubRemove( watchId: watchId, ids: ids ) @@ -498,7 +493,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.watchPoll( + with: request.haystack().watchPoll( watchId: watchId, refresh: refresh ) @@ -524,7 +519,7 @@ public struct HaystackRouteCollection: RouteCollection { } return try await request.respond( - with: delegate.invokeAction( + with: request.haystack().invokeAction( id: id, action: action, args: args @@ -547,7 +542,7 @@ public struct HaystackRouteCollection: RouteCollection { throw Abort(.badRequest, reason: error.localizedDescription) } - return try await request.respond(with: delegate.eval(expression: expr)) + return try await request.respond(with: request.haystack().eval(expression: expr)) } } } diff --git a/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift b/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift index d1b96d8..49615f9 100644 --- a/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift +++ b/Tests/HaystackClientNIOIntegrationTests/HaystackClientNIOIntegrationTests.swift @@ -24,7 +24,7 @@ final class HaystackClientNIOIntegrationTests: XCTestCase { override func tearDown() async throws { try await client.close() - try httpClient.syncShutdown() + try await httpClient.shutdown() } func testCloseAndOpen() async throws { diff --git a/Tests/HaystackServerTests/HaystackServerTests.swift b/Tests/HaystackServerTests/HaystackServerTests.swift index f207ee9..da4a126 100644 --- a/Tests/HaystackServerTests/HaystackServerTests.swift +++ b/Tests/HaystackServerTests/HaystackServerTests.swift @@ -504,7 +504,7 @@ final class HaystackServerTests: XCTestCase { /// This is a super inefficient record store based on an in-memory list of Diffs /// /// Any change automatically updates the `mod` DateTime tag on the record -class InMemoryRecordStore: RecordStore { +actor InMemoryRecordStore: RecordStore { var recs: [Haystack.Ref: Haystack.Dict] = [:] init() {} @@ -564,7 +564,7 @@ class InMemoryRecordStore: RecordStore { } /// This is a super inefficient history store based on an in-memory list of histories -class InMemoryHistoryStore: HistoryStore { +actor InMemoryHistoryStore: HistoryStore { /// Maps Refs to histories. These histories are not assumed to be ordered in time (for simplicity). var histories: [Ref: [HisItem]] @@ -604,7 +604,7 @@ class InMemoryHistoryStore: HistoryStore { } /// This is a super inefficient history store based on an in-memory list of histories -class InMemoryWatchStore: WatchStore { +actor InMemoryWatchStore: WatchStore { /// Maps watch IDs to a list of Refs. This is used to track which records are being watched. var watches: [String: Watch] = [:] @@ -659,7 +659,7 @@ class InMemoryWatchStore: WatchStore { watches[watchId] = nil } - public struct Watch: Hashable { + struct Watch: Hashable { let id: String var ids: [Haystack.Ref] let lease: Haystack.Number diff --git a/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift b/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift index 795d928..bde2fb2 100644 --- a/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift +++ b/Tests/HaystackServerVaporTests/HaystackServerVaporTests.swift @@ -6,7 +6,8 @@ import XCTVapor final class HaystackServerVaporTests: XCTestCase { func testGet() throws { let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) + app.haystack = HaystackAPIMock() + try app.register(collection: HaystackRouteCollection()) defer { app.shutdown() } let responseGrid = try GridBuilder() @@ -50,7 +51,8 @@ final class HaystackServerVaporTests: XCTestCase { func testGetBadQuery() throws { let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) + app.haystack = HaystackAPIMock() + try app.register(collection: HaystackRouteCollection()) defer { app.shutdown() } try app.test( @@ -63,7 +65,8 @@ final class HaystackServerVaporTests: XCTestCase { func testPost() throws { let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) + app.haystack = HaystackAPIMock() + try app.register(collection: HaystackRouteCollection()) defer { app.shutdown() } let requestGrid = try GridBuilder() @@ -121,7 +124,8 @@ final class HaystackServerVaporTests: XCTestCase { func testPostBadQuery() throws { let app = Application(.testing) - try app.register(collection: HaystackRouteCollection(delegate: HaystackAPIMock())) + app.haystack = HaystackAPIMock() + try app.register(collection: HaystackRouteCollection()) defer { app.shutdown() } let requestGrid = try GridBuilder() @@ -156,7 +160,7 @@ final class HaystackServerVaporTests: XCTestCase { } } -struct HaystackAPIMock: Haystack.API { +struct HaystackAPIMock: Haystack.API, Sendable { func close() async throws {} func about() async throws -> Haystack.Grid {