Skip to content

Commit ef798f1

Browse files
feat!: Server concurrency support
This removes any Swift structured concurrency `Sendable` warnings.
1 parent 6b45620 commit ef798f1

File tree

10 files changed

+83
-56
lines changed

10 files changed

+83
-56
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+
storage[HaystackServerKey.self]
13+
}
14+
set {
15+
storage[HaystackServerKey.self] = newValue
16+
}
17+
}
18+
}
19+
20+
extension Request {
21+
func haystack() throws -> any API {
22+
guard let haystack = 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 & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,29 @@ 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
9-
10-
public init(delegate: any API) {
11-
self.delegate = delegate
12-
}
7+
public init() {}
138

149
public func boot(routes: any Vapor.RoutesBuilder) throws {
1510
/// Closes the current authentication session.
1611
///
1712
/// https://project-haystack.org/doc/docHaystack/Ops#close
18-
routes.post("close") { _ in
19-
try await delegate.close()
13+
routes.post("close") { request in
14+
try await request.haystack().close()
2015
return ""
2116
}
2217

2318
/// Queries basic information about the server
2419
///
2520
/// https://project-haystack.org/doc/docHaystack/Ops#about
2621
routes.get("about") { request in
27-
try await request.respond(with: delegate.about())
22+
try await request.respond(with: request.haystack().about())
2823
}
2924

3025
/// Queries basic information about the server
3126
///
3227
/// https://project-haystack.org/doc/docHaystack/Ops#about
3328
routes.post("about") { request in
34-
try await request.respond(with: delegate.about())
29+
try await request.respond(with: request.haystack().about())
3530
}
3631

3732
/// Queries def dicts from the current namespace
@@ -49,7 +44,7 @@ public struct HaystackRouteCollection: RouteCollection {
4944
}
5045

5146
return try await request.respond(
52-
with: delegate.defs(filter: filter, limit: limit)
47+
with: request.haystack().defs(filter: filter, limit: limit)
5348
)
5449
}
5550

@@ -68,7 +63,7 @@ public struct HaystackRouteCollection: RouteCollection {
6863
}
6964

7065
return try await request.respond(
71-
with: delegate.defs(filter: filter, limit: limit)
66+
with: request.haystack().defs(filter: filter, limit: limit)
7267
)
7368
}
7469

@@ -87,7 +82,7 @@ public struct HaystackRouteCollection: RouteCollection {
8782
}
8883

8984
return try await request.respond(
90-
with: delegate.libs(filter: filter, limit: limit)
85+
with: request.haystack().libs(filter: filter, limit: limit)
9186
)
9287
}
9388

@@ -106,7 +101,7 @@ public struct HaystackRouteCollection: RouteCollection {
106101
}
107102

108103
return try await request.respond(
109-
with: delegate.libs(filter: filter, limit: limit)
104+
with: request.haystack().libs(filter: filter, limit: limit)
110105
)
111106
}
112107

@@ -125,7 +120,7 @@ public struct HaystackRouteCollection: RouteCollection {
125120
}
126121

127122
return try await request.respond(
128-
with: delegate.libs(filter: filter, limit: limit)
123+
with: request.haystack().libs(filter: filter, limit: limit)
129124
)
130125
}
131126

@@ -144,7 +139,7 @@ public struct HaystackRouteCollection: RouteCollection {
144139
}
145140

146141
return try await request.respond(
147-
with: delegate.ops(filter: filter, limit: limit)
142+
with: request.haystack().ops(filter: filter, limit: limit)
148143
)
149144
}
150145

@@ -163,7 +158,7 @@ public struct HaystackRouteCollection: RouteCollection {
163158
}
164159

165160
return try await request.respond(
166-
with: delegate.filetypes(filter: filter, limit: limit)
161+
with: request.haystack().filetypes(filter: filter, limit: limit)
167162
)
168163
}
169164

@@ -182,7 +177,7 @@ public struct HaystackRouteCollection: RouteCollection {
182177
}
183178

184179
return try await request.respond(
185-
with: delegate.filetypes(filter: filter, limit: limit)
180+
with: request.haystack().filetypes(filter: filter, limit: limit)
186181
)
187182
}
188183

@@ -209,10 +204,10 @@ public struct HaystackRouteCollection: RouteCollection {
209204
}
210205

211206
if let ids = ids {
212-
return try await request.respond(with: delegate.read(ids: ids))
207+
return try await request.respond(with: request.haystack().read(ids: ids))
213208
} else if let filter = filter {
214209
return try await request.respond(
215-
with: delegate.read(filter: filter, limit: limit)
210+
with: request.haystack().read(filter: filter, limit: limit)
216211
)
217212
} else {
218213
throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'")
@@ -242,10 +237,10 @@ public struct HaystackRouteCollection: RouteCollection {
242237
}
243238

244239
if let ids = ids {
245-
return try await request.respond(with: delegate.read(ids: ids))
240+
return try await request.respond(with: request.haystack().read(ids: ids))
246241
} else if let filter = filter {
247242
return try await request.respond(
248-
with: delegate.read(filter: filter, limit: limit)
243+
with: request.haystack().read(filter: filter, limit: limit)
249244
)
250245
} else {
251246
throw Abort(.badRequest, reason: "Read request must have either 'id' or 'filter'")
@@ -265,7 +260,7 @@ public struct HaystackRouteCollection: RouteCollection {
265260
}
266261

267262
return try await request.respond(
268-
with: delegate.nav(navId: navId)
263+
with: request.haystack().nav(navId: navId)
269264
)
270265
}
271266

@@ -282,7 +277,7 @@ public struct HaystackRouteCollection: RouteCollection {
282277
}
283278

284279
return try await request.respond(
285-
with: delegate.nav(navId: navId)
280+
with: request.haystack().nav(navId: navId)
286281
)
287282
}
288283

@@ -301,7 +296,7 @@ public struct HaystackRouteCollection: RouteCollection {
301296
}
302297

303298
return try await request.respond(
304-
with: delegate.hisRead(
299+
with: request.haystack().hisRead(
305300
id: id,
306301
range: range
307302
)
@@ -326,7 +321,7 @@ public struct HaystackRouteCollection: RouteCollection {
326321
}
327322

328323
return try await request.respond(
329-
with: delegate.hisRead(
324+
with: request.haystack().hisRead(
330325
id: id,
331326
range: range
332327
)
@@ -352,7 +347,7 @@ public struct HaystackRouteCollection: RouteCollection {
352347
}
353348

354349
return try await request.respond(
355-
with: delegate.hisWrite(id: id, items: items)
350+
with: request.haystack().hisWrite(id: id, items: items)
356351
)
357352
}
358353

@@ -376,7 +371,7 @@ public struct HaystackRouteCollection: RouteCollection {
376371
}
377372
guard let level = level else {
378373
return try await request.respond(
379-
with: delegate.pointWriteStatus(id: id)
374+
with: request.haystack().pointWriteStatus(id: id)
380375
)
381376
}
382377

@@ -392,7 +387,7 @@ public struct HaystackRouteCollection: RouteCollection {
392387
throw Abort(.badRequest, reason: error.localizedDescription)
393388
}
394389
return try await request.respond(
395-
with: delegate.pointWrite(
390+
with: request.haystack().pointWrite(
396391
id: id,
397392
level: level,
398393
val: val,
@@ -425,7 +420,7 @@ public struct HaystackRouteCollection: RouteCollection {
425420

426421
if let watchDis = watchDis {
427422
return try await request.respond(
428-
with: delegate.watchSubCreate(
423+
with: request.haystack().watchSubCreate(
429424
watchDis: watchDis,
430425
lease: lease,
431426
ids: ids
@@ -435,7 +430,7 @@ public struct HaystackRouteCollection: RouteCollection {
435430

436431
if let watchId = watchId {
437432
return try await request.respond(
438-
with: delegate.watchSubAdd(
433+
with: request.haystack().watchSubAdd(
439434
watchId: watchId,
440435
lease: lease,
441436
ids: ids
@@ -461,7 +456,7 @@ public struct HaystackRouteCollection: RouteCollection {
461456

462457
if grid.meta.has("close") {
463458
return try await request.respond(
464-
with: delegate.watchUnsubDelete(
459+
with: request.haystack().watchUnsubDelete(
465460
watchId: watchId
466461
)
467462
)
@@ -476,7 +471,7 @@ public struct HaystackRouteCollection: RouteCollection {
476471
}
477472

478473
return try await request.respond(
479-
with: delegate.watchUnsubRemove(
474+
with: request.haystack().watchUnsubRemove(
480475
watchId: watchId,
481476
ids: ids
482477
)
@@ -499,7 +494,7 @@ public struct HaystackRouteCollection: RouteCollection {
499494
}
500495

501496
return try await request.respond(
502-
with: delegate.watchPoll(
497+
with: request.haystack().watchPoll(
503498
watchId: watchId,
504499
refresh: refresh
505500
)
@@ -525,7 +520,7 @@ public struct HaystackRouteCollection: RouteCollection {
525520
}
526521

527522
return try await request.respond(
528-
with: delegate.invokeAction(
523+
with: request.haystack().invokeAction(
529524
id: id,
530525
action: action,
531526
args: args
@@ -548,7 +543,7 @@ public struct HaystackRouteCollection: RouteCollection {
548543
throw Abort(.badRequest, reason: error.localizedDescription)
549544
}
550545

551-
return try await request.respond(with: delegate.eval(expression: expr))
546+
return try await request.respond(with: request.haystack().eval(expression: expr))
552547
}
553548
}
554549
}

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)