@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.107 → 0.1.1-beta.112
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 +25 -5
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +10 -4
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-x64/native-shell/TokamakApp.swift +81 -63
- package/ios-simulator-x64/native-shell/TokamakBindings.swift +347 -0
- package/ios-simulator-x64/native-shell/TokamakControls.swift +98 -0
- package/ios-simulator-x64/native-shell/TokamakOrigin.swift +7 -0
- package/ios-simulator-x64/native-shell/TokamakPlugins.swift +307 -156
- package/ios-simulator-x64/native-shell/TokamakResources.swift +74 -0
- package/ios-simulator-x64/native-shell/TokamakShell.swift +3 -20
- package/ios-simulator-x64/platform-pack.json +1 -0
- package/ios-simulator-x64/tools/tokamak-apple-signing +0 -0
- package/package.json +1 -1
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import WebKit
|
|
3
3
|
|
|
4
|
+
#if os(iOS)
|
|
5
|
+
import UIKit
|
|
6
|
+
#else
|
|
7
|
+
import AppKit
|
|
8
|
+
#endif
|
|
9
|
+
|
|
4
10
|
struct TokamakPluginError: Error {
|
|
5
11
|
let name: String
|
|
6
12
|
let message: String
|
|
13
|
+
/// JSON details the Worker receives as the error's `cause`.
|
|
14
|
+
var cause: Any? = nil
|
|
7
15
|
|
|
8
16
|
static func notSupported(_ message: String) -> Self {
|
|
9
17
|
Self(name: "NotSupportedError", message: message)
|
|
10
18
|
}
|
|
11
19
|
|
|
20
|
+
/// A native screen was needed while the app has no window.
|
|
21
|
+
static let noWindow = Self(name: "InvalidStateError", message: "The app has no window")
|
|
22
|
+
|
|
12
23
|
/// UI was needed while the app cannot show it.
|
|
13
24
|
static let needsUI = Self(
|
|
14
25
|
name: "NeedsUIError",
|
|
@@ -17,6 +28,68 @@ struct TokamakPluginError: Error {
|
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
|
|
31
|
+
typealias TokamakPluginBytesReply = (TokamakPluginResult) -> Void
|
|
32
|
+
|
|
33
|
+
struct TokamakPluginResult {
|
|
34
|
+
let payload: Data
|
|
35
|
+
let failed: Bool
|
|
36
|
+
let done: Bool
|
|
37
|
+
|
|
38
|
+
init(payload: Data, failed: Bool, done: Bool) {
|
|
39
|
+
self.payload = payload
|
|
40
|
+
self.failed = failed
|
|
41
|
+
self.done = done
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
init(_ result: Result<Data, TokamakPluginError>, done: Bool) {
|
|
45
|
+
self.done = done
|
|
46
|
+
switch result {
|
|
47
|
+
case .success(let payload):
|
|
48
|
+
self.payload = payload
|
|
49
|
+
failed = false
|
|
50
|
+
case .failure(let error):
|
|
51
|
+
var failure: [String: Any] = ["name": error.name, "message": error.message]
|
|
52
|
+
failure["cause"] = error.cause
|
|
53
|
+
payload = try! JSONSerialization.data(withJSONObject: failure)
|
|
54
|
+
failed = true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// `value` as a payload: `Data` as it is, and any other value as JSON.
|
|
60
|
+
func encodePluginValue(_ value: Any?) -> Result<Data, TokamakPluginError> {
|
|
61
|
+
if let data = value as? Data { return .success(data) }
|
|
62
|
+
let value = value ?? NSNull()
|
|
63
|
+
// JSONSerialization raises, rather than throws, for values it cannot encode.
|
|
64
|
+
guard JSONSerialization.isValidJSONObject([value]),
|
|
65
|
+
let json = try? JSONSerialization.data(withJSONObject: value, options: .fragmentsAllowed)
|
|
66
|
+
else {
|
|
67
|
+
return .failure(.init(name: "DataCloneError", message: "The value cannot be encoded as JSON"))
|
|
68
|
+
}
|
|
69
|
+
return .success(json)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Decodes `payload` as JSON for `call`, and encodes its reply.
|
|
73
|
+
private func callJSON(
|
|
74
|
+
_ payload: Data,
|
|
75
|
+
_ reply: @escaping TokamakPluginBytesReply,
|
|
76
|
+
_ call: (Any, @escaping TokamakPluginReply) -> (() -> Void)?
|
|
77
|
+
) -> (() -> Void)? {
|
|
78
|
+
do {
|
|
79
|
+
return call(try decodePluginValue(payload)) {
|
|
80
|
+
reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: true))
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
reply(TokamakPluginResult(.failure(error), done: true))
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func decodePluginValue(_ payload: Data) throws(TokamakPluginError) -> Any {
|
|
89
|
+
do { return try JSONSerialization.jsonObject(with: payload, options: .fragmentsAllowed) } catch {
|
|
90
|
+
throw .init(name: "DataCloneError", message: error.localizedDescription)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
20
93
|
|
|
21
94
|
/// The app as one plugin sees it.
|
|
22
95
|
final class TokamakHost {
|
|
@@ -28,6 +101,11 @@ final class TokamakHost {
|
|
|
28
101
|
self.plugin = plugin
|
|
29
102
|
}
|
|
30
103
|
|
|
104
|
+
var webView: WKWebView? { app.webView }
|
|
105
|
+
|
|
106
|
+
/// The native objects plugins share with each other by ID.
|
|
107
|
+
var resources: TokamakResources { app.resources }
|
|
108
|
+
|
|
31
109
|
/// Throws `NeedsUIError` unless the app can show UI now: on iOS, while it
|
|
32
110
|
/// is in the foreground. A hidden or minimised macOS app still shows system
|
|
33
111
|
/// prompts. Call it on the main thread, where the plugin decides to show
|
|
@@ -38,18 +116,61 @@ final class TokamakHost {
|
|
|
38
116
|
#endif
|
|
39
117
|
}
|
|
40
118
|
|
|
119
|
+
#if os(iOS)
|
|
120
|
+
/// The view controller to present a native screen from: the top of the
|
|
121
|
+
/// app window's presentation stack. Throws `NeedsUIError` unless the app
|
|
122
|
+
/// can show UI now. Call it on the main thread.
|
|
123
|
+
func presenter() throws(TokamakPluginError) -> UIViewController {
|
|
124
|
+
try requireUI()
|
|
125
|
+
guard var top = webView?.window?.rootViewController else { throw .noWindow }
|
|
126
|
+
while let presented = top.presentedViewController { top = presented }
|
|
127
|
+
return top
|
|
128
|
+
}
|
|
129
|
+
#else
|
|
130
|
+
/// The app window, to present a native screen over. Call it on the main
|
|
131
|
+
/// thread.
|
|
132
|
+
func presenter() throws(TokamakPluginError) -> NSWindow {
|
|
133
|
+
guard let window = webView?.window else { throw .noWindow }
|
|
134
|
+
return window
|
|
135
|
+
}
|
|
136
|
+
#endif
|
|
137
|
+
|
|
41
138
|
/// Delivers the plugin's event `name`, which the Worker's listeners
|
|
42
|
-
/// receive as `<plugin>.<name>`, with `
|
|
43
|
-
/// `completion` on the main thread with their reply,
|
|
44
|
-
///
|
|
45
|
-
/// startup. Call it on the main thread.
|
|
139
|
+
/// receive as `<plugin>.<name>`, with its encoded `payload`, and calls
|
|
140
|
+
/// `completion` on the main thread with their encoded reply, which is empty
|
|
141
|
+
/// when none replied. Fails unless they return within `timeout`, which
|
|
142
|
+
/// includes runtime startup. Call it on the main thread.
|
|
143
|
+
func emitBytes(
|
|
144
|
+
_ name: String,
|
|
145
|
+
payload: Data,
|
|
146
|
+
timeout: TimeInterval,
|
|
147
|
+
completion: @escaping (Result<Data, Error>) -> Void
|
|
148
|
+
) {
|
|
149
|
+
app.emit("\(plugin).\(name)", payload: payload, timeout: timeout, completion: completion)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// `emitBytes` with `event` encoded as JSON, calling `completion` with the
|
|
153
|
+
/// reply parsed from JSON, or nil when none replied.
|
|
46
154
|
func emit(
|
|
47
155
|
_ name: String,
|
|
48
156
|
event: Any,
|
|
49
157
|
timeout: TimeInterval,
|
|
50
158
|
completion: @escaping (Result<Any?, Error>) -> Void
|
|
51
159
|
) {
|
|
52
|
-
|
|
160
|
+
let payload: Data
|
|
161
|
+
switch encodePluginValue(event) {
|
|
162
|
+
case .success(let encoded): payload = encoded
|
|
163
|
+
case .failure(let error): return completion(.failure(error))
|
|
164
|
+
}
|
|
165
|
+
emitBytes(name, payload: payload, timeout: timeout) { result in
|
|
166
|
+
completion(
|
|
167
|
+
result.flatMap { reply in
|
|
168
|
+
Result { () throws(TokamakPluginError) -> Any? in
|
|
169
|
+
let value = reply.isEmpty ? NSNull() : try decodePluginValue(reply)
|
|
170
|
+
return value is NSNull ? nil : value
|
|
171
|
+
}.mapError { $0 }
|
|
172
|
+
})
|
|
173
|
+
}
|
|
53
174
|
}
|
|
54
175
|
|
|
55
176
|
/// Fetches what the app serves at `path`, as its WebView would load it: a
|
|
@@ -65,18 +186,40 @@ final class TokamakHost {
|
|
|
65
186
|
}
|
|
66
187
|
}
|
|
67
188
|
|
|
68
|
-
/// A native plugin, created once per process
|
|
69
|
-
/// and the Worker call it on the main thread.
|
|
189
|
+
/// A native plugin, created once per process.
|
|
70
190
|
protocol TokamakPlugin: AnyObject {
|
|
71
191
|
var id: String { get }
|
|
72
192
|
|
|
73
193
|
init(host: TokamakHost)
|
|
74
194
|
|
|
195
|
+
/// Serial executor for this operation and its cancellation.
|
|
196
|
+
func executor(for method: String) -> DispatchQueue
|
|
197
|
+
|
|
198
|
+
func callBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply) -> (
|
|
199
|
+
() -> Void
|
|
200
|
+
)?
|
|
201
|
+
func callBytes(
|
|
202
|
+
method: String, payload: Data, body: TokamakPluginBody,
|
|
203
|
+
reply: @escaping TokamakPluginBytesReply
|
|
204
|
+
) -> (() -> Void)?
|
|
205
|
+
func subscribeBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply)
|
|
206
|
+
throws(TokamakPluginError) -> (() -> Void)
|
|
207
|
+
|
|
208
|
+
/// Returns cancellation for pending work when the platform supports it.
|
|
75
209
|
func call(
|
|
76
210
|
method: String,
|
|
77
211
|
arguments: Any,
|
|
78
212
|
reply: @escaping TokamakPluginReply
|
|
79
|
-
)
|
|
213
|
+
) -> (() -> Void)?
|
|
214
|
+
|
|
215
|
+
/// A call whose `body` of bytes the Worker streams, which the plugin reads
|
|
216
|
+
/// one chunk at a time.
|
|
217
|
+
func call(
|
|
218
|
+
method: String,
|
|
219
|
+
arguments: Any,
|
|
220
|
+
body: TokamakPluginBody,
|
|
221
|
+
reply: @escaping TokamakPluginReply
|
|
222
|
+
) -> (() -> Void)?
|
|
80
223
|
|
|
81
224
|
/// Replies with each value until the returned function is called. Throws
|
|
82
225
|
/// when the subscription cannot start, which ends it.
|
|
@@ -99,12 +242,46 @@ protocol TokamakPlugin: AnyObject {
|
|
|
99
242
|
}
|
|
100
243
|
|
|
101
244
|
extension TokamakPlugin {
|
|
245
|
+
func executor(for method: String) -> DispatchQueue { .main }
|
|
246
|
+
|
|
247
|
+
func callBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply) -> (
|
|
248
|
+
() -> Void
|
|
249
|
+
)? {
|
|
250
|
+
callJSON(payload, reply) { call(method: method, arguments: $0, reply: $1) }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
func callBytes(
|
|
254
|
+
method: String, payload: Data, body: TokamakPluginBody,
|
|
255
|
+
reply: @escaping TokamakPluginBytesReply
|
|
256
|
+
) -> (() -> Void)? {
|
|
257
|
+
callJSON(payload, reply) { call(method: method, arguments: $0, body: body, reply: $1) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func subscribeBytes(method: String, payload: Data, reply: @escaping TokamakPluginBytesReply)
|
|
261
|
+
throws(TokamakPluginError) -> (() -> Void)
|
|
262
|
+
{
|
|
263
|
+
try subscribe(method: method, arguments: decodePluginValue(payload)) {
|
|
264
|
+
reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: false))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
102
268
|
func call(
|
|
103
269
|
method: String,
|
|
104
270
|
arguments: Any,
|
|
105
271
|
reply: @escaping TokamakPluginReply
|
|
106
|
-
) {
|
|
272
|
+
) -> (() -> Void)? {
|
|
107
273
|
reply(.failure(.notSupported("\(id).\(method) is not supported")))
|
|
274
|
+
return nil
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func call(
|
|
278
|
+
method: String,
|
|
279
|
+
arguments: Any,
|
|
280
|
+
body: TokamakPluginBody,
|
|
281
|
+
reply: @escaping TokamakPluginReply
|
|
282
|
+
) -> (() -> Void)? {
|
|
283
|
+
reply(.failure(.notSupported("\(id).\(method) does not take a body")))
|
|
284
|
+
return nil
|
|
108
285
|
}
|
|
109
286
|
|
|
110
287
|
func subscribe(
|
|
@@ -127,183 +304,157 @@ extension TokamakPlugin {
|
|
|
127
304
|
}
|
|
128
305
|
}
|
|
129
306
|
|
|
130
|
-
///
|
|
131
|
-
///
|
|
132
|
-
|
|
133
|
-
|
|
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] = [:]
|
|
307
|
+
/// The bytes the Worker streams to a call, which the plugin reads one chunk
|
|
308
|
+
/// at a time.
|
|
309
|
+
final class TokamakPluginBody {
|
|
310
|
+
typealias Chunk = (Result<Data?, TokamakPluginError>) -> Void
|
|
138
311
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
312
|
+
private let queue: DispatchQueue
|
|
313
|
+
private let pull: () -> Void
|
|
314
|
+
private let lock = NSLock()
|
|
315
|
+
private var reading: Chunk?
|
|
166
316
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
317
|
+
init(queue: DispatchQueue, pull: @escaping () -> Void) {
|
|
318
|
+
self.queue = queue
|
|
319
|
+
self.pull = pull
|
|
170
320
|
}
|
|
171
321
|
|
|
172
|
-
///
|
|
173
|
-
|
|
174
|
-
|
|
322
|
+
/// Reads the next chunk: its bytes, nil after the last, or the Worker's
|
|
323
|
+
/// error. `handler` runs on the call's executor; read again only after it
|
|
324
|
+
/// has.
|
|
325
|
+
func read(_ handler: @escaping Chunk) {
|
|
326
|
+
lock.withLock { reading = handler }
|
|
327
|
+
pull()
|
|
175
328
|
}
|
|
176
329
|
|
|
177
|
-
///
|
|
178
|
-
func
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
cancel()
|
|
330
|
+
/// Answers the pending read with the chunk the Worker sent.
|
|
331
|
+
fileprivate func receive(_ chunk: TokamakPluginResult) {
|
|
332
|
+
let handler = lock.withLock {
|
|
333
|
+
defer { reading = nil }
|
|
334
|
+
return reading
|
|
183
335
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
336
|
+
guard let handler else { return }
|
|
337
|
+
queue.async {
|
|
338
|
+
if chunk.failed {
|
|
339
|
+
let error = try? JSONSerialization.jsonObject(with: chunk.payload) as? [String: String]
|
|
340
|
+
handler(.failure(.init(name: error?["name"] ?? "Error", message: error?["message"] ?? "")))
|
|
341
|
+
} else {
|
|
342
|
+
handler(.success(chunk.done ? nil : chunk.payload))
|
|
196
343
|
}
|
|
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
344
|
}
|
|
203
345
|
}
|
|
204
346
|
}
|
|
205
347
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
348
|
+
/// What the Worker asked a plugin for.
|
|
349
|
+
enum TokamakPluginRequest {
|
|
350
|
+
case call, callWithBody, subscribe
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Runs the Worker's requests on the executor each plugin chooses, and passes
|
|
354
|
+
/// replies straight to the runtime.
|
|
355
|
+
final class TokamakPluginRequests<Key: Hashable> {
|
|
356
|
+
/// A pending call or subscription. Only its serial executor touches
|
|
357
|
+
/// `cancellation`, so a cancellation runs after the operation started.
|
|
358
|
+
private final class Request {
|
|
359
|
+
let queue: DispatchQueue
|
|
360
|
+
var body: TokamakPluginBody?
|
|
361
|
+
var cancellation: (() -> Void)?
|
|
362
|
+
init(queue: DispatchQueue) { self.queue = queue }
|
|
210
363
|
}
|
|
211
364
|
|
|
212
|
-
private let host: String
|
|
213
365
|
private let plugins: [String: any TokamakPlugin]
|
|
214
|
-
private
|
|
215
|
-
|
|
216
|
-
|
|
366
|
+
private let send: (Key, TokamakPluginResult) -> Void
|
|
367
|
+
private let pull: (Key) -> Void
|
|
368
|
+
private let lock = NSLock()
|
|
369
|
+
private var pending: [Key: Request] = [:]
|
|
370
|
+
|
|
371
|
+
/// Routes requests to `plugins`, sending their results with `send` and
|
|
372
|
+
/// asking the Worker for the next chunk of a call's body with `pull`.
|
|
373
|
+
init(
|
|
374
|
+
plugins: [String: any TokamakPlugin],
|
|
375
|
+
send: @escaping (Key, TokamakPluginResult) -> Void,
|
|
376
|
+
pull: @escaping (Key) -> Void = { _ in }
|
|
377
|
+
) {
|
|
378
|
+
self.plugins = plugins
|
|
379
|
+
self.send = send
|
|
380
|
+
self.pull = pull
|
|
217
381
|
}
|
|
218
|
-
weak var webView: WKWebView?
|
|
219
382
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
383
|
+
func run(
|
|
384
|
+
_ key: Key, _ kind: TokamakPluginRequest, plugin: String, method: String, payload: Data
|
|
385
|
+
) {
|
|
386
|
+
guard let target = plugins[plugin], !method.isEmpty else {
|
|
387
|
+
send(key, TokamakPluginResult(.failure(.notSupported("Plugin is not supported")), done: true))
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
let request = Request(queue: target.executor(for: method))
|
|
391
|
+
if kind == .callWithBody {
|
|
392
|
+
request.body = TokamakPluginBody(queue: request.queue) { [weak self] in self?.pull(key) }
|
|
393
|
+
}
|
|
394
|
+
lock.withLock { pending[key] = request }
|
|
395
|
+
let subscribe = kind == .subscribe
|
|
396
|
+
// A weak request lets a completed call release its cancellation.
|
|
397
|
+
let reply: TokamakPluginBytesReply = { [weak self, weak request] result in
|
|
398
|
+
guard let self, let request else { return }
|
|
399
|
+
let last = !subscribe || result.done
|
|
400
|
+
guard last ? remove(key, request) : isPending(key, request) else { return }
|
|
401
|
+
send(key, result)
|
|
402
|
+
if subscribe && result.done { cancel(request) }
|
|
403
|
+
}
|
|
404
|
+
request.queue.async { [self] in
|
|
405
|
+
guard isPending(key, request) else { return }
|
|
406
|
+
do throws(TokamakPluginError) {
|
|
407
|
+
if subscribe {
|
|
408
|
+
request.cancellation = try target.subscribeBytes(
|
|
409
|
+
method: method, payload: payload, reply: reply)
|
|
410
|
+
} else if let body = request.body {
|
|
411
|
+
request.cancellation = target.callBytes(
|
|
412
|
+
method: method, payload: payload, body: body, reply: reply)
|
|
413
|
+
} else {
|
|
414
|
+
request.cancellation = target.callBytes(method: method, payload: payload, reply: reply)
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
if remove(key, request) { send(key, TokamakPluginResult(.failure(error), done: true)) }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
223
420
|
}
|
|
224
421
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
source: Self.bootstrap(host: host),
|
|
230
|
-
injectionTime: .atDocumentStart,
|
|
231
|
-
forMainFrameOnly: true
|
|
232
|
-
)
|
|
233
|
-
)
|
|
422
|
+
/// Answers the pending read of the call `key`'s body with the chunk the
|
|
423
|
+
/// Worker sent.
|
|
424
|
+
func chunk(_ key: Key, _ chunk: TokamakPluginResult) {
|
|
425
|
+
lock.withLock { pending[key]?.body }?.receive(chunk)
|
|
234
426
|
}
|
|
235
427
|
|
|
236
|
-
func
|
|
237
|
-
|
|
428
|
+
func cancel(_ key: Key) {
|
|
429
|
+
if let request = lock.withLock({ pending.removeValue(forKey: key) }) { cancel(request) }
|
|
238
430
|
}
|
|
239
431
|
|
|
240
|
-
func
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
432
|
+
func cancelAll() {
|
|
433
|
+
let requests = lock.withLock {
|
|
434
|
+
defer { pending.removeAll() }
|
|
435
|
+
return Array(pending.values)
|
|
254
436
|
}
|
|
437
|
+
for request in requests { cancel(request) }
|
|
438
|
+
}
|
|
255
439
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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"))
|
|
440
|
+
/// Runs the operation's cancellation on its executor.
|
|
441
|
+
private func cancel(_ request: Request) {
|
|
442
|
+
request.queue.async {
|
|
443
|
+
request.cancellation?()
|
|
444
|
+
request.cancellation = nil
|
|
275
445
|
}
|
|
276
446
|
}
|
|
277
447
|
|
|
278
|
-
private func
|
|
279
|
-
|
|
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
|
-
)
|
|
448
|
+
private func isPending(_ key: Key, _ request: Request) -> Bool {
|
|
449
|
+
lock.withLock { pending[key] === request }
|
|
295
450
|
}
|
|
296
451
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
globalThis.webkit.messageHandlers.tokamak.postMessage(message);
|
|
304
|
-
}
|
|
305
|
-
};
|
|
452
|
+
/// Removes `request`, returning whether it was pending.
|
|
453
|
+
private func remove(_ key: Key, _ request: Request) -> Bool {
|
|
454
|
+
lock.withLock {
|
|
455
|
+
guard pending[key] === request else { return false }
|
|
456
|
+
pending[key] = nil
|
|
457
|
+
return true
|
|
306
458
|
}
|
|
307
|
-
"""
|
|
308
459
|
}
|
|
309
460
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Native objects that plugins share by ID. Each belongs to a scope and is
|
|
4
|
+
/// kept until it is released or its scope ends.
|
|
5
|
+
final class TokamakResources {
|
|
6
|
+
private let lock = NSLock()
|
|
7
|
+
private var objects: [String: (object: Any, scope: String)] = [:]
|
|
8
|
+
private var scopes: [String: Set<String>] = [:]
|
|
9
|
+
|
|
10
|
+
/// Starts a scope and returns its ID.
|
|
11
|
+
func open() -> String {
|
|
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`.
|
|
30
|
+
func retain(_ object: Any, reply: TokamakPluginReply) -> () -> Void {
|
|
31
|
+
let scope = open()
|
|
32
|
+
reply(.success(retain(object, in: scope)))
|
|
33
|
+
return { [self] in end(scope) }
|
|
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)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// The object retained as `id`.
|
|
57
|
+
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]?.object }) else {
|
|
59
|
+
throw TokamakPluginError(name: "InvalidStateError", message: "The native reference is closed")
|
|
60
|
+
}
|
|
61
|
+
guard let typed = object as? Object else {
|
|
62
|
+
throw TokamakPluginError(
|
|
63
|
+
name: "TypeError", message: "The native reference is not \(Object.self)")
|
|
64
|
+
}
|
|
65
|
+
return typed
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Lets go of released objects on the main thread, where UI and run loop
|
|
69
|
+
/// objects expect to be deallocated.
|
|
70
|
+
private func drop(_ objects: Any?) {
|
|
71
|
+
guard let objects else { return }
|
|
72
|
+
DispatchQueue.main.async { withExtendedLifetime(objects) {} }
|
|
73
|
+
}
|
|
74
|
+
}
|