Skip to content

Commit 3657895

Browse files
feat!: Server concurrency support
This removes any Swift structured concurrency `Sendable` warnings.
1 parent 4fea617 commit 3657895

File tree

10 files changed

+83
-55
lines changed

10 files changed

+83
-55
lines changed

Sources/Haystack/API/HisItem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// A timestamp/value pair.
2-
public struct HisItem {
2+
public struct HisItem: Sendable {
33
public let ts: DateTime
44
public let val: any Val
55

Sources/Haystack/API/HisReadRange.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Query-able DateTime ranges, which support relative and absolute values.
4-
public enum HisReadRange {
4+
public enum HisReadRange: Sendable {
55
case today
66
case yesterday
77
case date(Haystack.Date)

Sources/HaystackServer/HaystackServer.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ import Haystack
33

44
/// A HaystackServer is a server that implements the Haystack API.
55
/// It translates API calls into operations on the underlying data stores.
6-
public class HaystackServer: API {
6+
public final class HaystackServer: API, Sendable {
77
let recordStore: RecordStore
88
let historyStore: HistoryStore
99
let watchStore: WatchStore
1010

11-
let onInvokeAction: (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid
12-
let onEval: (String) async throws -> Haystack.Grid
11+
let onInvokeAction: @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid
12+
let onEval: @Sendable (String) async throws -> Haystack.Grid
1313

1414
public init(
1515
recordStore: RecordStore,
1616
historyStore: HistoryStore,
1717
watchStore: WatchStore,
18-
onInvokeAction: @escaping (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in
18+
onInvokeAction: @escaping @Sendable (Haystack.Ref, String, [String: any Haystack.Val]) async throws -> Haystack.Grid = { _, _, _ in
1919
GridBuilder().toGrid()
2020
},
21-
onEval: @escaping (String) async throws -> Haystack.Grid = { _ in
21+
onEval: @escaping @Sendable (String) async throws -> Haystack.Grid = { _ in
2222
GridBuilder().toGrid()
2323
}
2424
) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#if ServerVapor
2+
import Haystack
3+
import Vapor
4+
5+
extension Application {
6+
struct HaystackServerKey: StorageKey {
7+
typealias Value = API & Sendable
8+
}
9+
10+
public var haystack: (API & Sendable)? {
11+
get {
12+
self.storage[HaystackServerKey.self]
13+
}
14+
set {
15+
self.storage[HaystackServerKey.self] = newValue
16+
}
17+
}
18+
}
19+
20+
extension Request {
21+
func haystack() throws -> any API {
22+
guard let haystack = self.application.haystack else {
23+
fatalError("HaystackServer is not configured in the Vapor application.")
24+
}
25+
return haystack
26+
}
27+
}
28+
#endif

Sources/HaystackServer/ServerVapor/HaystackRouteCollection.swift

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,30 @@ import Vapor
44

55
/// A route collection that exposes Haystack API endpoints.
66
public struct HaystackRouteCollection: RouteCollection {
7-
/// This instance defines all Haystack API processing that is done server-side.
8-
let delegate: any API
97

10-
public init(delegate: any API) {
11-
self.delegate = delegate
12-
}
8+
public init() {}
139

1410
public func boot(routes: any Vapor.RoutesBuilder) throws {
1511
/// Closes the current authentication session.
1612
///
1713
/// https://project-haystack.org/doc/docHaystack/Ops#close
18-
routes.post("close") { _ in
19-
try await delegate.close()
14+
routes.post("close") { request in
15+
try await request.haystack().close()
2016
return ""
2117
}
2218

2319
/// Queries basic information about the server
2420
///
2521
/// https://project-haystack.org/doc/docHaystack/Ops#about
2622
routes.get("about") { request in
27-
try await request.respond(with: delegate.about())
23+
try await request.respond(with: request.haystack().about())
2824
}
2925

3026
/// Queries basic information about the server
3127
///
3228
/// https://project-haystack.org/doc/docHaystack/Ops#about
3329
routes.post("about") { request in
34-
try await request.respond(with: delegate.about())
30+
try await request.respond(with: request.haystack().about())
3531
}
3632

3733
/// Queries def dicts from the current namespace
@@ -49,7 +45,7 @@ public struct HaystackRouteCollection: RouteCollection {
4945
}
5046

5147
return try await request.respond(
52-
with: delegate.defs(filter: filter, limit: limit)
48+
with: request.haystack().defs(filter: filter, limit: limit)
5349
)
5450
}
5551

@@ -68,7 +64,7 @@ public struct HaystackRouteCollection: RouteCollection {
6864
}
6965

7066
return try await request.respond(
71-
with: delegate.defs(filter: filter, limit: limit)
67+
with: request.haystack().defs(filter: filter, limit: limit)
7268
)
7369
}
7470

@@ -87,7 +83,7 @@ public struct HaystackRouteCollection: RouteCollection {
8783
}
8884

8985
return try await request.respond(
90-
with: delegate.libs(filter: filter, limit: limit)
86+
with: request.haystack().libs(filter: filter, limit: limit)
9187
)
9288
}
9389

@@ -106,7 +102,7 @@ public struct HaystackRouteCollection: RouteCollection {
106102
}
107103

108104
return try await request.respond(
109-
with: delegate.libs(filter: filter, limit: limit)
105+
with: request.haystack().libs(filter: filter, limit: limit)
110106
)
111107
}
112108

@@ -125,7 +121,7 @@ public struct HaystackRouteCollection: RouteCollection {
125121
}
126122

127123
return try await request.respond(
128-
with: delegate.libs(filter: filter, limit: limit)
124+
with: request.haystack().libs(filter: filter, limit: limit)
129125
)
130126
}
131127

@@ -144,7 +140,7 @@ public struct HaystackRouteCollection: RouteCollection {
144140
}
145141

146142
return try await request.respond(
147-
with: delegate.ops(filter: filter, limit: limit)
143+
with: request.haystack().ops(filter: filter, limit: limit)
148144
)
149145
}
150146

@@ -163,7 +159,7 @@ public struct HaystackRouteCollection: RouteCollection {
163159
}
164160

165161
return try await request.respond(
166-
with: delegate.filetypes(filter: filter, limit: limit)
162+
with: request.haystack().filetypes(filter: filter, limit: limit)
167163
)
168164
}
169165

@@ -182,7 +178,7 @@ public struct HaystackRouteCollection: RouteCollection {
182178
}
183179

184180
return try await request.respond(
185-
with: delegate.filetypes(filter: filter, limit: limit)
181+
with: request.haystack().filetypes(filter: filter, limit: limit)
186182
)
187183
}
188184

@@ -209,10 +205,10 @@ public struct HaystackRouteCollection: RouteCollection {
209205
}
210206

211207
if let ids = ids {
212-
return try await request.respond(with: delegate.read(ids: ids))
208+
return try await request.respond(with: request.haystack().read(ids: ids))
213209
} else if let filter = filter {
214210
return try await request.respond(
215-
with: delegate.read(filter: filter, limit: limit)
211+
with: request.haystack().read(filter: filter, limit: limit)
216212
)
217213
} else {
218214
throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'")
@@ -242,10 +238,10 @@ public struct HaystackRouteCollection: RouteCollection {
242238
}
243239

244240
if let ids = ids {
245-
return try await request.respond(with: delegate.read(ids: ids))
241+
return try await request.respond(with: request.haystack().read(ids: ids))
246242
} else if let filter = filter {
247243
return try await request.respond(
248-
with: delegate.read(filter: filter, limit: limit)
244+
with: request.haystack().read(filter: filter, limit: limit)
249245
)
250246
} else {
251247
throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'")
@@ -265,7 +261,7 @@ public struct HaystackRouteCollection: RouteCollection {
265261
}
266262

267263
return try await request.respond(
268-
with: delegate.nav(navId: navId)
264+
with: request.haystack().nav(navId: navId)
269265
)
270266
}
271267

@@ -282,7 +278,7 @@ public struct HaystackRouteCollection: RouteCollection {
282278
}
283279

284280
return try await request.respond(
285-
with: delegate.nav(navId: navId)
281+
with: request.haystack().nav(navId: navId)
286282
)
287283
}
288284

@@ -301,7 +297,7 @@ public struct HaystackRouteCollection: RouteCollection {
301297
}
302298

303299
return try await request.respond(
304-
with: delegate.hisRead(
300+
with: request.haystack().hisRead(
305301
id: id,
306302
range: range
307303
)
@@ -326,7 +322,7 @@ public struct HaystackRouteCollection: RouteCollection {
326322
}
327323

328324
return try await request.respond(
329-
with: delegate.hisRead(
325+
with: request.haystack().hisRead(
330326
id: id,
331327
range: range
332328
)
@@ -352,7 +348,7 @@ public struct HaystackRouteCollection: RouteCollection {
352348
}
353349

354350
return try await request.respond(
355-
with: delegate.hisWrite(id: id, items: items)
351+
with: request.haystack().hisWrite(id: id, items: items)
356352
)
357353
}
358354

@@ -376,7 +372,7 @@ public struct HaystackRouteCollection: RouteCollection {
376372
}
377373
guard let level = level else {
378374
return try await request.respond(
379-
with: delegate.pointWriteStatus(id: id)
375+
with: request.haystack().pointWriteStatus(id: id)
380376
)
381377
}
382378

@@ -392,7 +388,7 @@ public struct HaystackRouteCollection: RouteCollection {
392388
throw Abort(.badRequest, reason: error.localizedDescription)
393389
}
394390
return try await request.respond(
395-
with: delegate.pointWrite(
391+
with: request.haystack().pointWrite(
396392
id: id,
397393
level: level,
398394
val: val,
@@ -425,7 +421,7 @@ public struct HaystackRouteCollection: RouteCollection {
425421

426422
if let watchDis = watchDis {
427423
return try await request.respond(
428-
with: delegate.watchSubCreate(
424+
with: request.haystack().watchSubCreate(
429425
watchDis: watchDis,
430426
lease: lease,
431427
ids: ids
@@ -435,7 +431,7 @@ public struct HaystackRouteCollection: RouteCollection {
435431

436432
if let watchId = watchId {
437433
return try await request.respond(
438-
with: delegate.watchSubAdd(
434+
with: request.haystack().watchSubAdd(
439435
watchId: watchId,
440436
lease: lease,
441437
ids: ids
@@ -461,7 +457,7 @@ public struct HaystackRouteCollection: RouteCollection {
461457

462458
if grid.meta.has("close") {
463459
return try await request.respond(
464-
with: delegate.watchUnsubDelete(
460+
with: request.haystack().watchUnsubDelete(
465461
watchId: watchId
466462
)
467463
)
@@ -476,7 +472,7 @@ public struct HaystackRouteCollection: RouteCollection {
476472
}
477473

478474
return try await request.respond(
479-
with: delegate.watchUnsubRemove(
475+
with: request.haystack().watchUnsubRemove(
480476
watchId: watchId,
481477
ids: ids
482478
)
@@ -499,7 +495,7 @@ public struct HaystackRouteCollection: RouteCollection {
499495
}
500496

501497
return try await request.respond(
502-
with: delegate.watchPoll(
498+
with: request.haystack().watchPoll(
503499
watchId: watchId,
504500
refresh: refresh
505501
)
@@ -525,7 +521,7 @@ public struct HaystackRouteCollection: RouteCollection {
525521
}
526522

527523
return try await request.respond(
528-
with: delegate.invokeAction(
524+
with: request.haystack().invokeAction(
529525
id: id,
530526
action: action,
531527
args: args
@@ -548,7 +544,7 @@ public struct HaystackRouteCollection: RouteCollection {
548544
throw Abort(.badRequest, reason: error.localizedDescription)
549545
}
550546

551-
return try await request.respond(with: delegate.eval(expression: expr))
547+
return try await request.respond(with: request.haystack().eval(expression: expr))
552548
}
553549
}
554550
}

Sources/HaystackServer/Stores/HistoryStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Haystack
22

33
/// Defines a storage system that allows reading and writing of Haystack history data.
4-
public protocol HistoryStore {
4+
public protocol HistoryStore: Sendable {
55
/// Reads history data for a given ID and time range.
66
func hisRead(id: Haystack.Ref, range: Haystack.HisReadRange) async throws -> [Haystack.Dict]
77

Sources/HaystackServer/Stores/RecordStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Haystack
22

33
/// Defines a storage system that allows reading and writing of Haystack records.
4-
public protocol RecordStore {
4+
public protocol RecordStore: Sendable {
55
/// Reads records from the store based on a list of IDs.
66
func read(ids: [Haystack.Ref]) async throws -> [Haystack.Dict]
77

@@ -12,7 +12,7 @@ public protocol RecordStore {
1212
func commitAll(diffs: [RecordDiff]) async throws -> [RecordDiff]
1313
}
1414

15-
public struct RecordDiff {
15+
public struct RecordDiff: Sendable {
1616
public init(
1717
id: Haystack.Ref,
1818
old: Haystack.Dict?,

Sources/HaystackServer/Stores/WatchStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import Haystack
33

44
/// Defines a storage system that allows stateful storage of system watches.
5-
public protocol WatchStore {
5+
public protocol WatchStore: Sendable {
66
func read(watchId: String) async throws -> WatchResponse
77
func create(ids: [Haystack.Ref], lease: Haystack.Number?) async throws -> String
88
func addIds(watchId: String, ids: [Haystack.Ref]) async throws
@@ -11,7 +11,7 @@ public protocol WatchStore {
1111
func delete(watchId: String) async throws
1212
}
1313

14-
public struct WatchResponse {
14+
public struct WatchResponse: Sendable {
1515
public let ids: [Haystack.Ref]
1616
public let lease: Haystack.Number
1717
public let lastReported: Foundation.Date?

0 commit comments

Comments
 (0)