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

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