@swiftbrowser/native 0.18.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/bin/swiftbrowser-native.mjs +47 -0
- package/dist/blob.d.ts +85 -11
- package/dist/blob.d.ts.map +1 -1
- package/dist/blob.js +184 -34
- package/dist/blob.js.map +1 -1
- package/dist/bridge.d.ts +66 -8
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js.map +1 -1
- package/dist/encoding.d.ts +13 -0
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +24 -0
- package/dist/encoding.js.map +1 -1
- package/dist/fetch.d.ts +27 -13
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +117 -45
- package/dist/fetch.js.map +1 -1
- package/dist/hostRequests.d.ts +24 -6
- package/dist/hostRequests.d.ts.map +1 -1
- package/dist/hostRequests.js +108 -15
- package/dist/hostRequests.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/ios.d.ts +79 -0
- package/dist/ios.d.ts.map +1 -0
- package/dist/ios.js +181 -0
- package/dist/ios.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +16 -7
- package/dist/runtime.js.map +1 -1
- package/dist/subtle.d.ts +1 -1
- package/dist/subtle.d.ts.map +1 -1
- package/dist/subtle.js +9 -5
- package/dist/subtle.js.map +1 -1
- package/dist/webSocket.d.ts +12 -5
- package/dist/webSocket.d.ts.map +1 -1
- package/dist/webSocket.js +37 -11
- package/dist/webSocket.js.map +1 -1
- package/package.json +7 -3
- package/src/blob.ts +215 -34
- package/src/bridge.ts +56 -8
- package/src/encoding.ts +22 -0
- package/src/fetch.ts +111 -48
- package/src/hostRequests.ts +129 -22
- package/src/index.ts +1 -2
- package/src/ios.ts +253 -0
- package/src/runtime.ts +23 -8
- package/src/subtle.ts +10 -6
- package/src/webSocket.ts +37 -13
- package/swift/Packages/SwiftBrowserDSP/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserDSP/README.md +42 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ProgramNode.swift +306 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSP/ScopeView.swift +79 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Machine.swift +1561 -0
- package/swift/Packages/SwiftBrowserDSP/Sources/SwiftBrowserDSPCore/Program.swift +483 -0
- package/swift/Packages/SwiftBrowserLocalModel/Package.swift +35 -0
- package/swift/Packages/SwiftBrowserLocalModel/README.md +66 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LlamaEngine.swift +223 -0
- package/swift/Packages/SwiftBrowserLocalModel/Sources/SwiftBrowserLocalModel/LocalLanguageModel.swift +409 -0
- package/swift/Packages/SwiftBrowserShader/Package.swift +31 -0
- package/swift/Packages/SwiftBrowserShader/README.md +45 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderRenderer.swift +103 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShader/ShaderView.swift +168 -0
- package/swift/Packages/SwiftBrowserShader/Sources/SwiftBrowserShaderCore/ShaderProps.swift +120 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineBridge.swift +921 -0
- package/swift/Sources/SwiftBrowserNative/Host/AudioEngineNodes.swift +70 -0
- package/swift/Sources/SwiftBrowserNative/Host/BodyStream.swift +166 -0
- package/swift/Sources/SwiftBrowserNative/Host/FilesBridge.swift +144 -10
- package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +151 -27
- package/swift/Sources/SwiftBrowserNative/Host/HostRequestBridge.swift +139 -40
- package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +116 -0
- package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +141 -2
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelBridge.swift +511 -0
- package/swift/Sources/SwiftBrowserNative/Host/LanguageModelProvider.swift +195 -0
- package/swift/Sources/SwiftBrowserNative/Host/WebSocketBridge.swift +24 -11
- package/swift/Sources/SwiftBrowserNative/Host/WorkerHost.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeFunctions.swift +1 -1
- package/swift/Sources/SwiftBrowserNative/Runtime/ViewRegistry.swift +57 -0
- package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +12 -7
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +18 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Drawing.swift +378 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +41 -1
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +88 -17
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +87 -21
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PinchTracker.swift +200 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/PopTap.swift +103 -0
- package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +3 -2
- package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +146 -0
- package/swift/Sources/SwiftBrowserProtocol/Drawing.swift +306 -0
- package/swift/Sources/SwiftBrowserProtocol/Props.swift +31 -2
- package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +24 -3
- package/swift/Sources/SwiftBrowserProtocol/Style.swift +87 -0
- package/swift/Sources/SwiftBrowserTree/OpTree.swift +25 -0
- package/wasm/SwiftBrowserNative.wasm +2 -2
- package/dist/native.d.ts +0 -19
- package/dist/native.d.ts.map +0 -1
- package/dist/native.js +0 -17
- package/dist/native.js.map +0 -1
- package/src/native.ts +0 -35
- package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +0 -53
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#if canImport(SwiftUI) && !canImport(FoundationBridge)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
|
|
5
|
+
/// A language model an app brings for `LanguageModelSession`
|
|
6
|
+
/// (LanguageModelBridge.swift) where Apple Intelligence cannot answer: set
|
|
7
|
+
/// `LanguageModels.fallback` at launch, and the app's sessions use it
|
|
8
|
+
/// whenever the system model is unavailable for a reason other than still
|
|
9
|
+
/// downloading. SwiftBrowserLocalModel (Native/Packages) is one, running an
|
|
10
|
+
/// open model with llama.cpp; an app that adds no such package pays nothing
|
|
11
|
+
/// for this hook.
|
|
12
|
+
@MainActor
|
|
13
|
+
public protocol LanguageModelProvider: AnyObject {
|
|
14
|
+
/// Whether it can answer now, asked each time the app asks. A provider
|
|
15
|
+
/// whose model is not on the device yet answers `.unavailable(.modelNotReady)`
|
|
16
|
+
/// and says how its download stands in `downloadState`.
|
|
17
|
+
func availability() async -> LanguageModelAvailability
|
|
18
|
+
|
|
19
|
+
/// For a model the app fetches: its size, whether it is on the device,
|
|
20
|
+
/// and how far a download under way has come. Nil (the default) for a
|
|
21
|
+
/// model that is simply there.
|
|
22
|
+
var downloadState: LanguageModelDownload? { get }
|
|
23
|
+
|
|
24
|
+
/// Starts fetching the model, which the viewer asked for (the app shows
|
|
25
|
+
/// the size first). Nothing for a model without a download.
|
|
26
|
+
func startDownload()
|
|
27
|
+
|
|
28
|
+
/// Stops a download under way and removes the model from the device,
|
|
29
|
+
/// freeing its space; it can be fetched again.
|
|
30
|
+
func deleteDownload() async throws
|
|
31
|
+
|
|
32
|
+
/// A conversation with `instructions` followed throughout, and `tools`
|
|
33
|
+
/// the model may call while it answers. Making one should be cheap: load
|
|
34
|
+
/// what it needs on `prewarm` or the first prompt.
|
|
35
|
+
func makeSession(instructions: String?, tools: [LanguageModelTool]) throws -> any LanguageModelProviderSession
|
|
36
|
+
|
|
37
|
+
/// The models it can run, for the app to offer: empty (the default) for
|
|
38
|
+
/// a provider of one model.
|
|
39
|
+
var choices: [LanguageModelChoice] { get }
|
|
40
|
+
|
|
41
|
+
/// The choice that answers, by its `id`.
|
|
42
|
+
var selectedChoice: String? { get }
|
|
43
|
+
|
|
44
|
+
/// Makes the choice with this `id` the one that answers, from the next session on.
|
|
45
|
+
func select(_ id: String)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// One conversation: each prompt follows the ones before it and their responses.
|
|
49
|
+
@MainActor
|
|
50
|
+
public protocol LanguageModelProviderSession: AnyObject {
|
|
51
|
+
/// Loads what the first response needs, ahead of it.
|
|
52
|
+
func prewarm()
|
|
53
|
+
|
|
54
|
+
/// The response to `prompt`, each element the whole response so far.
|
|
55
|
+
/// Ending the iteration (or cancelling the task iterating it) stops the
|
|
56
|
+
/// model. Failures are best thrown as `LanguageModelError`s, whose kinds
|
|
57
|
+
/// the app reads as `GenerationError` cases.
|
|
58
|
+
func streamResponse(to prompt: String, options: LanguageModelOptions) -> AsyncThrowingStream<String, Error>
|
|
59
|
+
|
|
60
|
+
/// The app is done with the conversation.
|
|
61
|
+
func close()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@MainActor
|
|
65
|
+
extension LanguageModelProvider {
|
|
66
|
+
/// A conversation without tools.
|
|
67
|
+
public func makeSession(instructions: String?) throws -> any LanguageModelProviderSession {
|
|
68
|
+
try makeSession(instructions: instructions, tools: [])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public var choices: [LanguageModelChoice] { [] }
|
|
72
|
+
public var selectedChoice: String? { nil }
|
|
73
|
+
public func select(_ id: String) {}
|
|
74
|
+
public var downloadState: LanguageModelDownload? { nil }
|
|
75
|
+
public func startDownload() {}
|
|
76
|
+
public func deleteDownload() async throws {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// A function the model may call while it answers (FoundationModels' `Tool`):
|
|
80
|
+
/// its name and what it does, its arguments as a JSON schema (an object of
|
|
81
|
+
/// properties; strings, numbers, integers, booleans, arrays and enums), and
|
|
82
|
+
/// `call`, which runs it (in the app) and answers what the model reads.
|
|
83
|
+
public struct LanguageModelTool: Sendable {
|
|
84
|
+
public let name: String
|
|
85
|
+
public let description: String
|
|
86
|
+
public let parameters: JSONValue
|
|
87
|
+
public let call: @Sendable (JSONValue) async throws -> String
|
|
88
|
+
|
|
89
|
+
public init(name: String, description: String, parameters: JSONValue, call: @escaping @Sendable (JSONValue) async throws -> String) {
|
|
90
|
+
self.name = name
|
|
91
|
+
self.description = description
|
|
92
|
+
self.parameters = parameters
|
|
93
|
+
self.call = call
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A model the app can offer to choose (`LanguageModelProvider.choices`).
|
|
98
|
+
public struct LanguageModelChoice: Sendable, Equatable {
|
|
99
|
+
public var id: String
|
|
100
|
+
public var name: String
|
|
101
|
+
/// A line about it: what it is good at.
|
|
102
|
+
public var summary: String?
|
|
103
|
+
/// Where its download stands, for a model the app fetches.
|
|
104
|
+
public var download: LanguageModelDownload?
|
|
105
|
+
|
|
106
|
+
public init(id: String, name: String, summary: String? = nil, download: LanguageModelDownload? = nil) {
|
|
107
|
+
self.id = id
|
|
108
|
+
self.name = name
|
|
109
|
+
self.summary = summary
|
|
110
|
+
self.download = download
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Where a model the app fetches stands.
|
|
115
|
+
public struct LanguageModelDownload: Sendable, Equatable {
|
|
116
|
+
/// The file's size in bytes, where known.
|
|
117
|
+
public var size: Int64?
|
|
118
|
+
/// Whether the model is on the device.
|
|
119
|
+
public var isDownloaded: Bool
|
|
120
|
+
/// From 0 to 1 while a download is under way; nil when none is.
|
|
121
|
+
public var progress: Double?
|
|
122
|
+
/// Why the last download failed, until the next one starts.
|
|
123
|
+
public var failure: String?
|
|
124
|
+
|
|
125
|
+
public init(size: Int64? = nil, isDownloaded: Bool, progress: Double? = nil, failure: String? = nil) {
|
|
126
|
+
self.size = size
|
|
127
|
+
self.isDownloaded = isDownloaded
|
|
128
|
+
self.progress = progress
|
|
129
|
+
self.failure = failure
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Whether a model can answer, as `SystemLanguageModel.Availability`.
|
|
134
|
+
public enum LanguageModelAvailability: Sendable, Equatable {
|
|
135
|
+
case available
|
|
136
|
+
case unavailable(Reason)
|
|
137
|
+
|
|
138
|
+
public enum Reason: String, Sendable {
|
|
139
|
+
case deviceNotEligible
|
|
140
|
+
case appleIntelligenceNotEnabled
|
|
141
|
+
case modelNotReady
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// `GenerationOptions`, as the app sends them.
|
|
146
|
+
public struct LanguageModelOptions: Sendable, Equatable {
|
|
147
|
+
public var temperature: Double?
|
|
148
|
+
public var maximumResponseTokens: Int?
|
|
149
|
+
/// Always the likeliest token, rather than sampling.
|
|
150
|
+
public var greedy: Bool
|
|
151
|
+
|
|
152
|
+
public init(temperature: Double? = nil, maximumResponseTokens: Int? = nil, greedy: Bool = false) {
|
|
153
|
+
self.temperature = temperature
|
|
154
|
+
self.maximumResponseTokens = maximumResponseTokens
|
|
155
|
+
self.greedy = greedy
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// A failed response, by the `GenerationError` case the app reads it as.
|
|
160
|
+
public struct LanguageModelError: Error, CustomStringConvertible, Sendable {
|
|
161
|
+
public enum Kind: String, Sendable {
|
|
162
|
+
/// `assetsUnavailable`
|
|
163
|
+
case unavailable
|
|
164
|
+
/// `exceededContextWindowSize`
|
|
165
|
+
case context
|
|
166
|
+
/// `guardrailViolation`
|
|
167
|
+
case guardrail
|
|
168
|
+
/// `unsupportedLanguageOrLocale`
|
|
169
|
+
case language
|
|
170
|
+
/// `concurrentRequests`
|
|
171
|
+
case concurrent
|
|
172
|
+
case rateLimited
|
|
173
|
+
case refusal
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public let kind: Kind
|
|
177
|
+
public let message: String
|
|
178
|
+
|
|
179
|
+
public init(_ kind: Kind, _ message: String) {
|
|
180
|
+
self.kind = kind
|
|
181
|
+
self.message = message
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// The tagged message the app reads the case from (`context: …`).
|
|
185
|
+
public var description: String { "\(kind.rawValue): \(message)" }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// The models `LanguageModelSession` can use besides Apple Intelligence's.
|
|
189
|
+
public enum LanguageModels {
|
|
190
|
+
/// Used where the system model is unavailable for a reason other than
|
|
191
|
+
/// still downloading (an ineligible device, Apple Intelligence off, a
|
|
192
|
+
/// system before iOS 26). Set it at launch, before the app asks.
|
|
193
|
+
@MainActor public static var fallback: (any LanguageModelProvider)?
|
|
194
|
+
}
|
|
195
|
+
#endif
|
|
@@ -10,22 +10,26 @@ import FoundationBridge
|
|
|
10
10
|
/// first ping is answered (the handshake is done by then), `wsSend` sends
|
|
11
11
|
/// text or bytes, `wsClose` closes it. What a socket receives, and how it
|
|
12
12
|
/// ends, goes to the app as `__sbWebSocketEvent(id, json)`: `open`,
|
|
13
|
-
/// `message` (`text` or
|
|
14
|
-
/// `
|
|
13
|
+
/// `message` (`text` or `binary`), `error` and `close` (`code`, `reason`,
|
|
14
|
+
/// `wasClean`). Binary crosses as base64, or, for a socket the app opened
|
|
15
|
+
/// saying it reads bytes, as a `Uint8Array` after the JSON, which names it
|
|
16
|
+
/// `{ "$bytes": 0 }` (docs/notes/bytes.md); `wsSend` takes it either way. Against the shim (the round trip) the task is the
|
|
15
17
|
/// shim's, over the page's WebSocket.
|
|
16
18
|
@MainActor
|
|
17
19
|
final class WebSocketBridge {
|
|
18
20
|
private let session: URLSession
|
|
19
|
-
private let send: (Int, JSONValue) -> Void
|
|
21
|
+
private let send: (Int, JSONValue, [HostBytes]) -> Void
|
|
20
22
|
private var tasks: [Int: URLSessionWebSocketTask] = [:]
|
|
23
|
+
/// The sockets whose app reads binary messages as bytes.
|
|
24
|
+
private var bytesSockets: Set<Int> = []
|
|
21
25
|
|
|
22
|
-
/// `send` hands an event for socket `id` to the app.
|
|
23
|
-
init(session: URLSession, send: @escaping (Int, JSONValue) -> Void) {
|
|
26
|
+
/// `send` hands an event for socket `id` to the app, with the bytes its JSON names.
|
|
27
|
+
init(session: URLSession, send: @escaping (Int, JSONValue, [HostBytes]) -> Void) {
|
|
24
28
|
self.session = session
|
|
25
29
|
self.send = send
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
func open(id: Int, url string: String, protocols: [String]) {
|
|
32
|
+
func open(id: Int, url string: String, protocols: [String], bytes: Bool = false) {
|
|
29
33
|
guard let url = URL(string: string) else {
|
|
30
34
|
emit(id, ["type": .string("error"), "message": .string("invalid URL")])
|
|
31
35
|
emit(id, ["type": .string("close"), "code": .number(1006), "reason": .string(""), "wasClean": .bool(false)])
|
|
@@ -33,6 +37,7 @@ final class WebSocketBridge {
|
|
|
33
37
|
}
|
|
34
38
|
let task = protocols.isEmpty ? session.webSocketTask(with: url) : session.webSocketTask(with: url, protocols: protocols)
|
|
35
39
|
tasks[id] = task
|
|
40
|
+
if bytes { bytesSockets.insert(id) }
|
|
36
41
|
task.resume()
|
|
37
42
|
task.sendPing { [weak self] error in
|
|
38
43
|
Task { @MainActor [weak self] in
|
|
@@ -47,13 +52,13 @@ final class WebSocketBridge {
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
/// `{ text }` or `{ binary }`
|
|
51
|
-
func send(id: Int, json: String) {
|
|
55
|
+
/// `{ text }` or `{ binary }`: base64, or `{ "$bytes": 0 }` and the view in `bytes`.
|
|
56
|
+
func send(id: Int, json: String, bytes: [HostBytes] = []) {
|
|
52
57
|
guard let task = tasks[id], let fields = (try? JSONValue.parse(json))?.object else { return }
|
|
53
58
|
let message: URLSessionWebSocketTask.Message
|
|
54
59
|
if let text = fields["text"]?.string {
|
|
55
60
|
message = .string(text)
|
|
56
|
-
} else if let
|
|
61
|
+
} else if let data = fields["binary"]?.bytes(from: bytes) {
|
|
57
62
|
message = .data(data)
|
|
58
63
|
} else {
|
|
59
64
|
return
|
|
@@ -69,6 +74,7 @@ final class WebSocketBridge {
|
|
|
69
74
|
|
|
70
75
|
func close(id: Int, code: Int, reason: String) {
|
|
71
76
|
guard let task = tasks.removeValue(forKey: id) else { return }
|
|
77
|
+
defer { bytesSockets.remove(id) }
|
|
72
78
|
task.cancel(with: URLSessionWebSocketTask.CloseCode(rawValue: code) ?? .normalClosure, reason: reason.isEmpty ? nil : Data(reason.utf8))
|
|
73
79
|
emit(id, ["type": .string("close"), "code": .number(Double(code)), "reason": .string(reason), "wasClean": .bool(true)])
|
|
74
80
|
}
|
|
@@ -77,6 +83,7 @@ final class WebSocketBridge {
|
|
|
77
83
|
func invalidate() {
|
|
78
84
|
for task in tasks.values { task.cancel(with: .goingAway, reason: nil) }
|
|
79
85
|
tasks.removeAll()
|
|
86
|
+
bytesSockets.removeAll()
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
private func receive(id: Int, task: URLSessionWebSocketTask) {
|
|
@@ -87,7 +94,12 @@ final class WebSocketBridge {
|
|
|
87
94
|
case .success(let message):
|
|
88
95
|
switch message {
|
|
89
96
|
case .string(let text): self.emit(id, ["type": .string("message"), "text": .string(text)])
|
|
90
|
-
case .data(let data):
|
|
97
|
+
case .data(let data):
|
|
98
|
+
if self.bytesSockets.contains(id) {
|
|
99
|
+
self.send(id, .object(["type": .string("message"), "binary": .object(["$bytes": .number(0)])]), [HostBytes(data, kind: .uint8)])
|
|
100
|
+
} else {
|
|
101
|
+
self.emit(id, ["type": .string("message"), "binary": .string(data.base64EncodedString())])
|
|
102
|
+
}
|
|
91
103
|
@unknown default: break
|
|
92
104
|
}
|
|
93
105
|
self.receive(id: id, task: task)
|
|
@@ -101,6 +113,7 @@ final class WebSocketBridge {
|
|
|
101
113
|
/// The socket ended: a close the server sent (its code and reason), else an error and an unclean close.
|
|
102
114
|
private func fail(id: Int, task: URLSessionWebSocketTask, error: any Error) {
|
|
103
115
|
tasks[id] = nil
|
|
116
|
+
bytesSockets.remove(id)
|
|
104
117
|
let code = task.closeCode
|
|
105
118
|
if code != .invalid {
|
|
106
119
|
let reason = task.closeReason.map { String(decoding: $0, as: UTF8.self) } ?? ""
|
|
@@ -112,7 +125,7 @@ final class WebSocketBridge {
|
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
private func emit(_ id: Int, _ fields: JSONObject) {
|
|
115
|
-
send(id, .object(fields))
|
|
128
|
+
send(id, .object(fields), [])
|
|
116
129
|
}
|
|
117
130
|
}
|
|
118
131
|
#endif
|
|
@@ -62,7 +62,8 @@ final class WorkerHost: @unchecked Sendable {
|
|
|
62
62
|
// MARK: On the worker's thread
|
|
63
63
|
|
|
64
64
|
private func setUp() {
|
|
65
|
-
|
|
65
|
+
// Its own virtual machine, made one at a time with the app's (JavaScriptCoreHost.swift).
|
|
66
|
+
let context = JavaScriptCoreHost.withVirtualMachineLock { JSContext(virtualMachine: JSVirtualMachine())! }
|
|
66
67
|
context.name = "SwiftBrowser worker \(name)"
|
|
67
68
|
context.exceptionHandler = { [weak self] _, exception in
|
|
68
69
|
guard let self else { return }
|
|
@@ -85,7 +86,7 @@ final class WorkerHost: @unchecked Sendable {
|
|
|
85
86
|
for timer in timers.values { timer.invalidate() }
|
|
86
87
|
timers.removeAll()
|
|
87
88
|
context?.exceptionHandler = nil
|
|
88
|
-
context = nil
|
|
89
|
+
JavaScriptCoreHost.withVirtualMachineLock { context = nil }
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
private func install(_ native: JSValue, _ name: String, _ body: @escaping ([JSONValue]) -> JSONValue?) {
|
|
@@ -35,7 +35,7 @@ public struct NativeFunctionError: Error, CustomStringConvertible {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/// Swift the app's JavaScript can call: the function half of the escape hatch
|
|
38
|
-
/// (docs/native.md; `
|
|
38
|
+
/// (docs/native.md; `ViewRegistry` is the view half). The Swift app
|
|
39
39
|
/// registers functions once, before the app loads, and `@swiftbrowser/native`
|
|
40
40
|
/// calls them: `callNative(name, params)` resolves with what an async function
|
|
41
41
|
/// returns, through the host request `app:<name>` (HostRequestBridge.swift);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#if canImport(SwiftUI)
|
|
2
|
+
import Foundation
|
|
3
|
+
import SwiftBrowserProtocol
|
|
4
|
+
import SwiftBrowserTree
|
|
5
|
+
import SwiftUI
|
|
6
|
+
|
|
7
|
+
/// A view a package or the app draws itself, by name: the `registered`
|
|
8
|
+
/// element kind (docs/native.md), which `RegisteredView` in
|
|
9
|
+
/// @swiftbrowser/react emits. A page registers its own element under the
|
|
10
|
+
/// same name (@swiftbrowser/web's `ViewRegistry`), so one component is a
|
|
11
|
+
/// SwiftUI view here and the package's DOM there, drawing what never crosses
|
|
12
|
+
/// the bridge (an audio node's frames, a map's tiles).
|
|
13
|
+
///
|
|
14
|
+
/// ```swift
|
|
15
|
+
/// ViewRegistry.register("MapView") { context in
|
|
16
|
+
/// MapView(region: context.props["region"]) { context.send(["tapped": true]) }
|
|
17
|
+
/// }
|
|
18
|
+
/// ```
|
|
19
|
+
///
|
|
20
|
+
/// ```tsx
|
|
21
|
+
/// <RegisteredView name="MapView" props={{ region }} onEvent={(value) => …} />
|
|
22
|
+
/// ```
|
|
23
|
+
@MainActor
|
|
24
|
+
public enum ViewRegistry {
|
|
25
|
+
/// What a registered view gets: the element's `props` as the app wrote
|
|
26
|
+
/// them, its children (SwiftUI views the app put inside it) and a way to
|
|
27
|
+
/// send a value back, which reaches the element's `onEvent` in JavaScript.
|
|
28
|
+
public struct Context {
|
|
29
|
+
public let id: Int
|
|
30
|
+
public let props: JSONValue
|
|
31
|
+
public let children: [ElementNode]
|
|
32
|
+
public let send: (JSONValue) -> Void
|
|
33
|
+
|
|
34
|
+
/// The children as a view (built on the main actor, as views are).
|
|
35
|
+
@MainActor public var content: some View {
|
|
36
|
+
ForEach(children) { ElementView(node: $0) }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public typealias Builder = (Context) -> AnyView
|
|
41
|
+
private static var builders: [String: Builder] = [:]
|
|
42
|
+
|
|
43
|
+
public static func register<V: View>(_ name: String, @ViewBuilder _ builder: @escaping (Context) -> V) {
|
|
44
|
+
builders[name] = { AnyView(builder($0)) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static func unregister(_ name: String) {
|
|
48
|
+
builders[name] = nil
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static func isRegistered(_ name: String) -> Bool { builders[name] != nil }
|
|
52
|
+
|
|
53
|
+
static func view(name: String, context: Context) -> AnyView? {
|
|
54
|
+
builders[name]?(context)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
#endif
|
|
@@ -61,6 +61,7 @@ struct ElementView: View {
|
|
|
61
61
|
// Lists
|
|
62
62
|
case "list": return AnyView(ListElement(node: node))
|
|
63
63
|
case "section": return AnyView(SectionElement(node: node))
|
|
64
|
+
case "disclosuregroup": return AnyView(DisclosureGroupElement(node: node))
|
|
64
65
|
case "listrow": return AnyView(ListRowElement(node: node))
|
|
65
66
|
case "swipeactions": return AnyView(SwipeActionsElement(node: node))
|
|
66
67
|
// Navigation and presentation
|
|
@@ -88,8 +89,12 @@ struct ElementView: View {
|
|
|
88
89
|
// Shapes and colors
|
|
89
90
|
case "color": return AnyView(ColorElement(node: node))
|
|
90
91
|
case "shape": return ShapeElement(node: node).reportingSize(of: node)
|
|
92
|
+
case "canvas": return AnyView(CanvasElement(node: node))
|
|
93
|
+
case "layout": return AnyView(LayoutElement(node: node))
|
|
94
|
+
case "draggable": return AnyView(DraggableElement(node: node))
|
|
95
|
+
case "dropdestination": return AnyView(DropDestinationElement(node: node))
|
|
91
96
|
// The escape hatch
|
|
92
|
-
case "
|
|
97
|
+
case "registered": return AnyView(RegisteredElement(node: node))
|
|
93
98
|
default: return AnyView(UnsupportedElement(node: node))
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -130,18 +135,18 @@ struct UnsupportedElement: View {
|
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
/// The `
|
|
134
|
-
struct
|
|
138
|
+
/// The `registered` kind: a view registered under `name` (`ViewRegistry`).
|
|
139
|
+
struct RegisteredElement: View {
|
|
135
140
|
let node: ElementNode
|
|
136
141
|
@Environment(\.nativeApp) private var app
|
|
137
142
|
|
|
138
143
|
var body: some View {
|
|
139
|
-
let props =
|
|
144
|
+
let props = RegisteredProps(props: node.props)
|
|
140
145
|
let app = app
|
|
141
|
-
let context =
|
|
142
|
-
app?.dispatch(.
|
|
146
|
+
let context = ViewRegistry.Context(id: node.id, props: props.props, children: node.children) { value in
|
|
147
|
+
app?.dispatch(.registered(node.id, name: props.name, value: value))
|
|
143
148
|
}
|
|
144
|
-
if let view =
|
|
149
|
+
if let view = ViewRegistry.view(name: props.name, context: context) {
|
|
145
150
|
view
|
|
146
151
|
} else {
|
|
147
152
|
UnsupportedElement(node: node)
|
|
@@ -527,7 +527,24 @@ struct SliderElement: View {
|
|
|
527
527
|
let range = props.min...max(props.min, props.max)
|
|
528
528
|
// The drag's start and end, with the value then (`onEditingChanged`).
|
|
529
529
|
let editing = { (editing: Bool) -> Void in app?.dispatch(.slide(node.id, SliderProps(props: node.props).value, editing: editing)) }
|
|
530
|
-
|
|
530
|
+
// The children by what `labels` says each is; the value labels sit at
|
|
531
|
+
// the track's ends (the label itself iOS keeps for accessibility).
|
|
532
|
+
let part = { (name: String) -> ElementNode? in
|
|
533
|
+
guard let index = props.labels.firstIndex(of: name), node.children.indices.contains(index) else { return nil }
|
|
534
|
+
return node.children[index]
|
|
535
|
+
}
|
|
536
|
+
let low = part("min"), high = part("max")
|
|
537
|
+
if low != nil || high != nil {
|
|
538
|
+
let label = part("label")
|
|
539
|
+
let labelView = { label.map { AnyView(ElementView(node: $0)) } ?? AnyView(EmptyView()) }
|
|
540
|
+
let lowView = { low.map { AnyView(ElementView(node: $0)) } ?? AnyView(EmptyView()) }
|
|
541
|
+
let highView = { high.map { AnyView(ElementView(node: $0)) } ?? AnyView(EmptyView()) }
|
|
542
|
+
if let step = props.step, step > 0 {
|
|
543
|
+
Slider(value: value, in: range, step: step, label: labelView, minimumValueLabel: lowView, maximumValueLabel: highView, onEditingChanged: editing)
|
|
544
|
+
} else {
|
|
545
|
+
Slider(value: value, in: range, label: labelView, minimumValueLabel: lowView, maximumValueLabel: highView, onEditingChanged: editing)
|
|
546
|
+
}
|
|
547
|
+
} else if node.children.isEmpty {
|
|
531
548
|
if let step = props.step, step > 0 {
|
|
532
549
|
Slider(value: value, in: range, step: step, onEditingChanged: editing)
|
|
533
550
|
} else {
|