@tokamakdev/platform-ios-arm64 0.1.1-beta.127 → 0.1.1-beta.128

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.
@@ -171,38 +171,27 @@ fi
171
171
  # A plugin's class is a Swift type name, which the registry creates, and its
172
172
  # sources are Swift.
173
173
  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 the source staged as
175
- # <plugin>/<key>/<name> compiles as <plugin>-<key>-<name>.
174
+ # Swift requires unique file names in a module, so a plugin's source
175
+ # <name> compiles as <plugin>-<name>.
176
176
  plugin_sources=$temporary/plugin-sources
177
177
  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
178
  registry=$temporary/TokamakPluginRegistry.swift
186
179
  printf '%s\n\n%s\n%s\n' 'import Foundation' 'func tokamakPlugins(app: TokamakApp) -> [any TokamakPlugin] {' ' [' > "$registry"
187
180
  for plugin in "$input/plugins"/*/; do
188
181
  plugin_id=$(basename "$plugin")
189
182
  class=$(cat "$plugin/class" 2>/dev/null || true)
190
- # A plugin whose native code is its generated bindings alone has no class.
191
- if [[ -n "$class" || ! -d "$plugin/bindings" ]]; then
192
- if [[ ! "$class" =~ $swift_type ]]; then
193
- echo "plugin '$plugin_id' has invalid $platform class '$class'" >&2
194
- exit 1
195
- fi
196
- printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$plugin_id" >> "$registry"
183
+ if [[ ! "$class" =~ $swift_type ]]; then
184
+ echo "plugin '$plugin_id' has invalid $platform class '$class'" >&2
185
+ exit 1
197
186
  fi
198
- # `tok bindings` names the generated class TokamakBindings_<id>, with - as _.
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
187
+ printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$plugin_id" >> "$registry"
203
188
  for source in "$plugin/sources"/*; do
204
189
  case "$source" in
205
- *.swift) add_plugin_source "$plugin_id" "$source" ;;
190
+ *.swift)
191
+ named=$plugin_sources/$plugin_id-${source##*/}
192
+ cp "$source" "$named"
193
+ swift_sources+=("$named")
194
+ ;;
206
195
  *)
207
196
  echo "Apple plugin source must be Swift: $source" >&2
208
197
  exit 1
@@ -9,10 +9,9 @@ final class TokamakCorePlugin: TokamakPlugin {
9
9
 
10
10
  init(host: TokamakHost) { self.host = host }
11
11
 
12
- /// WebView operations and the presenter run on the main thread, which
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.") || method == "resource.root" ? .main : queue
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> {
@@ -1,61 +1,23 @@
1
1
  import Foundation
2
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.
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: (object: Any, scope: String)] = [:]
8
- private var scopes: [String: Set<String>] = [:]
7
+ private var objects: [String: Any] = [:]
9
8
 
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`.
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 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)
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]?.object }) else {
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 objects on the main thread, where UI and run loop
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(_ objects: Any?) {
71
- guard let objects else { return }
72
- DispatchQueue.main.async { withExtendedLifetime(objects) {} }
32
+ private func drop(_ object: Any?) {
33
+ guard let object else { return }
34
+ DispatchQueue.main.async { withExtendedLifetime(object) {} }
73
35
  }
74
36
  }
@@ -28,7 +28,6 @@
28
28
  }
29
29
  },
30
30
  "pluginKeys": {
31
- "bindings": "paths",
32
31
  "class": "string",
33
32
  "plist": "path",
34
33
  "resources": "paths",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/platform-ios-arm64",
3
- "version": "0.1.1-beta.127",
3
+ "version": "0.1.1-beta.128",
4
4
  "description": "The tokamak platform pack for ios-arm64",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- }