@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.100
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/ios-simulator-x64/build/entrypoint +245 -0
- package/ios-simulator-x64/build/ios-deployment-target +1 -0
- package/ios-simulator-x64/build/macos-deployment-target +1 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +67 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Modules/module.modulemap +4 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-x64/native-shell/TokamakApp.swift +569 -0
- package/ios-simulator-x64/native-shell/TokamakOrigin.swift +20 -0
- package/ios-simulator-x64/native-shell/TokamakPlugins.swift +309 -0
- package/ios-simulator-x64/native-shell/TokamakShell.swift +515 -0
- package/ios-simulator-x64/platform-pack.json +36 -0
- package/ios-simulator-x64/tools/tokamak-apple-signing +0 -0
- package/package.json +23 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
struct TokamakPluginError: Error {
|
|
5
|
+
let name: String
|
|
6
|
+
let message: String
|
|
7
|
+
|
|
8
|
+
static func notSupported(_ message: String) -> Self {
|
|
9
|
+
Self(name: "NotSupportedError", message: message)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// UI was needed while the app cannot show it.
|
|
13
|
+
static let needsUI = Self(
|
|
14
|
+
name: "NeedsUIError",
|
|
15
|
+
message: "The app cannot show UI while it is in the background"
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
|
|
20
|
+
|
|
21
|
+
/// The app as one plugin sees it.
|
|
22
|
+
final class TokamakHost {
|
|
23
|
+
private unowned let app: TokamakApp
|
|
24
|
+
private let plugin: String
|
|
25
|
+
|
|
26
|
+
init(app: TokamakApp, plugin: String) {
|
|
27
|
+
self.app = app
|
|
28
|
+
self.plugin = plugin
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Throws `NeedsUIError` unless the app can show UI now: on iOS, while it
|
|
32
|
+
/// is in the foreground. A hidden or minimised macOS app still shows system
|
|
33
|
+
/// prompts. Call it on the main thread, where the plugin decides to show
|
|
34
|
+
/// UI.
|
|
35
|
+
func requireUI() throws(TokamakPluginError) {
|
|
36
|
+
#if os(iOS)
|
|
37
|
+
guard app.isInForeground else { throw .needsUI }
|
|
38
|
+
#endif
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Delivers the plugin's event `name`, which the Worker's listeners
|
|
42
|
+
/// receive as `<plugin>.<name>`, with `event`, JSON-serialisable, and calls
|
|
43
|
+
/// `completion` on the main thread with their reply, parsed from JSON, or
|
|
44
|
+
/// nil. Fails unless they return within `timeout`, which includes runtime
|
|
45
|
+
/// startup. Call it on the main thread.
|
|
46
|
+
func emit(
|
|
47
|
+
_ name: String,
|
|
48
|
+
event: Any,
|
|
49
|
+
timeout: TimeInterval,
|
|
50
|
+
completion: @escaping (Result<Any?, Error>) -> Void
|
|
51
|
+
) {
|
|
52
|
+
app.emit("\(plugin).\(name)", event: event, timeout: timeout, completion: completion)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Fetches what the app serves at `path`, as its WebView would load it: a
|
|
56
|
+
/// file the app ships or the Worker's response. Calls `completion` on the
|
|
57
|
+
/// main thread with the body. Fails unless it arrives within `timeout`,
|
|
58
|
+
/// which includes runtime startup. Call it on the main thread.
|
|
59
|
+
func fetch(
|
|
60
|
+
_ path: String,
|
|
61
|
+
timeout: TimeInterval,
|
|
62
|
+
completion: @escaping (Result<Data, Error>) -> Void
|
|
63
|
+
) {
|
|
64
|
+
app.fetch(path, timeout: timeout, completion: completion)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// A native plugin, created once per process when the app launches. The page
|
|
69
|
+
/// and the Worker call it on the main thread.
|
|
70
|
+
protocol TokamakPlugin: AnyObject {
|
|
71
|
+
var id: String { get }
|
|
72
|
+
|
|
73
|
+
init(host: TokamakHost)
|
|
74
|
+
|
|
75
|
+
func call(
|
|
76
|
+
method: String,
|
|
77
|
+
arguments: Any,
|
|
78
|
+
reply: @escaping TokamakPluginReply
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
/// Replies with each value until the returned function is called. Throws
|
|
82
|
+
/// when the subscription cannot start, which ends it.
|
|
83
|
+
func subscribe(
|
|
84
|
+
method: String,
|
|
85
|
+
arguments: Any,
|
|
86
|
+
reply: @escaping TokamakPluginReply
|
|
87
|
+
) throws(TokamakPluginError) -> (() -> Void)
|
|
88
|
+
|
|
89
|
+
func didRegisterForRemoteNotifications(deviceToken: Data)
|
|
90
|
+
|
|
91
|
+
func didFailToRegisterForRemoteNotifications(error: Error)
|
|
92
|
+
|
|
93
|
+
/// Handles a remote notification delivered to the app, calling `completion`
|
|
94
|
+
/// once when finished with it.
|
|
95
|
+
func didReceiveRemoteNotification(
|
|
96
|
+
_ userInfo: [AnyHashable: Any],
|
|
97
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
extension TokamakPlugin {
|
|
102
|
+
func call(
|
|
103
|
+
method: String,
|
|
104
|
+
arguments: Any,
|
|
105
|
+
reply: @escaping TokamakPluginReply
|
|
106
|
+
) {
|
|
107
|
+
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func subscribe(
|
|
111
|
+
method: String,
|
|
112
|
+
arguments: Any,
|
|
113
|
+
reply: @escaping TokamakPluginReply
|
|
114
|
+
) throws(TokamakPluginError) -> (() -> Void) {
|
|
115
|
+
throw .notSupported("\(id).\(method) is not supported")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func didRegisterForRemoteNotifications(deviceToken: Data) {}
|
|
119
|
+
|
|
120
|
+
func didFailToRegisterForRemoteNotifications(error: Error) {}
|
|
121
|
+
|
|
122
|
+
func didReceiveRemoteNotification(
|
|
123
|
+
_ userInfo: [AnyHashable: Any],
|
|
124
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
125
|
+
) {
|
|
126
|
+
completion(.noData)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Runs one caller's calls and subscriptions, the page's or the Worker's, on
|
|
131
|
+
/// the app's plugins, keyed by `Key`. `send` receives each result on the main
|
|
132
|
+
/// thread, as the JSON object the page's native transport receives without its
|
|
133
|
+
/// session and ID. Use it on the main thread.
|
|
134
|
+
final class TokamakPluginRequests<Key: Hashable> {
|
|
135
|
+
private let plugins: [String: any TokamakPlugin]
|
|
136
|
+
private let send: (Key, [String: Any]) -> Void
|
|
137
|
+
private var cancellations: [Key: () -> Void] = [:]
|
|
138
|
+
|
|
139
|
+
init(plugins: [String: any TokamakPlugin], send: @escaping (Key, [String: Any]) -> Void) {
|
|
140
|
+
self.plugins = plugins
|
|
141
|
+
self.send = send
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Calls `method` of `plugin`, or subscribes to it when `subscribe`.
|
|
145
|
+
func run(_ key: Key, subscribe: Bool, plugin: String, method: String, arguments: Any) {
|
|
146
|
+
guard let target = plugins[plugin], !method.isEmpty else {
|
|
147
|
+
fail(key, .notSupported("Plugin is not supported"))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
// Plugins reply from any thread.
|
|
151
|
+
let reply: TokamakPluginReply = { [weak self] result in
|
|
152
|
+
DispatchQueue.main.async {
|
|
153
|
+
self?.send(key, Self.result(result, done: !subscribe))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
guard subscribe else {
|
|
157
|
+
target.call(method: method, arguments: arguments, reply: reply)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
do {
|
|
161
|
+
cancellations[key] = try target.subscribe(method: method, arguments: arguments, reply: reply)
|
|
162
|
+
} catch {
|
|
163
|
+
fail(key, error)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Ends the request `key` with `error`.
|
|
168
|
+
func fail(_ key: Key, _ error: TokamakPluginError) {
|
|
169
|
+
send(key, Self.result(.failure(error), done: true))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Ends the subscription `key`.
|
|
173
|
+
func cancel(_ key: Key) {
|
|
174
|
+
cancellations.removeValue(forKey: key)?()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Ends every subscription.
|
|
178
|
+
func cancelAll() {
|
|
179
|
+
let cancelAll = Array(cancellations.values)
|
|
180
|
+
cancellations.removeAll()
|
|
181
|
+
for cancel in cancelAll {
|
|
182
|
+
cancel()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// `result` as JSON, or the failure to make it JSON.
|
|
187
|
+
private static func result(
|
|
188
|
+
_ result: Result<Any?, TokamakPluginError>,
|
|
189
|
+
done: Bool
|
|
190
|
+
) -> [String: Any] {
|
|
191
|
+
switch result {
|
|
192
|
+
case .success(let value):
|
|
193
|
+
let json: [String: Any] = ["value": value ?? NSNull(), "done": done]
|
|
194
|
+
if JSONSerialization.isValidJSONObject(json) {
|
|
195
|
+
return json
|
|
196
|
+
}
|
|
197
|
+
let error = TokamakPluginError(
|
|
198
|
+
name: "OperationError", message: "The plugin's value is not JSON")
|
|
199
|
+
return Self.result(.failure(error), done: done)
|
|
200
|
+
case .failure(let error):
|
|
201
|
+
return ["error": ["name": error.name, "message": error.message], "done": done]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
|
|
207
|
+
private struct RequestKey: Hashable {
|
|
208
|
+
let session: String
|
|
209
|
+
let id: Int
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private let host: String
|
|
213
|
+
private let plugins: [String: any TokamakPlugin]
|
|
214
|
+
private lazy var requests = TokamakPluginRequests<RequestKey>(plugins: plugins) {
|
|
215
|
+
[weak self] key, result in
|
|
216
|
+
self?.deliver(key: key, result: result)
|
|
217
|
+
}
|
|
218
|
+
weak var webView: WKWebView?
|
|
219
|
+
|
|
220
|
+
init(host: String, plugins: [String: any TokamakPlugin]) {
|
|
221
|
+
self.host = host
|
|
222
|
+
self.plugins = plugins
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func install(in controller: WKUserContentController) {
|
|
226
|
+
controller.add(self, name: "tokamak")
|
|
227
|
+
controller.addUserScript(
|
|
228
|
+
WKUserScript(
|
|
229
|
+
source: Self.bootstrap(host: host),
|
|
230
|
+
injectionTime: .atDocumentStart,
|
|
231
|
+
forMainFrameOnly: true
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func close() {
|
|
237
|
+
requests.cancelAll()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
func userContentController(
|
|
241
|
+
_ userContentController: WKUserContentController,
|
|
242
|
+
didReceive message: WKScriptMessage
|
|
243
|
+
) {
|
|
244
|
+
guard
|
|
245
|
+
message.frameInfo.isMainFrame,
|
|
246
|
+
message.frameInfo.securityOrigin.isAppOrigin(host),
|
|
247
|
+
let encoded = message.body as? String,
|
|
248
|
+
let data = encoded.data(using: .utf8),
|
|
249
|
+
let request = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
250
|
+
let type = request["type"] as? String,
|
|
251
|
+
let session = request["session"] as? String
|
|
252
|
+
else {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if type == "reset" {
|
|
257
|
+
close()
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
guard let id = request["id"] as? Int else { return }
|
|
261
|
+
let key = RequestKey(session: session, id: id)
|
|
262
|
+
switch type {
|
|
263
|
+
case "cancel":
|
|
264
|
+
requests.cancel(key)
|
|
265
|
+
case "call", "subscribe":
|
|
266
|
+
requests.run(
|
|
267
|
+
key,
|
|
268
|
+
subscribe: type == "subscribe",
|
|
269
|
+
plugin: request["plugin"] as? String ?? "",
|
|
270
|
+
method: request["method"] as? String ?? "",
|
|
271
|
+
arguments: request["arguments"] ?? NSNull()
|
|
272
|
+
)
|
|
273
|
+
default:
|
|
274
|
+
requests.fail(key, .notSupported("Plugin operation is not supported"))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private func deliver(key: RequestKey, result: [String: Any]) {
|
|
279
|
+
var response = result
|
|
280
|
+
response["session"] = key.session
|
|
281
|
+
response["id"] = key.id
|
|
282
|
+
guard
|
|
283
|
+
let data = try? JSONSerialization.data(withJSONObject: response),
|
|
284
|
+
let json = String(data: data, encoding: .utf8),
|
|
285
|
+
webView?.url?.isAppOrigin(host) == true
|
|
286
|
+
else {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
webView?.callAsyncJavaScript(
|
|
290
|
+
"globalThis.__tokamakNative?.onmessage?.({ data })",
|
|
291
|
+
arguments: ["data": json],
|
|
292
|
+
in: nil,
|
|
293
|
+
in: .page
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private static func bootstrap(host: String) -> String {
|
|
298
|
+
"""
|
|
299
|
+
if (globalThis.location.origin === "https://\(host)") {
|
|
300
|
+
globalThis.__tokamakNative = {
|
|
301
|
+
onmessage: null,
|
|
302
|
+
postMessage(message) {
|
|
303
|
+
globalThis.webkit.messageHandlers.tokamak.postMessage(message);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
"""
|
|
308
|
+
}
|
|
309
|
+
}
|