@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.83 → 0.1.1-beta.85

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.
@@ -21,14 +21,27 @@ typedef struct {
21
21
  TokamakBytes private_key;
22
22
  } TokamakIdentity;
23
23
 
24
+ typedef void (*TokamakPluginRequest)(void *context, uint64_t id,
25
+ const char *plugin, const char *method,
26
+ const char *arguments);
27
+
28
+ typedef struct {
29
+ void *context;
30
+ TokamakPluginRequest call;
31
+ TokamakPluginRequest subscribe;
32
+ void (*unsubscribe)(void *context, uint64_t id);
33
+ } TokamakPluginHandler;
34
+
24
35
  void *tokamak_runtime_start(const char *packaged_dir, const char *state_dir,
25
36
  const char *storage_dir, const char *host,
26
- bool foreground, char *error, size_t error_len);
37
+ bool foreground, TokamakPluginHandler plugins,
38
+ char *error, size_t error_len);
27
39
  void *tokamak_runtime_start_development(const char *state_dir, const char *host,
28
40
  const char *endpoint,
29
41
  const char *session_token,
30
- bool foreground, char *error,
31
- size_t error_len);
42
+ bool foreground,
43
+ TokamakPluginHandler plugins,
44
+ char *error, size_t error_len);
32
45
  uint16_t tokamak_runtime_port(const void *runtime);
33
46
  uint16_t tokamak_runtime_restore_gateway(const void *runtime, char *error,
34
47
  size_t error_len);
@@ -36,6 +49,9 @@ void tokamak_runtime_stop(void *runtime);
36
49
  bool tokamak_runtime_emit(const void *runtime, const char *name,
37
50
  const char *event, uint64_t timeout_ms,
38
51
  TokamakBytes *reply, char *error, size_t error_len);
52
+ bool tokamak_runtime_set_foreground(const void *runtime, bool foreground);
53
+ bool tokamak_runtime_is_foreground(const void *runtime);
54
+ void tokamak_runtime_reply(const void *runtime, uint64_t id, const char *result);
39
55
 
40
56
  int32_t tokamak_runtime_server_authority(const void *runtime, const char *host,
41
57
  TokamakBytes *authority);
@@ -17,9 +17,19 @@ private let lifecycleDeadline: TimeInterval = 10
17
17
  /// WebView, so plugins receive app events before a page loads and a Worker
18
18
  /// event can run while no page is loaded.
19
19
  final class TokamakApp {
20
- private(set) var plugins: [any TokamakPlugin] = []
20
+ /// Every plugin, by ID.
21
+ private(set) var plugins: [String: any TokamakPlugin] = [:]
21
22
  private var runtime: Result<RuntimeHandle, Error>?
22
23
  private var waiting: [(Result<RuntimeHandle, Error>) -> Void] = []
24
+ /// The Worker's calls and subscriptions on the plugins.
25
+ private lazy var workerRequests = TokamakPluginRequests<UInt64>(plugins: plugins) {
26
+ [unowned self] id, result in
27
+ whenStarted { started in
28
+ if case .success(let runtime) = started {
29
+ runtime.reply(id, result: result)
30
+ }
31
+ }
32
+ }
23
33
  /// Delivers `resume` and `suspend` one at a time, in order.
24
34
  private let lifecycle = DispatchQueue(label: "tokamak.lifecycle")
25
35
  #if os(iOS)
@@ -29,19 +39,60 @@ final class TokamakApp {
29
39
 
30
40
  /// Creates the plugins. Call it before launch finishes.
31
41
  init() {
32
- plugins = tokamakPlugins(app: self)
42
+ for plugin in tokamakPlugins(app: self) {
43
+ precondition(plugins.updateValue(plugin, forKey: plugin.id) == nil, "Duplicate plugin ID")
44
+ }
33
45
  }
34
46
 
35
47
  /// Starts the runtime in the background, which delivers `start` with
36
48
  /// `foreground`. Call it once, on the main thread.
37
49
  func start(foreground: Bool) {
38
50
  dispatchPrecondition(condition: .onQueue(.main))
51
+ let plugins = workerPlugins
39
52
  DispatchQueue.global(qos: .userInitiated).async {
40
- let result = Result { try RuntimeHandle(foreground: foreground) }
53
+ let result = Result { try RuntimeHandle(foreground: foreground, plugins: plugins) }
41
54
  DispatchQueue.main.async { self.started(result) }
42
55
  }
43
56
  }
44
57
 
58
+ /// The plugins as the runtime calls them, on its own threads. The app
59
+ /// outlives the runtime, which holds it unretained.
60
+ private var workerPlugins: TokamakPluginHandler {
61
+ TokamakPluginHandler(
62
+ context: Unmanaged.passUnretained(self).toOpaque(),
63
+ call: { context, id, plugin, method, arguments in
64
+ TokamakApp.receive(context, id, subscribe: false, plugin, method, arguments)
65
+ },
66
+ subscribe: { context, id, plugin, method, arguments in
67
+ TokamakApp.receive(context, id, subscribe: true, plugin, method, arguments)
68
+ },
69
+ unsubscribe: { context, id in
70
+ let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
71
+ DispatchQueue.main.async { app.workerRequests.cancel(id) }
72
+ }
73
+ )
74
+ }
75
+
76
+ /// Runs a call or subscription the Worker made through `context`'s app.
77
+ private static func receive(
78
+ _ context: UnsafeMutableRawPointer?,
79
+ _ id: UInt64,
80
+ subscribe: Bool,
81
+ _ plugin: UnsafePointer<CChar>?,
82
+ _ method: UnsafePointer<CChar>?,
83
+ _ arguments: UnsafePointer<CChar>?
84
+ ) {
85
+ let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
86
+ let (plugin, method) = (String(cString: plugin!), String(cString: method!))
87
+ let arguments =
88
+ (try? JSONSerialization.jsonObject(
89
+ with: Data(String(cString: arguments!).utf8), options: .fragmentsAllowed)) ?? NSNull()
90
+ DispatchQueue.main.async {
91
+ app.workerRequests.run(
92
+ id, subscribe: subscribe, plugin: plugin, method: method, arguments: arguments)
93
+ }
94
+ }
95
+
45
96
  /// Calls `completion` on the main thread once the runtime has started or
46
97
  /// failed to start.
47
98
  func whenStarted(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
@@ -81,8 +132,9 @@ final class TokamakApp {
81
132
  }
82
133
  }
83
134
 
84
- /// Delivers `resume` when the app moves into the foreground and `suspend`
85
- /// when it moves out. Call it on the main thread.
135
+ /// Records the app moving into or out of the foreground once the runtime
136
+ /// has started, and delivers `resume` or `suspend` when that changed its
137
+ /// stage. Call it on the main thread.
86
138
  func moved(toForeground foreground: Bool) {
87
139
  let name = foreground ? "resume" : "suspend"
88
140
  #if os(iOS)
@@ -103,11 +155,17 @@ final class TokamakApp {
103
155
  let finish = {}
104
156
  let timeout = lifecycleDeadline
105
157
  #endif
106
- emit(name, event: [String: Any](), timeout: timeout, on: lifecycle) { result in
107
- if case .failure(let error) = result {
108
- print("tokamak \(name) failed: \(error)")
158
+ whenStarted { started in
159
+ guard case .success(let runtime) = started, runtime.setForeground(foreground) else {
160
+ finish()
161
+ return
162
+ }
163
+ self.emit(name, event: [String: Any](), timeout: timeout, on: self.lifecycle) { result in
164
+ if case .failure(let error) = result {
165
+ print("tokamak \(name) failed: \(error)")
166
+ }
167
+ finish()
109
168
  }
110
- finish()
111
169
  }
112
170
  }
113
171
 
@@ -129,19 +187,27 @@ final class TokamakApp {
129
187
  }
130
188
 
131
189
  /// Whether a scene is in the foreground.
132
- var isInForeground: Bool {
190
+ var hasForegroundScene: Bool {
133
191
  foregroundScenes > 0
134
192
  }
193
+
194
+ /// Whether the app is in the foreground, as the runtime records it, or
195
+ /// until the runtime has started, whether a scene is in the foreground.
196
+ /// Read it on the main thread.
197
+ var isInForeground: Bool {
198
+ guard case .success(let runtime) = runtime else { return hasForegroundScene }
199
+ return runtime.isForeground
200
+ }
135
201
  #endif
136
202
 
137
203
  func didRegisterForRemoteNotifications(deviceToken: Data) {
138
- for plugin in plugins {
204
+ for plugin in plugins.values {
139
205
  plugin.didRegisterForRemoteNotifications(deviceToken: deviceToken)
140
206
  }
141
207
  }
142
208
 
143
209
  func didFailToRegisterForRemoteNotifications(error: Error) {
144
- for plugin in plugins {
210
+ for plugin in plugins.values {
145
211
  plugin.didFailToRegisterForRemoteNotifications(error: error)
146
212
  }
147
213
  }
@@ -154,7 +220,7 @@ final class TokamakApp {
154
220
  ) {
155
221
  let group = DispatchGroup()
156
222
  var results: [TokamakBackgroundResult] = []
157
- for plugin in plugins {
223
+ for plugin in plugins.values {
158
224
  group.enter()
159
225
  plugin.didReceiveRemoteNotification(userInfo) { result in
160
226
  DispatchQueue.main.async {
@@ -223,8 +289,9 @@ final class RuntimeHandle {
223
289
  let appHost: String
224
290
  private var handle: UnsafeMutableRawPointer?
225
291
 
226
- /// Starts the runtime, which delivers `start` with `foreground`.
227
- init(foreground: Bool) throws {
292
+ /// Starts the runtime, which delivers `start` with `foreground`, and whose
293
+ /// Worker calls `plugins`.
294
+ init(foreground: Bool, plugins: TokamakPluginHandler) throws {
228
295
  guard
229
296
  let appHost = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
230
297
  !appHost.isEmpty
@@ -250,6 +317,7 @@ final class RuntimeHandle {
250
317
  endpoint,
251
318
  sessionToken,
252
319
  foreground,
320
+ plugins,
253
321
  error.baseAddress,
254
322
  error.count
255
323
  )
@@ -275,6 +343,7 @@ final class RuntimeHandle {
275
343
  storagePath,
276
344
  appHost,
277
345
  foreground,
346
+ plugins,
278
347
  error.baseAddress,
279
348
  error.count
280
349
  )
@@ -347,6 +416,25 @@ final class RuntimeHandle {
347
416
  return value is NSNull ? nil : value
348
417
  }
349
418
 
419
+ /// Records whether the app is in the foreground, which the Worker's
420
+ /// `getLifecycleStage()` reports from then on, and returns whether that
421
+ /// changed.
422
+ func setForeground(_ foreground: Bool) -> Bool {
423
+ tokamak_runtime_set_foreground(handle, foreground)
424
+ }
425
+
426
+ /// Whether the app is in the foreground, as last recorded.
427
+ var isForeground: Bool {
428
+ tokamak_runtime_is_foreground(handle)
429
+ }
430
+
431
+ /// Passes a plugin's `result`, JSON-serialisable, to the Worker's call or
432
+ /// subscription `id`.
433
+ func reply(_ id: UInt64, result: [String: Any]) {
434
+ guard let json = try? JSONSerialization.data(withJSONObject: result) else { return }
435
+ String(decoding: json, as: UTF8.self).withCString { tokamak_runtime_reply(handle, id, $0) }
436
+ }
437
+
350
438
  func serverAuthority(host: String) -> AuthenticationMaterial<Data> {
351
439
  var bytes = TokamakBytes()
352
440
  let decision = host.withCString {
@@ -8,6 +8,12 @@ struct TokamakPluginError: Error {
8
8
  static func notSupported(_ message: String) -> Self {
9
9
  Self(name: "NotSupportedError", message: message)
10
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
+ )
11
17
  }
12
18
 
13
19
  typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
@@ -22,6 +28,16 @@ final class TokamakHost {
22
28
  self.plugin = plugin
23
29
  }
24
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
+
25
41
  /// Delivers the plugin's event `name`, which the Worker's listeners
26
42
  /// receive as `<plugin>.<name>`, with `event`, JSON-serialisable, and calls
27
43
  /// `completion` on the main thread with their reply, parsed from JSON, or
@@ -37,7 +53,8 @@ final class TokamakHost {
37
53
  }
38
54
  }
39
55
 
40
- /// A native plugin, created once per process when the app launches.
56
+ /// A native plugin, created once per process when the app launches. The page
57
+ /// and the Worker call it on the main thread.
41
58
  protocol TokamakPlugin: AnyObject {
42
59
  var id: String { get }
43
60
 
@@ -49,11 +66,13 @@ protocol TokamakPlugin: AnyObject {
49
66
  reply: @escaping TokamakPluginReply
50
67
  )
51
68
 
69
+ /// Replies with each value until the returned function is called. Throws
70
+ /// when the subscription cannot start, which ends it.
52
71
  func subscribe(
53
72
  method: String,
54
73
  arguments: Any,
55
74
  reply: @escaping TokamakPluginReply
56
- ) -> (() -> Void)
75
+ ) throws(TokamakPluginError) -> (() -> Void)
57
76
 
58
77
  func didRegisterForRemoteNotifications(deviceToken: Data)
59
78
 
@@ -80,9 +99,8 @@ extension TokamakPlugin {
80
99
  method: String,
81
100
  arguments: Any,
82
101
  reply: @escaping TokamakPluginReply
83
- ) -> (() -> Void) {
84
- reply(.failure(.notSupported("\(id).\(method) is not supported")))
85
- return {}
102
+ ) throws(TokamakPluginError) -> (() -> Void) {
103
+ throw .notSupported("\(id).\(method) is not supported")
86
104
  }
87
105
 
88
106
  func didRegisterForRemoteNotifications(deviceToken: Data) {}
@@ -97,6 +115,82 @@ extension TokamakPlugin {
97
115
  }
98
116
  }
99
117
 
118
+ /// Runs one caller's calls and subscriptions, the page's or the Worker's, on
119
+ /// the app's plugins, keyed by `Key`. `send` receives each result on the main
120
+ /// thread, as the JSON object the page's native transport receives without its
121
+ /// session and ID. Use it on the main thread.
122
+ final class TokamakPluginRequests<Key: Hashable> {
123
+ private let plugins: [String: any TokamakPlugin]
124
+ private let send: (Key, [String: Any]) -> Void
125
+ private var cancellations: [Key: () -> Void] = [:]
126
+
127
+ init(plugins: [String: any TokamakPlugin], send: @escaping (Key, [String: Any]) -> Void) {
128
+ self.plugins = plugins
129
+ self.send = send
130
+ }
131
+
132
+ /// Calls `method` of `plugin`, or subscribes to it when `subscribe`.
133
+ func run(_ key: Key, subscribe: Bool, plugin: String, method: String, arguments: Any) {
134
+ guard let target = plugins[plugin], !method.isEmpty else {
135
+ fail(key, .notSupported("Plugin is not supported"))
136
+ return
137
+ }
138
+ // Plugins reply from any thread.
139
+ let reply: TokamakPluginReply = { [weak self] result in
140
+ DispatchQueue.main.async {
141
+ self?.send(key, Self.result(result, done: !subscribe))
142
+ }
143
+ }
144
+ guard subscribe else {
145
+ target.call(method: method, arguments: arguments, reply: reply)
146
+ return
147
+ }
148
+ do {
149
+ cancellations[key] = try target.subscribe(method: method, arguments: arguments, reply: reply)
150
+ } catch {
151
+ fail(key, error)
152
+ }
153
+ }
154
+
155
+ /// Ends the request `key` with `error`.
156
+ func fail(_ key: Key, _ error: TokamakPluginError) {
157
+ send(key, Self.result(.failure(error), done: true))
158
+ }
159
+
160
+ /// Ends the subscription `key`.
161
+ func cancel(_ key: Key) {
162
+ cancellations.removeValue(forKey: key)?()
163
+ }
164
+
165
+ /// Ends every subscription.
166
+ func cancelAll() {
167
+ let cancelAll = Array(cancellations.values)
168
+ cancellations.removeAll()
169
+ for cancel in cancelAll {
170
+ cancel()
171
+ }
172
+ }
173
+
174
+ /// `result` as JSON, or the failure to make it JSON.
175
+ private static func result(
176
+ _ result: Result<Any?, TokamakPluginError>,
177
+ done: Bool
178
+ ) -> [String: Any] {
179
+ switch result {
180
+ case .success(let value):
181
+ let json: [String: Any] = ["value": value ?? NSNull(), "done": done]
182
+ if JSONSerialization.isValidJSONObject(json) {
183
+ return json
184
+ }
185
+ let error = TokamakPluginError(
186
+ name: "OperationError", message: "The plugin's value is not JSON")
187
+ return Self.result(.failure(error), done: done)
188
+ case .failure(let error):
189
+ return ["error": ["name": error.name, "message": error.message], "done": done]
190
+ }
191
+ }
192
+ }
193
+
100
194
  final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
101
195
  private struct RequestKey: Hashable {
102
196
  let session: String
@@ -105,17 +199,15 @@ final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
105
199
 
106
200
  private let host: String
107
201
  private let plugins: [String: any TokamakPlugin]
108
- private var cancellations: [RequestKey: () -> Void] = [:]
202
+ private lazy var requests = TokamakPluginRequests<RequestKey>(plugins: plugins) {
203
+ [weak self] key, result in
204
+ self?.deliver(key: key, result: result)
205
+ }
109
206
  weak var webView: WKWebView?
110
207
 
111
- init(host: String, plugins: [any TokamakPlugin]) {
208
+ init(host: String, plugins: [String: any TokamakPlugin]) {
112
209
  self.host = host
113
- var pluginsById: [String: any TokamakPlugin] = [:]
114
- for plugin in plugins {
115
- precondition(pluginsById[plugin.id] == nil, "Duplicate plugin ID")
116
- pluginsById[plugin.id] = plugin
117
- }
118
- self.plugins = pluginsById
210
+ self.plugins = plugins
119
211
  }
120
212
 
121
213
  func install(in controller: WKUserContentController) {
@@ -130,11 +222,7 @@ final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
130
222
  }
131
223
 
132
224
  func close() {
133
- let cancelAll = Array(cancellations.values)
134
- cancellations.removeAll()
135
- for cancel in cancelAll {
136
- cancel()
137
- }
225
+ requests.cancelAll()
138
226
  }
139
227
 
140
228
  func userContentController(
@@ -159,66 +247,27 @@ final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
159
247
  }
160
248
  guard let id = request["id"] as? Int else { return }
161
249
  let key = RequestKey(session: session, id: id)
162
- if type == "cancel" {
163
- cancellations.removeValue(forKey: key)?()
164
- return
165
- }
166
- dispatch(type: type, key: key, request: request)
167
- }
168
-
169
- private func dispatch(type: String, key: RequestKey, request: [String: Any]) {
170
- guard
171
- let pluginId = request["plugin"] as? String,
172
- let method = request["method"] as? String,
173
- let plugin = plugins[pluginId]
174
- else {
175
- send(key: key, result: .failure(.notSupported("Plugin is not supported")), done: true)
176
- return
177
- }
178
- let arguments = request["arguments"] ?? NSNull()
179
- let reply: TokamakPluginReply = { [weak self] result in
180
- self?.send(key: key, result: result, done: type == "call")
181
- }
182
250
  switch type {
183
- case "call":
184
- plugin.call(method: method, arguments: arguments, reply: reply)
185
- case "subscribe":
186
- let cancellation = plugin.subscribe(
187
- method: method,
188
- arguments: arguments,
189
- reply: reply
251
+ case "cancel":
252
+ requests.cancel(key)
253
+ case "call", "subscribe":
254
+ requests.run(
255
+ key,
256
+ subscribe: type == "subscribe",
257
+ plugin: request["plugin"] as? String ?? "",
258
+ method: request["method"] as? String ?? "",
259
+ arguments: request["arguments"] ?? NSNull()
190
260
  )
191
- cancellations[key] = cancellation
192
261
  default:
193
- send(
194
- key: key, result: .failure(.notSupported("Plugin operation is not supported")), done: true)
262
+ requests.fail(key, .notSupported("Plugin operation is not supported"))
195
263
  }
196
264
  }
197
265
 
198
- /// Plugins reply from any thread; delivery happens on the main thread.
199
- private func send(key: RequestKey, result: Result<Any?, TokamakPluginError>, done: Bool) {
200
- DispatchQueue.main.async { [weak self] in
201
- self?.deliver(key: key, result: result, done: done)
202
- }
203
- }
204
-
205
- private func deliver(key: RequestKey, result: Result<Any?, TokamakPluginError>, done: Bool) {
206
- if done {
207
- cancellations.removeValue(forKey: key)?()
208
- }
209
- var response: [String: Any] = [
210
- "session": key.session,
211
- "id": key.id,
212
- "done": done,
213
- ]
214
- switch result {
215
- case .success(let value):
216
- response["value"] = value ?? NSNull()
217
- case .failure(let error):
218
- response["error"] = ["name": error.name, "message": error.message]
219
- }
266
+ private func deliver(key: RequestKey, result: [String: Any]) {
267
+ var response = result
268
+ response["session"] = key.session
269
+ response["id"] = key.id
220
270
  guard
221
- JSONSerialization.isValidJSONObject(response),
222
271
  let data = try? JSONSerialization.data(withJSONObject: response),
223
272
  let json = String(data: data, encoding: .utf8),
224
273
  webView?.url?.isAppOrigin(host) == true
@@ -431,7 +431,7 @@ private final class TokamakController {
431
431
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
432
432
  ) -> Bool {
433
433
  DispatchQueue.main.async {
434
- self.app.start(foreground: self.app.isInForeground)
434
+ self.app.start(foreground: self.app.hasForegroundScene)
435
435
  }
436
436
  return true
437
437
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/platform-ios-simulator-x64",
3
- "version": "0.1.1-beta.83",
3
+ "version": "0.1.1-beta.85",
4
4
  "description": "The tokamak platform pack for ios-simulator-x64",
5
5
  "repository": {
6
6
  "type": "git",