@tokamakdev/platform-macos-x64 0.1.1-beta.127 → 0.1.1-beta.129
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/macos-x64/build/entrypoint +81 -31
- package/macos-x64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/macos-x64/native-shell/TokamakControls.swift +2 -30
- package/macos-x64/native-shell/TokamakExports.swift +561 -0
- package/macos-x64/native-shell/TokamakPlugins.swift +58 -16
- package/macos-x64/native-shell/TokamakResources.swift +14 -52
- package/macos-x64/platform-pack.json +1 -3
- package/macos-x64/tools/tokamak-swift-describe +4 -0
- package/package.json +1 -1
- package/macos-x64/native-shell/TokamakBindings.swift +0 -324
|
@@ -11,8 +11,71 @@ fi
|
|
|
11
11
|
if [[ $# -eq 1 && $1 == certs ]]; then
|
|
12
12
|
exec "$apple_tool" inventory
|
|
13
13
|
fi
|
|
14
|
+
|
|
15
|
+
# Describes the methods PLUGIN/apple/Exports.swift exports on each PLATFORM
|
|
16
|
+
# into OUTPUT/description.json, with the dispatch that serves them,
|
|
17
|
+
# OUTPUT/Exports.generated.swift. The compiler's symbol graph of the plugin,
|
|
18
|
+
# built with the shell for each platform, gives the methods' types.
|
|
19
|
+
describe() {
|
|
20
|
+
local plugin=$1 output=$2
|
|
21
|
+
shift 2
|
|
22
|
+
if ! xcrun --sdk iphonesimulator --show-sdk-path > /dev/null 2>&1; then
|
|
23
|
+
echo "Describing an Apple plugin needs Xcode" >&2
|
|
24
|
+
exit 69
|
|
25
|
+
fi
|
|
26
|
+
# Global, for the exit trap.
|
|
27
|
+
temporary=$(mktemp -d "${TMPDIR:-/tmp}/tokamak-describe.XXXXXX")
|
|
28
|
+
trap 'rm -rf "$temporary"' EXIT
|
|
29
|
+
printf '%s\n' 'func tokamakPlugins(app: TokamakApp) -> [any TokamakPlugin] { [] }' > "$temporary/registry.swift"
|
|
30
|
+
local sources=("$pack/native-shell"/*.swift "$temporary/registry.swift") source
|
|
31
|
+
while IFS= read -r -d '' source; do
|
|
32
|
+
[[ ${source##*/} == Exports.generated.swift ]] || sources+=("$source")
|
|
33
|
+
done < <(find "$plugin/apple" -name '*.swift' -print0 | sort -z)
|
|
34
|
+
local graphs=() compiles=() platform sdk triple
|
|
35
|
+
for platform in "$@"; do
|
|
36
|
+
case "$platform" in
|
|
37
|
+
ios)
|
|
38
|
+
sdk=iphonesimulator
|
|
39
|
+
triple=$(uname -m)-apple-ios$(<"$pack/build/ios-deployment-target")-simulator
|
|
40
|
+
;;
|
|
41
|
+
macos)
|
|
42
|
+
sdk=macosx
|
|
43
|
+
triple=$(uname -m)-apple-macos$(<"$pack/build/macos-deployment-target")
|
|
44
|
+
;;
|
|
45
|
+
*)
|
|
46
|
+
echo "Apple plugins run on ios and macos, not $platform" >&2
|
|
47
|
+
exit 2
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
mkdir "$temporary/$platform"
|
|
51
|
+
# The platforms compile at once, each into its own log.
|
|
52
|
+
xcrun --sdk "$sdk" swiftc -emit-module -module-name TokamakDescribed -swift-version 5 \
|
|
53
|
+
-target "$triple" -F "$pack/frameworks" -module-cache-path "$temporary/cache" \
|
|
54
|
+
-emit-module-path "$temporary/$platform/TokamakDescribed.swiftmodule" \
|
|
55
|
+
-emit-symbol-graph -emit-symbol-graph-dir "$temporary/$platform" \
|
|
56
|
+
-symbol-graph-minimum-access-level private "${sources[@]}" > "$temporary/$platform.log" 2>&1 &
|
|
57
|
+
compiles+=("$!")
|
|
58
|
+
graphs+=("$platform=$temporary/$platform/TokamakDescribed.symbols.json")
|
|
59
|
+
done
|
|
60
|
+
# The platforms report the same diagnostics, so only the first failure's
|
|
61
|
+
# show, or else the first platform's warnings.
|
|
62
|
+
local platforms=("$@") index failed=
|
|
63
|
+
for index in "${!compiles[@]}"; do
|
|
64
|
+
if ! wait "${compiles[index]}" && [[ -z $failed ]]; then
|
|
65
|
+
failed=$index
|
|
66
|
+
fi
|
|
67
|
+
done
|
|
68
|
+
cat "$temporary/${platforms[${failed:-0}]}.log" >&2
|
|
69
|
+
[[ -z $failed ]] || exit 1
|
|
70
|
+
"$pack/tools/tokamak-swift-describe" "$plugin/apple/Exports.swift" "$output" "${graphs[@]}"
|
|
71
|
+
}
|
|
72
|
+
if [[ $# -ge 4 && $1 == describe ]]; then
|
|
73
|
+
describe "${@:2}"
|
|
74
|
+
exit
|
|
75
|
+
fi
|
|
76
|
+
|
|
14
77
|
if [[ $# -ne 3 || $1 != build ]]; then
|
|
15
|
-
echo "usage: entrypoint build INPUT OUTPUT | entrypoint certs" >&2
|
|
78
|
+
echo "usage: entrypoint build INPUT OUTPUT | entrypoint describe PLUGIN OUTPUT PLATFORM... | entrypoint certs" >&2
|
|
16
79
|
exit 2
|
|
17
80
|
fi
|
|
18
81
|
|
|
@@ -169,46 +232,33 @@ if [[ ! -f "$pack/native-shell/TokamakShell.swift" ]]; then
|
|
|
169
232
|
fi
|
|
170
233
|
|
|
171
234
|
# A plugin's class is a Swift type name, which the registry creates, and its
|
|
172
|
-
# sources are Swift.
|
|
235
|
+
# sources are the Swift files in its native directory.
|
|
173
236
|
swift_type='^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$'
|
|
174
|
-
# Swift requires unique file names in a module, so
|
|
175
|
-
# <
|
|
237
|
+
# Swift requires unique file names in a module, so a plugin's source
|
|
238
|
+
# <directory>/<name> compiles as <plugin>-<directory>-<name>.
|
|
176
239
|
plugin_sources=$temporary/plugin-sources
|
|
177
240
|
mkdir "$plugin_sources"
|
|
178
|
-
add_plugin_source() {
|
|
179
|
-
local plugin_id=$1 source=$2
|
|
180
|
-
local key=${source%/*}
|
|
181
|
-
local named=$plugin_sources/$plugin_id-${key##*/}-${source##*/}
|
|
182
|
-
cp "$source" "$named"
|
|
183
|
-
swift_sources+=("$named")
|
|
184
|
-
}
|
|
185
241
|
registry=$temporary/TokamakPluginRegistry.swift
|
|
186
242
|
printf '%s\n\n%s\n%s\n' 'import Foundation' 'func tokamakPlugins(app: TokamakApp) -> [any TokamakPlugin] {' ' [' > "$registry"
|
|
187
243
|
for plugin in "$input/plugins"/*/; do
|
|
188
244
|
plugin_id=$(basename "$plugin")
|
|
189
245
|
class=$(cat "$plugin/class" 2>/dev/null || true)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
246
|
+
if [[ ! "$class" =~ $swift_type ]]; then
|
|
247
|
+
echo "plugin '$plugin_id' has invalid $platform class '$class'" >&2
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
250
|
+
printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$plugin_id" >> "$registry"
|
|
251
|
+
[[ -d "$plugin/native" ]] || continue
|
|
252
|
+
while IFS= read -r -d '' source; do
|
|
253
|
+
relative=${source#"$plugin/native/"}
|
|
254
|
+
named=$plugin_sources/$plugin_id-${relative//\//-}
|
|
255
|
+
if [[ -e "$named" ]]; then
|
|
256
|
+
echo "plugin '$plugin_id' source $relative stages as ${named##*/}, as another source does; rename one" >&2
|
|
194
257
|
exit 1
|
|
195
258
|
fi
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for source in "$plugin/bindings"/*.swift; do
|
|
200
|
-
printf ' TokamakBindings_%s(host: TokamakHost(app: app, plugin: "%s")),\n' "${plugin_id//-/_}" "$plugin_id" >> "$registry"
|
|
201
|
-
add_plugin_source "$plugin_id" "$source"
|
|
202
|
-
done
|
|
203
|
-
for source in "$plugin/sources"/*; do
|
|
204
|
-
case "$source" in
|
|
205
|
-
*.swift) add_plugin_source "$plugin_id" "$source" ;;
|
|
206
|
-
*)
|
|
207
|
-
echo "Apple plugin source must be Swift: $source" >&2
|
|
208
|
-
exit 1
|
|
209
|
-
;;
|
|
210
|
-
esac
|
|
211
|
-
done
|
|
259
|
+
cp "$source" "$named"
|
|
260
|
+
swift_sources+=("$named")
|
|
261
|
+
done < <(find "$plugin/native" -name '*.swift' -print0 | sort -z)
|
|
212
262
|
done
|
|
213
263
|
printf '%s\n' ' ]' '}' >> "$registry"
|
|
214
264
|
|
|
Binary file
|
|
@@ -9,10 +9,9 @@ final class TokamakCorePlugin: TokamakPlugin {
|
|
|
9
9
|
|
|
10
10
|
init(host: TokamakHost) { self.host = host }
|
|
11
11
|
|
|
12
|
-
/// WebView operations
|
|
13
|
-
/// WebKit and UIKit require.
|
|
12
|
+
/// WebView operations run on the main thread, which WebKit requires.
|
|
14
13
|
func executor(for method: String) -> DispatchQueue {
|
|
15
|
-
method.hasPrefix("webview.")
|
|
14
|
+
method.hasPrefix("webview.") ? .main : queue
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
func call(method: String, arguments: Any, reply: @escaping TokamakPluginReply) -> (() -> Void)? {
|
|
@@ -22,11 +21,6 @@ final class TokamakCorePlugin: TokamakPlugin {
|
|
|
22
21
|
Result { () throws(TokamakPluginError) -> Any? in
|
|
23
22
|
try host.resources.get(arguments, as: Data.self)
|
|
24
23
|
})
|
|
25
|
-
case "resource.release":
|
|
26
|
-
host.resources.release(arguments)
|
|
27
|
-
reply(.success(nil))
|
|
28
|
-
case "resource.root":
|
|
29
|
-
reply(Result { () throws(TokamakPluginError) -> Any? in try root(arguments) })
|
|
30
24
|
case "webview.getCurrentPage":
|
|
31
25
|
reply(.success(host.webView?.url?.absoluteString))
|
|
32
26
|
case "webview.navigate":
|
|
@@ -39,28 +33,6 @@ final class TokamakCorePlugin: TokamakPlugin {
|
|
|
39
33
|
return nil
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
/// `resource.scope` opens a scope for the objects an invocation receives,
|
|
43
|
-
/// which ends with the subscription.
|
|
44
|
-
func subscribe(method: String, arguments: Any, reply: @escaping TokamakPluginReply)
|
|
45
|
-
throws(TokamakPluginError) -> () -> Void
|
|
46
|
-
{
|
|
47
|
-
guard method == "resource.scope" else {
|
|
48
|
-
throw .notSupported("\(id).\(method) is not supported")
|
|
49
|
-
}
|
|
50
|
-
let scope = host.resources.open()
|
|
51
|
-
reply(.success(scope))
|
|
52
|
-
return { [host] in host.resources.end(scope) }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/// Retains the app object `name` in `scope` and returns its ID: the
|
|
56
|
-
/// `presenter` a native screen is presented from.
|
|
57
|
-
private func root(_ arguments: Any) throws(TokamakPluginError) -> String {
|
|
58
|
-
let arguments = arguments as? [String: Any]
|
|
59
|
-
guard let scope = arguments?["scope"] as? String, arguments?["name"] as? String == "presenter"
|
|
60
|
-
else { throw .notSupported("\(id).resource.root has no such object") }
|
|
61
|
-
return try host.resources.retain(host.presenter(), in: scope)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
36
|
/// Starts navigation to the app URL `path`, relative to the current page or
|
|
65
37
|
/// the app origin.
|
|
66
38
|
private func navigate(to path: Any) -> Result<Void, TokamakPluginError> {
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// A plugin whose `apple/Exports.swift` lists the methods the Worker calls
|
|
4
|
+
/// and the events it receives. `tok plugin generate apple` reads it and writes
|
|
5
|
+
/// the plugin's dispatch and TypeScript API.
|
|
6
|
+
protocol TokamakExports: TokamakPlugin {
|
|
7
|
+
/// The plugin's host, whose resources hold the native references methods take.
|
|
8
|
+
var host: TokamakHost { get }
|
|
9
|
+
|
|
10
|
+
/// Lists the exported methods and events. The app never calls it.
|
|
11
|
+
static func exports(_ e: inout TokamakExporter<Self>)
|
|
12
|
+
|
|
13
|
+
/// `event` as the JSON data of the event `name`, which the registration lists.
|
|
14
|
+
func eventPayload(_ name: String, _ event: Any) throws(TokamakPluginError) -> Data
|
|
15
|
+
|
|
16
|
+
/// The listeners' JSON `reply` to the event `name`, which the registration
|
|
17
|
+
/// lists with a reply, as its type, or nil for null.
|
|
18
|
+
func eventReply(_ name: String, _ reply: Data) throws(TokamakPluginError) -> Any?
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
extension TokamakExports {
|
|
22
|
+
func eventPayload(_ name: String, _ event: Any) throws(TokamakPluginError) -> Data {
|
|
23
|
+
throw .unlistedEvent(name)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func eventReply(_ name: String, _ reply: Data) throws(TokamakPluginError) -> Any? {
|
|
27
|
+
throw .unlistedReply(name)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// What a stream does with values a slow reader has not taken.
|
|
32
|
+
enum TokamakOverflow {
|
|
33
|
+
/// Ends the stream with `QuotaExceededError`.
|
|
34
|
+
case error
|
|
35
|
+
/// Drops the oldest values, for samples where only recent ones matter.
|
|
36
|
+
case dropOldest
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Lists a plugin's exported methods and events: method parameters and event
|
|
40
|
+
/// replies are `Decodable`, method results and events `Encodable`, and options
|
|
41
|
+
/// literal values.
|
|
42
|
+
struct TokamakExporter<Plugin> {
|
|
43
|
+
/// Lists the event `name`, whose listeners receive an `E`.
|
|
44
|
+
mutating func event<E: Encodable>(_ name: String, _ type: E.Type, doc: String = "") {}
|
|
45
|
+
|
|
46
|
+
/// Lists the event `name`, whose listeners receive an `E` and may reply with an `R`.
|
|
47
|
+
mutating func event<E: Encodable, R: Decodable>(
|
|
48
|
+
_ name: String, _ type: E.Type, reply: R.Type, doc: String = ""
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
/// Exports `method`, which returns once.
|
|
52
|
+
mutating func call<each A: Decodable, R: Encodable>(
|
|
53
|
+
_ method: (Plugin) -> (repeat each A) async throws -> R, doc: String = ""
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
mutating func call<each A: Decodable>(
|
|
57
|
+
_ method: (Plugin) -> (repeat each A) async throws -> Void, doc: String = ""
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
/// Exports `method`, whose last parameter reads the bytes the Worker streams.
|
|
61
|
+
mutating func call<each A: Decodable, R: Encodable>(
|
|
62
|
+
_ method: (Plugin) -> (repeat each A, TokamakPluginBody) async throws -> R, doc: String = ""
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
mutating func call<each A: Decodable>(
|
|
66
|
+
_ method: (Plugin) -> (repeat each A, TokamakPluginBody) async throws -> Void, doc: String = ""
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
/// Exports `method`, which returns the values it sends until the Worker cancels.
|
|
70
|
+
mutating func stream<each A: Decodable, S: AsyncSequence>(
|
|
71
|
+
_ method: (Plugin) -> (repeat each A) throws -> S, overflow: TokamakOverflow = .error,
|
|
72
|
+
doc: String = ""
|
|
73
|
+
) where S.Element: Encodable {}
|
|
74
|
+
|
|
75
|
+
/// Exports `method`, which returns a native object the Worker holds as a
|
|
76
|
+
/// `NativeReference`.
|
|
77
|
+
mutating func resource<each A: Decodable, R>(
|
|
78
|
+
_ method: (Plugin) -> (repeat each A) async throws -> R, doc: String = ""
|
|
79
|
+
) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// A native object the Worker passes as a `NativeReference`, which another
|
|
83
|
+
/// plugin retained.
|
|
84
|
+
struct TokamakReference<Object>: Decodable {
|
|
85
|
+
let object: Object
|
|
86
|
+
|
|
87
|
+
init(from decoder: any Decoder) throws {
|
|
88
|
+
let id = try decoder.singleValueContainer().decode(String.self)
|
|
89
|
+
guard let resources = decoder.userInfo[.tokamakResources] as? TokamakResources else {
|
|
90
|
+
throw TokamakPluginError(name: "InvalidStateError", message: "No native references here")
|
|
91
|
+
}
|
|
92
|
+
object = try resources.get(id, as: Object.self)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Any JSON value, for data a plugin passes through or reads itself.
|
|
97
|
+
enum TokamakJSON: Codable, Equatable {
|
|
98
|
+
case null
|
|
99
|
+
case bool(Bool)
|
|
100
|
+
case number(Double)
|
|
101
|
+
case string(String)
|
|
102
|
+
case array([TokamakJSON])
|
|
103
|
+
case object([String: TokamakJSON])
|
|
104
|
+
|
|
105
|
+
init(from decoder: any Decoder) throws {
|
|
106
|
+
// Each failed attempt throws, so the commonest kinds of value come first.
|
|
107
|
+
let container = try decoder.singleValueContainer()
|
|
108
|
+
if container.decodeNil() {
|
|
109
|
+
self = .null
|
|
110
|
+
} else if let string = try? container.decode(String.self) {
|
|
111
|
+
self = .string(string)
|
|
112
|
+
} else if let number = try? container.decode(Double.self) {
|
|
113
|
+
self = .number(number)
|
|
114
|
+
} else if let bool = try? container.decode(Bool.self) {
|
|
115
|
+
self = .bool(bool)
|
|
116
|
+
} else if let object = try? container.decode([String: TokamakJSON].self) {
|
|
117
|
+
self = .object(object)
|
|
118
|
+
} else {
|
|
119
|
+
self = .array(try container.decode([TokamakJSON].self))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func encode(to encoder: any Encoder) throws {
|
|
124
|
+
var container = encoder.singleValueContainer()
|
|
125
|
+
switch self {
|
|
126
|
+
case .null: try container.encodeNil()
|
|
127
|
+
case .bool(let bool): try container.encode(bool)
|
|
128
|
+
case .number(let number): try container.encode(number)
|
|
129
|
+
case .string(let string): try container.encode(string)
|
|
130
|
+
case .array(let array): try container.encode(array)
|
|
131
|
+
case .object(let object): try container.encode(object)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// `value` as JSON, which `JSONSerialization` writes it as, or nil when it is not JSON.
|
|
136
|
+
init?(_ value: Any) {
|
|
137
|
+
switch value {
|
|
138
|
+
case is NSNull: self = .null
|
|
139
|
+
case let number as NSNumber where CFGetTypeID(number) == CFBooleanGetTypeID():
|
|
140
|
+
self = .bool(number.boolValue)
|
|
141
|
+
case let number as NSNumber: self = .number(number.doubleValue)
|
|
142
|
+
case let string as String: self = .string(string)
|
|
143
|
+
case let array as [Any]:
|
|
144
|
+
let elements = array.compactMap(TokamakJSON.init)
|
|
145
|
+
guard elements.count == array.count else { return nil }
|
|
146
|
+
self = .array(elements)
|
|
147
|
+
case let object as [String: Any]:
|
|
148
|
+
let entries = object.compactMapValues(TokamakJSON.init)
|
|
149
|
+
guard entries.count == object.count else { return nil }
|
|
150
|
+
self = .object(entries)
|
|
151
|
+
default: return nil
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The value as `JSONSerialization` reads it: `NSNull`, an `NSNumber` for a boolean or a
|
|
156
|
+
/// number, which `as? Int` reads when whole, `String`, arrays and dictionaries.
|
|
157
|
+
var value: Any {
|
|
158
|
+
switch self {
|
|
159
|
+
case .null: NSNull()
|
|
160
|
+
case .bool(let bool): NSNumber(value: bool)
|
|
161
|
+
case .number(let number): NSNumber(value: number)
|
|
162
|
+
case .string(let string): string
|
|
163
|
+
case .array(let array): array.map(\.value)
|
|
164
|
+
case .object(let object): object.mapValues(\.value)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// An enum whose cases are the forms its values take: a case without an associated value is
|
|
170
|
+
/// the string of its name, and a case with one unlabeled associated value is that value.
|
|
171
|
+
/// `tok plugin generate apple` writes its coding in place of these defaults, which throw.
|
|
172
|
+
protocol TokamakUnion: Codable {}
|
|
173
|
+
|
|
174
|
+
/// A `TokamakUnion` whose values are objects that name their case in the field `tag`, with a
|
|
175
|
+
/// struct case's fields.
|
|
176
|
+
protocol TokamakTaggedUnion: TokamakUnion {
|
|
177
|
+
/// The tag field's name, as a string literal, which `tok plugin generate apple` reads.
|
|
178
|
+
static var tag: String { get }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
extension TokamakUnion {
|
|
182
|
+
init(from decoder: any Decoder) throws {
|
|
183
|
+
throw TokamakPluginError.ungenerated(Self.self)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func encode(to encoder: any Encoder) throws {
|
|
187
|
+
throw TokamakPluginError.ungenerated(Self.self)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// The JSON value whose kind a union's generated decoder chooses its case by.
|
|
192
|
+
struct TokamakUnionValue {
|
|
193
|
+
enum Kind {
|
|
194
|
+
case null, boolean, number, array
|
|
195
|
+
case string(String)
|
|
196
|
+
/// An object, with its keys.
|
|
197
|
+
case object(Set<String>)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let kind: Kind
|
|
201
|
+
private let decoder: any Decoder
|
|
202
|
+
|
|
203
|
+
init(_ decoder: any Decoder) throws {
|
|
204
|
+
self.decoder = decoder
|
|
205
|
+
let value = try decoder.singleValueContainer()
|
|
206
|
+
if value.decodeNil() {
|
|
207
|
+
kind = .null
|
|
208
|
+
} else if let string = try? value.decode(String.self) {
|
|
209
|
+
kind = .string(string)
|
|
210
|
+
} else if (try? value.decode(Bool.self)) != nil {
|
|
211
|
+
kind = .boolean
|
|
212
|
+
} else if (try? value.decode(Double.self)) != nil {
|
|
213
|
+
kind = .number
|
|
214
|
+
} else if let object = try? decoder.container(keyedBy: TokamakUnionKey.self) {
|
|
215
|
+
kind = .object(Set(object.allKeys.map(\.stringValue)))
|
|
216
|
+
} else {
|
|
217
|
+
kind = .array
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// The value as a `T`.
|
|
222
|
+
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
|
223
|
+
try decoder.singleValueContainer().decode(type)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// The string in the object's field `tag`.
|
|
227
|
+
func tag(_ tag: String) throws -> String {
|
|
228
|
+
try decoder.container(keyedBy: TokamakUnionKey.self).decode(String.self, forKey: .init(tag))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// The error of a value that is none of the union `name`'s forms.
|
|
232
|
+
func mismatch(_ name: String) -> DecodingError {
|
|
233
|
+
.dataCorrupted(
|
|
234
|
+
.init(codingPath: decoder.codingPath, debugDescription: "must be one of \(name)'s forms"))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Encodes the case `name` of a tagged union as an object with `value`'s fields, if any, and the
|
|
239
|
+
/// field `tag` naming the case.
|
|
240
|
+
func tokamakEncode(
|
|
241
|
+
tag: String, _ name: String, _ value: (any Encodable)? = nil, to encoder: any Encoder
|
|
242
|
+
) throws {
|
|
243
|
+
try value?.encode(to: encoder)
|
|
244
|
+
var container = encoder.container(keyedBy: TokamakUnionKey.self)
|
|
245
|
+
try container.encode(name, forKey: .init(tag))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// A JSON object's key, as a union's coding reads and writes it.
|
|
249
|
+
struct TokamakUnionKey: CodingKey {
|
|
250
|
+
let stringValue: String
|
|
251
|
+
var intValue: Int? { nil }
|
|
252
|
+
|
|
253
|
+
init(_ stringValue: String) {
|
|
254
|
+
self.stringValue = stringValue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
init?(stringValue: String) {
|
|
258
|
+
self.stringValue = stringValue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
init?(intValue: Int) {
|
|
262
|
+
nil
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
extension CodingUserInfoKey {
|
|
267
|
+
/// The resources a `TokamakReference` finds its object in.
|
|
268
|
+
fileprivate static let tokamakResources = CodingUserInfoKey(rawValue: "tokamak.resources")!
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
extension TokamakPluginError {
|
|
272
|
+
/// A `TypeError` naming where the `root` value, such as a call's
|
|
273
|
+
/// arguments, fails to decode.
|
|
274
|
+
init(decoding error: DecodingError, in root: String = "arguments") {
|
|
275
|
+
let (path, problem): ([any CodingKey], String) =
|
|
276
|
+
switch error {
|
|
277
|
+
case .keyNotFound(let key, let context): (context.codingPath + [key], "is missing")
|
|
278
|
+
case .typeMismatch(_, let context), .valueNotFound(_, let context),
|
|
279
|
+
.dataCorrupted(let context):
|
|
280
|
+
(context.codingPath, context.debugDescription)
|
|
281
|
+
@unknown default: ([], "\(error)")
|
|
282
|
+
}
|
|
283
|
+
let location = path.reduce("") { location, key in
|
|
284
|
+
if let index = key.intValue { return "\(location)[\(index)]" }
|
|
285
|
+
return location.isEmpty ? key.stringValue : "\(location).\(key.stringValue)"
|
|
286
|
+
}
|
|
287
|
+
self.init(name: "TypeError", message: "\(location.isEmpty ? root : location) \(problem)")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// A `TokamakUnion` without the coding `tok plugin generate apple` writes.
|
|
291
|
+
static func ungenerated(_ union: Any.Type) -> Self {
|
|
292
|
+
Self(name: "TypeError", message: "run tok plugin generate apple to code \(union)")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// The plugin emitted an event its registration does not list.
|
|
296
|
+
static func unlistedEvent(_ name: String) -> Self {
|
|
297
|
+
Self(name: "TypeError", message: "apple/Exports.swift lists no event \(name)")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// The plugin asked for the reply to an event its registration lists without one.
|
|
301
|
+
static func unlistedReply(_ name: String) -> Self {
|
|
302
|
+
Self(name: "TypeError", message: "apple/Exports.swift lists no reply to event \(name)")
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Decodes a call's JSON arguments, or another `root` value, finding the
|
|
307
|
+
/// native references they name in `resources`.
|
|
308
|
+
func tokamakDecode<T: Decodable>(
|
|
309
|
+
_ type: T.Type, from payload: Data, resources: TokamakResources? = nil,
|
|
310
|
+
in root: String = "arguments"
|
|
311
|
+
) throws(TokamakPluginError) -> T {
|
|
312
|
+
let decoder = JSONDecoder()
|
|
313
|
+
decoder.userInfo[.tokamakResources] = resources
|
|
314
|
+
do {
|
|
315
|
+
return try decoder.decode(type, from: payload)
|
|
316
|
+
} catch let error as DecodingError {
|
|
317
|
+
throw TokamakPluginError(decoding: error, in: root)
|
|
318
|
+
} catch {
|
|
319
|
+
throw TokamakPluginError(error)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// `value` as a reply's payload: `Data` as it is, and any other value as the
|
|
324
|
+
/// JSON `encoder` writes.
|
|
325
|
+
func tokamakEncode<T: Encodable>(
|
|
326
|
+
_ value: T, with encoder: JSONEncoder = JSONEncoder()
|
|
327
|
+
) throws(TokamakPluginError) -> Data {
|
|
328
|
+
if let data = value as? Data { return data }
|
|
329
|
+
do {
|
|
330
|
+
return try encoder.encode(value)
|
|
331
|
+
} catch {
|
|
332
|
+
throw TokamakPluginError(name: "DataCloneError", message: "\(error)")
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// `event` as the JSON data of the event `name`, whose listeners receive a `T`.
|
|
337
|
+
func tokamakEvent<T: Encodable>(
|
|
338
|
+
_ name: String, _ event: Any, as type: T.Type
|
|
339
|
+
) throws(TokamakPluginError) -> Data {
|
|
340
|
+
guard let event = event as? T else {
|
|
341
|
+
throw TokamakPluginError(
|
|
342
|
+
name: "TypeError", message: "event \(name) takes \(T.self), not \(Swift.type(of: event))")
|
|
343
|
+
}
|
|
344
|
+
return try tokamakEncode(event)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Replies with what `method` returns. A synchronous method runs on the
|
|
348
|
+
/// call's executor and has nothing to cancel.
|
|
349
|
+
func tokamakCall<T: Encodable>(
|
|
350
|
+
_ reply: TokamakPluginBytesReply, _ method: () throws -> T
|
|
351
|
+
) -> (() -> Void)? {
|
|
352
|
+
reply(result { try tokamakEncode(try method()) })
|
|
353
|
+
return nil
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
func tokamakCall(_ reply: TokamakPluginBytesReply, _ method: () throws -> Void) -> (() -> Void)? {
|
|
357
|
+
reply(
|
|
358
|
+
result {
|
|
359
|
+
try method()
|
|
360
|
+
return null
|
|
361
|
+
})
|
|
362
|
+
return nil
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// Runs `method` in a task, replying with what it returns. Cancelling the
|
|
366
|
+
/// call cancels the task.
|
|
367
|
+
func tokamakCall<T: Encodable>(
|
|
368
|
+
_ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> T
|
|
369
|
+
) -> (() -> Void)? {
|
|
370
|
+
task { reply(await awaitResult { try tokamakEncode(try await method()) }) }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func tokamakCall(
|
|
374
|
+
_ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> Void
|
|
375
|
+
) -> (() -> Void)? {
|
|
376
|
+
task {
|
|
377
|
+
reply(
|
|
378
|
+
await awaitResult {
|
|
379
|
+
try await method()
|
|
380
|
+
return null
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// Replies with each value `method`'s sequence sends, until it ends or the
|
|
386
|
+
/// subscription is cancelled. A synchronous method runs on the
|
|
387
|
+
/// subscription's executor.
|
|
388
|
+
func tokamakStream<S: AsyncSequence>(
|
|
389
|
+
_ reply: @escaping TokamakPluginBytesReply, _ method: () throws -> S
|
|
390
|
+
) throws(TokamakPluginError) -> () -> Void where S.Element: Encodable {
|
|
391
|
+
let values = try catching(method)
|
|
392
|
+
return task { await send(values, reply) }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func tokamakStream<S: AsyncSequence>(
|
|
396
|
+
_ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> S
|
|
397
|
+
) -> () -> Void where S.Element: Encodable {
|
|
398
|
+
task {
|
|
399
|
+
do {
|
|
400
|
+
await send(try await method(), reply)
|
|
401
|
+
} catch {
|
|
402
|
+
reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Retains the object `method` returns in `resources`, replying with its
|
|
408
|
+
/// ID, until the subscription is cancelled. A synchronous method runs on the
|
|
409
|
+
/// subscription's executor.
|
|
410
|
+
func tokamakResource(
|
|
411
|
+
_ resources: TokamakResources, _ reply: @escaping TokamakPluginBytesReply,
|
|
412
|
+
_ method: () throws -> Any
|
|
413
|
+
) throws(TokamakPluginError) -> () -> Void {
|
|
414
|
+
resources.retain(try catching(method)) {
|
|
415
|
+
reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: false))
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func tokamakResource(
|
|
420
|
+
_ resources: TokamakResources, _ reply: @escaping TokamakPluginBytesReply,
|
|
421
|
+
_ method: @escaping () async throws -> Any
|
|
422
|
+
) -> () -> Void {
|
|
423
|
+
let retained = Retained()
|
|
424
|
+
let cancel = task {
|
|
425
|
+
do {
|
|
426
|
+
let object = try await method()
|
|
427
|
+
retained.hold(
|
|
428
|
+
resources.retain(object) {
|
|
429
|
+
reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: false))
|
|
430
|
+
})
|
|
431
|
+
} catch {
|
|
432
|
+
reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
cancel()
|
|
437
|
+
retained.end()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// How many values listeners keep: those a slow reader has not taken, or those
|
|
442
|
+
/// held for the first.
|
|
443
|
+
private let listenerCapacity = 64
|
|
444
|
+
|
|
445
|
+
/// The streams of an event's listeners, such as a page's, each of which
|
|
446
|
+
/// receives every value `send` delivers until it ends, keeping the newest 64
|
|
447
|
+
/// that a slow reader has not taken. When `holding`, the newest 64 values sent
|
|
448
|
+
/// while none listen wait for the first listener.
|
|
449
|
+
final class TokamakListeners<Value>: @unchecked Sendable {
|
|
450
|
+
private let lock = NSLock()
|
|
451
|
+
private let holding: Bool
|
|
452
|
+
private var continuations: [UUID: AsyncStream<Value>.Continuation] = [:]
|
|
453
|
+
private var held: [Value] = []
|
|
454
|
+
|
|
455
|
+
init(holding: Bool = false) {
|
|
456
|
+
self.holding = holding
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// A new listener's stream.
|
|
460
|
+
// Values are yielded under the lock, so one sent while a listener starts
|
|
461
|
+
// follows the held ones. Yielding only buffers.
|
|
462
|
+
func stream() -> AsyncStream<Value> {
|
|
463
|
+
let (stream, continuation) = AsyncStream.makeStream(
|
|
464
|
+
of: Value.self, bufferingPolicy: .bufferingNewest(listenerCapacity))
|
|
465
|
+
let key = UUID()
|
|
466
|
+
continuation.onTermination = { [weak self] _ in
|
|
467
|
+
self?.lock.withLock { _ = self?.continuations.removeValue(forKey: key) }
|
|
468
|
+
}
|
|
469
|
+
lock.withLock {
|
|
470
|
+
continuations[key] = continuation
|
|
471
|
+
for value in held {
|
|
472
|
+
continuation.yield(value)
|
|
473
|
+
}
|
|
474
|
+
held = []
|
|
475
|
+
}
|
|
476
|
+
return stream
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
func send(_ value: Value) {
|
|
480
|
+
lock.withLock {
|
|
481
|
+
if holding && continuations.isEmpty {
|
|
482
|
+
held.append(value)
|
|
483
|
+
if held.count > listenerCapacity { held.removeFirst() }
|
|
484
|
+
}
|
|
485
|
+
for continuation in continuations.values {
|
|
486
|
+
continuation.yield(value)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/// The JSON payload of a method that returns nothing.
|
|
493
|
+
private let null = Data("null".utf8)
|
|
494
|
+
|
|
495
|
+
/// A call's last reply: `payload`'s bytes, or its failure.
|
|
496
|
+
private func result(_ payload: () throws -> Data) -> TokamakPluginResult {
|
|
497
|
+
TokamakPluginResult(Result { () throws(TokamakPluginError) in try catching(payload) }, done: true)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private func awaitResult(_ payload: () async throws -> Data) async -> TokamakPluginResult {
|
|
501
|
+
do {
|
|
502
|
+
return TokamakPluginResult(.success(try await payload()), done: true)
|
|
503
|
+
} catch {
|
|
504
|
+
return TokamakPluginResult(.failure(TokamakPluginError(error)), done: true)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// What `body` returns, with what it throws as a `TokamakPluginError`.
|
|
509
|
+
private func catching<T>(_ body: () throws -> T) throws(TokamakPluginError) -> T {
|
|
510
|
+
do {
|
|
511
|
+
return try body()
|
|
512
|
+
} catch {
|
|
513
|
+
throw TokamakPluginError(error)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/// Runs `body` in a task, returning its cancellation.
|
|
518
|
+
private func task(_ body: @escaping () async -> Void) -> () -> Void {
|
|
519
|
+
Task { await body() }.cancel
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Replies with each of `values`, then with the end of the subscription.
|
|
523
|
+
private func send<S: AsyncSequence>(
|
|
524
|
+
_ values: S, _ reply: TokamakPluginBytesReply
|
|
525
|
+
) async where S.Element: Encodable {
|
|
526
|
+
// One encoder serves the stream, whose values it encodes one at a time.
|
|
527
|
+
let encoder = JSONEncoder()
|
|
528
|
+
do {
|
|
529
|
+
for try await value in values {
|
|
530
|
+
reply(TokamakPluginResult(.success(try tokamakEncode(value, with: encoder)), done: false))
|
|
531
|
+
}
|
|
532
|
+
reply(TokamakPluginResult(payload: Data(), failed: false, done: true))
|
|
533
|
+
} catch {
|
|
534
|
+
reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// The release of an object a subscription retains, which runs once the
|
|
539
|
+
/// subscription ends, whether the object is retained before or after.
|
|
540
|
+
private final class Retained {
|
|
541
|
+
private let lock = NSLock()
|
|
542
|
+
private var release: (() -> Void)?
|
|
543
|
+
private var ended = false
|
|
544
|
+
|
|
545
|
+
func hold(_ release: @escaping () -> Void) {
|
|
546
|
+
let ended = lock.withLock {
|
|
547
|
+
if !self.ended { self.release = release }
|
|
548
|
+
return self.ended
|
|
549
|
+
}
|
|
550
|
+
if ended { release() }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
func end() {
|
|
554
|
+
let release = lock.withLock {
|
|
555
|
+
ended = true
|
|
556
|
+
defer { self.release = nil }
|
|
557
|
+
return self.release
|
|
558
|
+
}
|
|
559
|
+
release?()
|
|
560
|
+
}
|
|
561
|
+
}
|
|
@@ -140,7 +140,7 @@ final class TokamakHost {
|
|
|
140
140
|
#endif
|
|
141
141
|
|
|
142
142
|
/// Delivers the plugin's event `name`, which the Worker's listeners
|
|
143
|
-
/// receive as `<plugin
|
|
143
|
+
/// receive as `<plugin>.apple.<name>`, with its encoded `payload`, and calls
|
|
144
144
|
/// `completion` on the main thread with their encoded reply, which is empty
|
|
145
145
|
/// when none replied. Fails unless they return within `timeout`, which
|
|
146
146
|
/// includes runtime startup. Call it on the main thread.
|
|
@@ -150,33 +150,75 @@ final class TokamakHost {
|
|
|
150
150
|
timeout: TimeInterval,
|
|
151
151
|
completion: @escaping (Result<Data, Error>) -> Void
|
|
152
152
|
) {
|
|
153
|
-
app.emit("\(plugin).\(name)", payload: payload, timeout: timeout, completion: completion)
|
|
153
|
+
app.emit("\(plugin).apple.\(name)", payload: payload, timeout: timeout, completion: completion)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
///
|
|
157
|
-
///
|
|
156
|
+
/// Delivers `event` to the Worker's listeners of the plugin's event `name`,
|
|
157
|
+
/// which `apple/Exports.swift` lists, then calls `completion` on the main
|
|
158
|
+
/// thread. Fails unless they return within `timeout`, which includes runtime
|
|
159
|
+
/// startup. Call it on the main thread.
|
|
158
160
|
func emit(
|
|
159
161
|
_ name: String,
|
|
160
|
-
event:
|
|
162
|
+
event: some Encodable,
|
|
161
163
|
timeout: TimeInterval,
|
|
162
|
-
completion: @escaping (Result<
|
|
164
|
+
completion: @escaping (Result<Void, Error>) -> Void
|
|
163
165
|
) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
send(name, event, timeout: timeout) { completion($0.map { _ in }) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// `emit` of an event whose listeners may reply, calling `completion` with
|
|
170
|
+
/// the reply as the plugin's generated dispatch decodes it, which is a
|
|
171
|
+
/// `Reply`, or nil when none replied.
|
|
172
|
+
func emit<Reply>(
|
|
173
|
+
_ name: String,
|
|
174
|
+
event: some Encodable,
|
|
175
|
+
reply: Reply.Type,
|
|
176
|
+
timeout: TimeInterval,
|
|
177
|
+
completion: @escaping (Result<Reply?, Error>) -> Void
|
|
178
|
+
) {
|
|
179
|
+
send(name, event, timeout: timeout) { result in
|
|
170
180
|
completion(
|
|
171
181
|
result.flatMap { reply in
|
|
172
|
-
Result {
|
|
173
|
-
let value = reply.isEmpty ? NSNull() : try decodePluginValue(reply)
|
|
174
|
-
return value is NSNull ? nil : value
|
|
175
|
-
}.mapError { $0 }
|
|
182
|
+
Result<Reply?, Error> { try self.decoded(reply, to: name, as: Reply.self) }
|
|
176
183
|
})
|
|
177
184
|
}
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
/// Delivers `event` as the plugin's generated dispatch encodes it.
|
|
188
|
+
private func send(
|
|
189
|
+
_ name: String, _ event: Any, timeout: TimeInterval,
|
|
190
|
+
completion: @escaping (Result<Data, Error>) -> Void
|
|
191
|
+
) {
|
|
192
|
+
let payload: Data
|
|
193
|
+
do throws(TokamakPluginError) {
|
|
194
|
+
payload = try exports(name).eventPayload(name, event)
|
|
195
|
+
} catch {
|
|
196
|
+
return completion(.failure(error))
|
|
197
|
+
}
|
|
198
|
+
emitBytes(name, payload: payload, timeout: timeout, completion: completion)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The listeners' `reply` to the event `name` as a `Reply`, or nil when none replied.
|
|
202
|
+
private func decoded<Reply>(
|
|
203
|
+
_ reply: Data, to name: String, as type: Reply.Type
|
|
204
|
+
) throws(TokamakPluginError) -> Reply? {
|
|
205
|
+
guard !reply.isEmpty, let value = try exports(name).eventReply(name, reply) else { return nil }
|
|
206
|
+
guard let reply = value as? Reply else {
|
|
207
|
+
throw TokamakPluginError(
|
|
208
|
+
name: "TypeError",
|
|
209
|
+
message: "event \(name) replies with \(Swift.type(of: value)), not \(Reply.self)")
|
|
210
|
+
}
|
|
211
|
+
return reply
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// The plugin, whose generated dispatch encodes the event `name`.
|
|
215
|
+
private func exports(_ name: String) throws(TokamakPluginError) -> any TokamakExports {
|
|
216
|
+
guard let exports = app.plugins[plugin] as? any TokamakExports else {
|
|
217
|
+
throw .unlistedEvent(name)
|
|
218
|
+
}
|
|
219
|
+
return exports
|
|
220
|
+
}
|
|
221
|
+
|
|
180
222
|
/// Fetches what the app serves at `path`, as its WebView would load it: a
|
|
181
223
|
/// file the app ships or the Worker's response. Calls `completion` on the
|
|
182
224
|
/// main thread with the body. Fails unless it arrives within `timeout`,
|
|
@@ -1,61 +1,23 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
-
/// Native objects that plugins share by ID. Each
|
|
4
|
-
///
|
|
3
|
+
/// Native objects that plugins share by ID. Each is kept until the
|
|
4
|
+
/// subscription that retained it ends.
|
|
5
5
|
final class TokamakResources {
|
|
6
6
|
private let lock = NSLock()
|
|
7
|
-
private var objects: [String:
|
|
8
|
-
private var scopes: [String: Set<String>] = [:]
|
|
7
|
+
private var objects: [String: Any] = [:]
|
|
9
8
|
|
|
10
|
-
///
|
|
11
|
-
|
|
12
|
-
let scope = UUID().uuidString
|
|
13
|
-
lock.withLock { scopes[scope] = [] }
|
|
14
|
-
return scope
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/// Keeps `object` in `scope` and returns its ID. An ended scope keeps
|
|
18
|
-
/// nothing, so the ID finds no object.
|
|
19
|
-
func retain(_ object: Any, in scope: String) -> String {
|
|
20
|
-
let id = UUID().uuidString
|
|
21
|
-
lock.withLock {
|
|
22
|
-
guard scopes[scope]?.insert(id) != nil else { return }
|
|
23
|
-
objects[id] = (object, scope)
|
|
24
|
-
}
|
|
25
|
-
return id
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/// Keeps `object` in a scope of its own, replies with its ID, and returns
|
|
29
|
-
/// the release to return from `subscribe`.
|
|
9
|
+
/// Keeps `object`, replies with its ID, and returns the release to return
|
|
10
|
+
/// from `subscribe`.
|
|
30
11
|
func retain(_ object: Any, reply: TokamakPluginReply) -> () -> Void {
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/// Releases the object retained as `id`, if it is still kept.
|
|
37
|
-
func release(_ id: Any) {
|
|
38
|
-
let removed = lock.withLock { () -> Any? in
|
|
39
|
-
guard let id = id as? String, let entry = objects.removeValue(forKey: id) else { return nil }
|
|
40
|
-
scopes[entry.scope]?.remove(id)
|
|
41
|
-
return entry.object
|
|
42
|
-
}
|
|
43
|
-
drop(removed)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/// Releases every object `scope` keeps, and keeps nothing in it later.
|
|
47
|
-
func end(_ scope: String) {
|
|
48
|
-
let removed = lock.withLock { () -> [Any] in
|
|
49
|
-
(scopes.removeValue(forKey: scope) ?? []).compactMap {
|
|
50
|
-
objects.removeValue(forKey: $0)?.object
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
drop(removed)
|
|
12
|
+
let id = UUID().uuidString
|
|
13
|
+
lock.withLock { objects[id] = object }
|
|
14
|
+
reply(.success(id))
|
|
15
|
+
return { [self] in drop(lock.withLock { objects.removeValue(forKey: id) }) }
|
|
54
16
|
}
|
|
55
17
|
|
|
56
18
|
/// The object retained as `id`.
|
|
57
19
|
func get<Object>(_ id: Any, as type: Object.Type) throws(TokamakPluginError) -> Object {
|
|
58
|
-
guard let id = id as? String, let object = lock.withLock({ objects[id]
|
|
20
|
+
guard let id = id as? String, let object = lock.withLock({ objects[id] }) else {
|
|
59
21
|
throw TokamakPluginError(name: "InvalidStateError", message: "The native reference is closed")
|
|
60
22
|
}
|
|
61
23
|
guard let typed = object as? Object else {
|
|
@@ -65,10 +27,10 @@ final class TokamakResources {
|
|
|
65
27
|
return typed
|
|
66
28
|
}
|
|
67
29
|
|
|
68
|
-
/// Lets go of released
|
|
30
|
+
/// Lets go of a released object on the main thread, where UI and run loop
|
|
69
31
|
/// objects expect to be deallocated.
|
|
70
|
-
private func drop(_
|
|
71
|
-
guard let
|
|
72
|
-
DispatchQueue.main.async { withExtendedLifetime(
|
|
32
|
+
private func drop(_ object: Any?) {
|
|
33
|
+
guard let object else { return }
|
|
34
|
+
DispatchQueue.main.async { withExtendedLifetime(object) {} }
|
|
73
35
|
}
|
|
74
36
|
}
|
package/package.json
CHANGED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// One call or subscription of a generated platform binding. It decodes the
|
|
4
|
-
/// Worker's JSON into native values and encodes native values back, keeping
|
|
5
|
-
/// the native objects it returns in the receiving invocation's scope.
|
|
6
|
-
///
|
|
7
|
-
/// JSON objects tagged `#` carry what JSON lacks: native objects, dates,
|
|
8
|
-
/// bytes, errors, non-finite numbers, and records whose keys include `#`.
|
|
9
|
-
struct TokamakBinding {
|
|
10
|
-
private let resources: TokamakResources
|
|
11
|
-
private let request: [String: Any]
|
|
12
|
-
private let reply: TokamakPluginBytesReply
|
|
13
|
-
|
|
14
|
-
init(
|
|
15
|
-
_ payload: Data, resources: TokamakResources, reply: @escaping TokamakPluginBytesReply
|
|
16
|
-
) throws(TokamakPluginError) {
|
|
17
|
-
guard let request = try decodePluginValue(payload) as? [String: Any] else {
|
|
18
|
-
throw typeError("A binding request is an object")
|
|
19
|
-
}
|
|
20
|
-
self.resources = resources
|
|
21
|
-
self.request = request
|
|
22
|
-
self.reply = reply
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/// The rejection of a member the running OS version lacks.
|
|
26
|
-
static let unavailable = TokamakPluginError.notSupported(
|
|
27
|
-
"The API is unavailable on this OS version")
|
|
28
|
-
|
|
29
|
-
/// Argument `index` as JSON.
|
|
30
|
-
subscript(index: Int) -> Any {
|
|
31
|
-
let arguments = request["arguments"] as? [Any] ?? []
|
|
32
|
-
return arguments.indices.contains(index) ? arguments[index] : NSNull()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/// The object the call is made on.
|
|
36
|
-
func target<T>(_ type: T.Type) throws -> T {
|
|
37
|
-
try object(request["target"] as Any, as: type)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// The handlers an implementation calls.
|
|
41
|
-
var methods: Set<String> {
|
|
42
|
-
Set(request["methods"] as? [String] ?? [])
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private var scope: String {
|
|
46
|
-
request["scope"] as? String ?? ""
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// MARK: Decoding
|
|
50
|
-
|
|
51
|
-
func decode(_ value: Any, as type: Bool.Type) throws -> Bool {
|
|
52
|
-
guard let value = value as? Bool else { throw mismatch(value, type) }
|
|
53
|
-
return value
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
func decode<T: BinaryInteger>(_ value: Any, as type: T.Type) throws -> T {
|
|
57
|
-
guard let number = value as? NSNumber, let value = T(exactly: number.doubleValue) else {
|
|
58
|
-
throw mismatch(value, type)
|
|
59
|
-
}
|
|
60
|
-
return value
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
func decode<T: BinaryFloatingPoint>(_ value: Any, as type: T.Type) throws -> T {
|
|
64
|
-
if let number = value as? NSNumber { return T(number.doubleValue) }
|
|
65
|
-
guard let number = try Double(decode(tagged(value, "number"), as: String.self)) else {
|
|
66
|
-
throw mismatch(value, type)
|
|
67
|
-
}
|
|
68
|
-
return T(number)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
func decode(_ value: Any, as type: String.Type) throws -> String {
|
|
72
|
-
guard let value = value as? String else { throw mismatch(value, type) }
|
|
73
|
-
return value
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
func decode(_ value: Any, as type: NSNumber.Type) throws -> NSNumber {
|
|
77
|
-
guard let value = value as? NSNumber else { throw mismatch(value, type) }
|
|
78
|
-
return value
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
func decode(_ value: Any, as type: Locale.Type) throws -> Locale {
|
|
82
|
-
Locale(identifier: try decode(value, as: String.self))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
func decode(_ value: Any, as type: TimeZone.Type) throws -> TimeZone {
|
|
86
|
-
guard let zone = TimeZone(identifier: try decode(value, as: String.self)) else {
|
|
87
|
-
throw mismatch(value, type)
|
|
88
|
-
}
|
|
89
|
-
return zone
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
func decode(_ value: Any, as type: Date.Type) throws -> Date {
|
|
93
|
-
Date(timeIntervalSince1970: try decode(tagged(value, "date"), as: Double.self) / 1000)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
func decode(_ value: Any, as type: Data.Type) throws -> Data {
|
|
97
|
-
guard let data = Data(base64Encoded: try decode(tagged(value, "bytes"), as: String.self)) else {
|
|
98
|
-
throw mismatch(value, type)
|
|
99
|
-
}
|
|
100
|
-
return data
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
func decode(_ value: Any, as type: URL.Type) throws -> URL {
|
|
104
|
-
guard let url = URL(string: try decode(value, as: String.self)) else {
|
|
105
|
-
throw mismatch(value, type)
|
|
106
|
-
}
|
|
107
|
-
return url
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
func decode(_ value: Any, as type: UUID.Type) throws -> UUID {
|
|
111
|
-
guard let uuid = UUID(uuidString: try decode(value, as: String.self)) else {
|
|
112
|
-
throw mismatch(value, type)
|
|
113
|
-
}
|
|
114
|
-
return uuid
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/// The field `name` of the struct `value`.
|
|
118
|
-
func field(_ value: Any, _ name: String) throws -> Any {
|
|
119
|
-
guard let fields = value as? [String: Any] else { throw mismatch(value, [String: Any].self) }
|
|
120
|
-
return fields[name] ?? NSNull()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/// The native object `value` refers to.
|
|
124
|
-
func object<T>(_ value: Any, as type: T.Type) throws -> T {
|
|
125
|
-
try resources.get(tagged(value, "object", key: "id"), as: type)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
func optional<T>(_ value: Any, _ decode: (Any) throws -> T) rethrows -> T? {
|
|
129
|
-
value is NSNull ? nil : try decode(value)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
func array<T>(_ value: Any, _ decode: (Any) throws -> T) throws -> [T] {
|
|
133
|
-
guard let values = value as? [Any] else { throw mismatch(value, [T].self) }
|
|
134
|
-
return try values.map(decode)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
func record<T>(_ value: Any, _ decode: (Any) throws -> T) throws -> [String: T] {
|
|
138
|
-
let fields = (try? tagged(value, "record")) ?? value
|
|
139
|
-
guard let fields = fields as? [String: Any] else { throw mismatch(value, [String: T].self) }
|
|
140
|
-
return try fields.mapValues(decode)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// MARK: Encoding
|
|
144
|
-
|
|
145
|
-
func encode(_ value: Bool) -> Any { value }
|
|
146
|
-
|
|
147
|
-
func encode<T: BinaryInteger>(_ value: T) -> Any {
|
|
148
|
-
Int64(exactly: value).map { NSNumber(value: $0) } ?? NSNumber(value: Double(value))
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
func encode<T: BinaryFloatingPoint>(_ value: T) -> Any {
|
|
152
|
-
let number = Double(value)
|
|
153
|
-
if number.isFinite { return number }
|
|
154
|
-
return ["#": "number", "value": number.isNaN ? "NaN" : number > 0 ? "Infinity" : "-Infinity"]
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
func encode(_ value: String) -> Any { value }
|
|
158
|
-
|
|
159
|
-
func encode(_ value: NSNumber) -> Any { encode(value.doubleValue) }
|
|
160
|
-
|
|
161
|
-
func encode(_ value: Locale) -> Any { value.identifier }
|
|
162
|
-
|
|
163
|
-
func encode(_ value: TimeZone) -> Any { value.identifier }
|
|
164
|
-
|
|
165
|
-
func encode(_ value: Date) -> Any { ["#": "date", "value": value.timeIntervalSince1970 * 1000] }
|
|
166
|
-
|
|
167
|
-
func encode(_ value: Data) -> Any { ["#": "bytes", "value": value.base64EncodedString()] }
|
|
168
|
-
|
|
169
|
-
func encode(_ value: URL) -> Any { value.absoluteString }
|
|
170
|
-
|
|
171
|
-
func encode(_ value: UUID) -> Any { value.uuidString }
|
|
172
|
-
|
|
173
|
-
func encode(_ value: any Error) -> Any {
|
|
174
|
-
TokamakPluginError(value).json.merging(["#": "error"]) { $1 }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/// Keeps `value` in the invocation's scope and refers to it, with the
|
|
178
|
-
/// names of its class and superclasses.
|
|
179
|
-
func encode(object value: Any) -> Any {
|
|
180
|
-
["#": "object", "id": resources.retain(value, in: scope), "types": types(of: value)]
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
func encode<T>(optional value: T?, _ encode: (T) -> Any) -> Any {
|
|
184
|
-
value.map(encode) ?? NSNull()
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
func encode<S: Sequence>(array values: S, _ encode: (S.Element) -> Any) -> Any {
|
|
188
|
-
values.map(encode)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
func encode<T>(record fields: [String: T], _ encode: (T) -> Any) -> Any {
|
|
192
|
-
let encoded = fields.mapValues(encode)
|
|
193
|
-
return encoded["#"] == nil ? encoded : ["#": "record", "value": encoded]
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// MARK: Replies
|
|
197
|
-
|
|
198
|
-
/// Replies with the JSON `body` returns, or the error it throws.
|
|
199
|
-
func run(_ body: () throws -> Any?) -> (() -> Void)? {
|
|
200
|
-
do { send(try body(), done: true) } catch { fail(error) }
|
|
201
|
-
return nil
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/// Replies with the JSON `body` resolves to on the main actor, or the
|
|
205
|
-
/// error it throws. The returned function cancels its task.
|
|
206
|
-
func task(_ body: @escaping @MainActor () async throws -> Any?) -> () -> Void {
|
|
207
|
-
let task = Task { @MainActor in
|
|
208
|
-
do { send(try await body(), done: true) } catch { fail(error) }
|
|
209
|
-
}
|
|
210
|
-
return { task.cancel() }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/// Replies with each element of the sequence `body` returns on the main
|
|
214
|
-
/// actor, encoded, then ends. The returned function cancels its task.
|
|
215
|
-
func stream<S: AsyncSequence>(
|
|
216
|
-
_ body: @escaping @MainActor () async throws -> S,
|
|
217
|
-
encode: @escaping (S.Element) -> Any
|
|
218
|
-
) -> () -> Void {
|
|
219
|
-
let task = Task {
|
|
220
|
-
do {
|
|
221
|
-
for try await element in try await body() { send(encode(element), done: false) }
|
|
222
|
-
reply(TokamakPluginResult(payload: Data(), failed: false, done: true))
|
|
223
|
-
} catch { fail(error) }
|
|
224
|
-
}
|
|
225
|
-
return { task.cancel() }
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/// Keeps `type`'s implementation while the subscription lasts, and replies
|
|
229
|
-
/// with its ID.
|
|
230
|
-
func implement(_ type: TokamakBindingImplementation.Type) -> () -> Void {
|
|
231
|
-
let scope = resources.open()
|
|
232
|
-
send(resources.retain(type.init(self), in: scope), done: false)
|
|
233
|
-
return { [resources] in resources.end(scope) }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
func send(_ value: Any?, done: Bool) {
|
|
237
|
-
reply(TokamakPluginResult(encodePluginValue(value), done: done))
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private func fail(_ error: any Error) {
|
|
241
|
-
reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/// The field `key` of `value` tagged `tag`.
|
|
245
|
-
private func tagged(_ value: Any, _ tag: String, key: String = "value") throws -> Any {
|
|
246
|
-
guard let fields = value as? [String: Any], fields["#"] as? String == tag,
|
|
247
|
-
let field = fields[key]
|
|
248
|
-
else { throw typeError("Expected a tagged \(tag)") }
|
|
249
|
-
return field
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/// The error of `value`, which is not a `type`.
|
|
253
|
-
func mismatch(_ value: Any, _ type: Any.Type) -> TokamakPluginError {
|
|
254
|
-
typeError("\(Swift.type(of: value)) is not \(type)")
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/// The names of `value`'s class and superclasses, then of the Objective-C
|
|
258
|
-
/// protocols they adopt, or of its type: Objective-C names, and
|
|
259
|
-
/// module-qualified Swift names.
|
|
260
|
-
private func types(of value: Any) -> [String] {
|
|
261
|
-
let name = { (type: Any.Type) in
|
|
262
|
-
let name = String(reflecting: type)
|
|
263
|
-
return name.hasPrefix("__C.") ? String(name.dropFirst(4)) : name
|
|
264
|
-
}
|
|
265
|
-
guard let type = Swift.type(of: value) as? AnyClass else {
|
|
266
|
-
return [name(Swift.type(of: value))]
|
|
267
|
-
}
|
|
268
|
-
let classes = Array(sequence(first: type, next: class_getSuperclass))
|
|
269
|
-
let protocols = classes.flatMap(adopted).map(NSStringFromProtocol)
|
|
270
|
-
var seen = Set<String>()
|
|
271
|
-
return (classes.map(name) + protocols).filter { seen.insert($0).inserted }
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/// The Objective-C protocols `type` adopts, each followed by those it inherits.
|
|
276
|
-
private func adopted(by type: AnyClass) -> [Protocol] {
|
|
277
|
-
protocols { class_copyProtocolList(type, $0) }.flatMap { [$0] + inherited(by: $0) }
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private func inherited(by parent: Protocol) -> [Protocol] {
|
|
281
|
-
protocols { protocol_copyProtocolList(parent, $0) }.flatMap { [$0] + inherited(by: $0) }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/// The protocols a runtime `copy` function lists.
|
|
285
|
-
private func protocols(
|
|
286
|
-
_ copy: (UnsafeMutablePointer<UInt32>) -> AutoreleasingUnsafeMutablePointer<Protocol>?
|
|
287
|
-
) -> [Protocol] {
|
|
288
|
-
var count: UInt32 = 0
|
|
289
|
-
guard let list = copy(&count) else { return [] }
|
|
290
|
-
defer { free(UnsafeMutableRawPointer(list)) }
|
|
291
|
-
return (0..<Int(count)).map { list[$0] }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/// A native object that implements a protocol by calling the JavaScript
|
|
295
|
-
/// handlers of an implementation subscription. A method without a handler
|
|
296
|
-
/// keeps the platform's default.
|
|
297
|
-
class TokamakBindingImplementation: NSObject {
|
|
298
|
-
private let binding: TokamakBinding
|
|
299
|
-
private let handled: Set<String>
|
|
300
|
-
|
|
301
|
-
required init(_ binding: TokamakBinding) {
|
|
302
|
-
self.binding = binding
|
|
303
|
-
handled = binding.methods
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/// The handler each optional Objective-C method calls, by selector.
|
|
307
|
-
class var selectors: [Selector: String] { [:] }
|
|
308
|
-
|
|
309
|
-
override func responds(to selector: Selector!) -> Bool {
|
|
310
|
-
guard let method = Self.selectors[selector] else { return super.responds(to: selector) }
|
|
311
|
-
return handled.contains(method)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/// Calls the handler `method`, when it has one, with the `arguments` it
|
|
315
|
-
/// encodes.
|
|
316
|
-
func call(_ method: String, _ arguments: (TokamakBinding) -> [Any]) {
|
|
317
|
-
guard handled.contains(method) else { return }
|
|
318
|
-
binding.send(["method": method, "arguments": arguments(binding)], done: false)
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private func typeError(_ message: String) -> TokamakPluginError {
|
|
323
|
-
TokamakPluginError(name: "TypeError", message: message)
|
|
324
|
-
}
|