@tokamakdev/platform-macos-x64 0.1.1-beta.128 → 0.1.1-beta.130

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.
@@ -11,8 +11,71 @@ fi
11
11
  if [[ $# -eq 1 && $1 == certs ]]; then
12
12
  exec "$apple_tool" inventory
13
13
  fi
14
+
15
+ # Describes the methods PLUGIN/apple/Exports.swift exports on each PLATFORM
16
+ # into OUTPUT/description.json, with the dispatch that serves them,
17
+ # OUTPUT/Exports.generated.swift. The compiler's symbol graph of the plugin,
18
+ # built with the shell for each platform, gives the methods' types.
19
+ describe() {
20
+ local plugin=$1 output=$2
21
+ shift 2
22
+ if ! xcrun --sdk iphonesimulator --show-sdk-path > /dev/null 2>&1; then
23
+ echo "Describing an Apple plugin needs Xcode" >&2
24
+ exit 69
25
+ fi
26
+ # Global, for the exit trap.
27
+ temporary=$(mktemp -d "${TMPDIR:-/tmp}/tokamak-describe.XXXXXX")
28
+ trap 'rm -rf "$temporary"' EXIT
29
+ printf '%s\n' 'func tokamakPlugins(app: TokamakApp) -> [any TokamakPlugin] { [] }' > "$temporary/registry.swift"
30
+ local sources=("$pack/native-shell"/*.swift "$temporary/registry.swift") source
31
+ while IFS= read -r -d '' source; do
32
+ [[ ${source##*/} == Exports.generated.swift ]] || sources+=("$source")
33
+ done < <(find "$plugin/apple" -name '*.swift' -print0 | sort -z)
34
+ local graphs=() compiles=() platform sdk triple
35
+ for platform in "$@"; do
36
+ case "$platform" in
37
+ ios)
38
+ sdk=iphonesimulator
39
+ triple=$(uname -m)-apple-ios$(<"$pack/build/ios-deployment-target")-simulator
40
+ ;;
41
+ macos)
42
+ sdk=macosx
43
+ triple=$(uname -m)-apple-macos$(<"$pack/build/macos-deployment-target")
44
+ ;;
45
+ *)
46
+ echo "Apple plugins run on ios and macos, not $platform" >&2
47
+ exit 2
48
+ ;;
49
+ esac
50
+ mkdir "$temporary/$platform"
51
+ # The platforms compile at once, each into its own log.
52
+ xcrun --sdk "$sdk" swiftc -emit-module -module-name TokamakDescribed -swift-version 5 \
53
+ -target "$triple" -F "$pack/frameworks" -module-cache-path "$temporary/cache" \
54
+ -emit-module-path "$temporary/$platform/TokamakDescribed.swiftmodule" \
55
+ -emit-symbol-graph -emit-symbol-graph-dir "$temporary/$platform" \
56
+ -symbol-graph-minimum-access-level private "${sources[@]}" > "$temporary/$platform.log" 2>&1 &
57
+ compiles+=("$!")
58
+ graphs+=("$platform=$temporary/$platform/TokamakDescribed.symbols.json")
59
+ done
60
+ # The platforms report the same diagnostics, so only the first failure's
61
+ # show, or else the first platform's warnings.
62
+ local platforms=("$@") index failed=
63
+ for index in "${!compiles[@]}"; do
64
+ if ! wait "${compiles[index]}" && [[ -z $failed ]]; then
65
+ failed=$index
66
+ fi
67
+ done
68
+ cat "$temporary/${platforms[${failed:-0}]}.log" >&2
69
+ [[ -z $failed ]] || exit 1
70
+ "$pack/tools/tokamak-swift-describe" "$plugin/apple/Exports.swift" "$output" "${graphs[@]}"
71
+ }
72
+ if [[ $# -ge 4 && $1 == describe ]]; then
73
+ describe "${@:2}"
74
+ exit
75
+ fi
76
+
14
77
  if [[ $# -ne 3 || $1 != build ]]; then
15
- echo "usage: entrypoint build INPUT OUTPUT | entrypoint certs" >&2
78
+ echo "usage: entrypoint build INPUT OUTPUT | entrypoint describe PLUGIN OUTPUT PLATFORM... | entrypoint certs" >&2
16
79
  exit 2
17
80
  fi
18
81
 
@@ -169,10 +232,10 @@ if [[ ! -f "$pack/native-shell/TokamakShell.swift" ]]; then
169
232
  fi
170
233
 
171
234
  # A plugin's class is a Swift type name, which the registry creates, and its
172
- # sources are Swift.
235
+ # sources are the Swift files in its native directory.
173
236
  swift_type='^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$'
174
237
  # Swift requires unique file names in a module, so a plugin's source
175
- # <name> compiles as <plugin>-<name>.
238
+ # <directory>/<name> compiles as <plugin>-<directory>-<name>.
176
239
  plugin_sources=$temporary/plugin-sources
177
240
  mkdir "$plugin_sources"
178
241
  registry=$temporary/TokamakPluginRegistry.swift
@@ -185,19 +248,17 @@ for plugin in "$input/plugins"/*/; do
185
248
  exit 1
186
249
  fi
187
250
  printf ' %s(host: TokamakHost(app: app, plugin: "%s")),\n' "$class" "$plugin_id" >> "$registry"
188
- for source in "$plugin/sources"/*; do
189
- case "$source" in
190
- *.swift)
191
- named=$plugin_sources/$plugin_id-${source##*/}
192
- cp "$source" "$named"
193
- swift_sources+=("$named")
194
- ;;
195
- *)
196
- echo "Apple plugin source must be Swift: $source" >&2
197
- exit 1
198
- ;;
199
- esac
200
- done
251
+ [[ -d "$plugin/native" ]] || continue
252
+ while IFS= read -r -d '' source; do
253
+ relative=${source#"$plugin/native/"}
254
+ named=$plugin_sources/$plugin_id-${relative//\//-}
255
+ if [[ -e "$named" ]]; then
256
+ echo "plugin '$plugin_id' source $relative stages as ${named##*/}, as another source does; rename one" >&2
257
+ exit 1
258
+ fi
259
+ cp "$source" "$named"
260
+ swift_sources+=("$named")
261
+ done < <(find "$plugin/native" -name '*.swift' -print0 | sort -z)
201
262
  done
202
263
  printf '%s\n' ' ]' '}' >> "$registry"
203
264
 
@@ -0,0 +1,561 @@
1
+ import Foundation
2
+
3
+ /// A plugin whose `apple/Exports.swift` lists the methods the Worker calls
4
+ /// and the events it receives. `tok plugin generate apple` reads it and writes
5
+ /// the plugin's dispatch and TypeScript API.
6
+ protocol TokamakExports: TokamakPlugin {
7
+ /// The plugin's host, whose resources hold the native references methods take.
8
+ var host: TokamakHost { get }
9
+
10
+ /// Lists the exported methods and events. The app never calls it.
11
+ static func exports(_ e: inout TokamakExporter<Self>)
12
+
13
+ /// `event` as the JSON data of the event `name`, which the registration lists.
14
+ func eventPayload(_ name: String, _ event: Any) throws(TokamakPluginError) -> Data
15
+
16
+ /// The listeners' JSON `reply` to the event `name`, which the registration
17
+ /// lists with a reply, as its type, or nil for null.
18
+ func eventReply(_ name: String, _ reply: Data) throws(TokamakPluginError) -> Any?
19
+ }
20
+
21
+ extension TokamakExports {
22
+ func eventPayload(_ name: String, _ event: Any) throws(TokamakPluginError) -> Data {
23
+ throw .unlistedEvent(name)
24
+ }
25
+
26
+ func eventReply(_ name: String, _ reply: Data) throws(TokamakPluginError) -> Any? {
27
+ throw .unlistedReply(name)
28
+ }
29
+ }
30
+
31
+ /// What a stream does with values a slow reader has not taken.
32
+ enum TokamakOverflow {
33
+ /// Ends the stream with `QuotaExceededError`.
34
+ case error
35
+ /// Drops the oldest values, for samples where only recent ones matter.
36
+ case dropOldest
37
+ }
38
+
39
+ /// Lists a plugin's exported methods and events: method parameters and event
40
+ /// replies are `Decodable`, method results and events `Encodable`, and options
41
+ /// literal values.
42
+ struct TokamakExporter<Plugin> {
43
+ /// Lists the event `name`, whose listeners receive an `E`.
44
+ mutating func event<E: Encodable>(_ name: String, _ type: E.Type, doc: String = "") {}
45
+
46
+ /// Lists the event `name`, whose listeners receive an `E` and may reply with an `R`.
47
+ mutating func event<E: Encodable, R: Decodable>(
48
+ _ name: String, _ type: E.Type, reply: R.Type, doc: String = ""
49
+ ) {}
50
+
51
+ /// Exports `method`, which returns once.
52
+ mutating func call<each A: Decodable, R: Encodable>(
53
+ _ method: (Plugin) -> (repeat each A) async throws -> R, doc: String = ""
54
+ ) {}
55
+
56
+ mutating func call<each A: Decodable>(
57
+ _ method: (Plugin) -> (repeat each A) async throws -> Void, doc: String = ""
58
+ ) {}
59
+
60
+ /// Exports `method`, whose last parameter reads the bytes the Worker streams.
61
+ mutating func call<each A: Decodable, R: Encodable>(
62
+ _ method: (Plugin) -> (repeat each A, TokamakPluginBody) async throws -> R, doc: String = ""
63
+ ) {}
64
+
65
+ mutating func call<each A: Decodable>(
66
+ _ method: (Plugin) -> (repeat each A, TokamakPluginBody) async throws -> Void, doc: String = ""
67
+ ) {}
68
+
69
+ /// Exports `method`, which returns the values it sends until the Worker cancels.
70
+ mutating func stream<each A: Decodable, S: AsyncSequence>(
71
+ _ method: (Plugin) -> (repeat each A) throws -> S, overflow: TokamakOverflow = .error,
72
+ doc: String = ""
73
+ ) where S.Element: Encodable {}
74
+
75
+ /// Exports `method`, which returns a native object the Worker holds as a
76
+ /// `NativeReference`.
77
+ mutating func resource<each A: Decodable, R>(
78
+ _ method: (Plugin) -> (repeat each A) async throws -> R, doc: String = ""
79
+ ) {}
80
+ }
81
+
82
+ /// A native object the Worker passes as a `NativeReference`, which another
83
+ /// plugin retained.
84
+ struct TokamakReference<Object>: Decodable {
85
+ let object: Object
86
+
87
+ init(from decoder: any Decoder) throws {
88
+ let id = try decoder.singleValueContainer().decode(String.self)
89
+ guard let resources = decoder.userInfo[.tokamakResources] as? TokamakResources else {
90
+ throw TokamakPluginError(name: "InvalidStateError", message: "No native references here")
91
+ }
92
+ object = try resources.get(id, as: Object.self)
93
+ }
94
+ }
95
+
96
+ /// Any JSON value, for data a plugin passes through or reads itself.
97
+ enum TokamakJSON: Codable, Equatable {
98
+ case null
99
+ case bool(Bool)
100
+ case number(Double)
101
+ case string(String)
102
+ case array([TokamakJSON])
103
+ case object([String: TokamakJSON])
104
+
105
+ init(from decoder: any Decoder) throws {
106
+ // Each failed attempt throws, so the commonest kinds of value come first.
107
+ let container = try decoder.singleValueContainer()
108
+ if container.decodeNil() {
109
+ self = .null
110
+ } else if let string = try? container.decode(String.self) {
111
+ self = .string(string)
112
+ } else if let number = try? container.decode(Double.self) {
113
+ self = .number(number)
114
+ } else if let bool = try? container.decode(Bool.self) {
115
+ self = .bool(bool)
116
+ } else if let object = try? container.decode([String: TokamakJSON].self) {
117
+ self = .object(object)
118
+ } else {
119
+ self = .array(try container.decode([TokamakJSON].self))
120
+ }
121
+ }
122
+
123
+ func encode(to encoder: any Encoder) throws {
124
+ var container = encoder.singleValueContainer()
125
+ switch self {
126
+ case .null: try container.encodeNil()
127
+ case .bool(let bool): try container.encode(bool)
128
+ case .number(let number): try container.encode(number)
129
+ case .string(let string): try container.encode(string)
130
+ case .array(let array): try container.encode(array)
131
+ case .object(let object): try container.encode(object)
132
+ }
133
+ }
134
+
135
+ /// `value` as JSON, which `JSONSerialization` writes it as, or nil when it is not JSON.
136
+ init?(_ value: Any) {
137
+ switch value {
138
+ case is NSNull: self = .null
139
+ case let number as NSNumber where CFGetTypeID(number) == CFBooleanGetTypeID():
140
+ self = .bool(number.boolValue)
141
+ case let number as NSNumber: self = .number(number.doubleValue)
142
+ case let string as String: self = .string(string)
143
+ case let array as [Any]:
144
+ let elements = array.compactMap(TokamakJSON.init)
145
+ guard elements.count == array.count else { return nil }
146
+ self = .array(elements)
147
+ case let object as [String: Any]:
148
+ let entries = object.compactMapValues(TokamakJSON.init)
149
+ guard entries.count == object.count else { return nil }
150
+ self = .object(entries)
151
+ default: return nil
152
+ }
153
+ }
154
+
155
+ /// The value as `JSONSerialization` reads it: `NSNull`, an `NSNumber` for a boolean or a
156
+ /// number, which `as? Int` reads when whole, `String`, arrays and dictionaries.
157
+ var value: Any {
158
+ switch self {
159
+ case .null: NSNull()
160
+ case .bool(let bool): NSNumber(value: bool)
161
+ case .number(let number): NSNumber(value: number)
162
+ case .string(let string): string
163
+ case .array(let array): array.map(\.value)
164
+ case .object(let object): object.mapValues(\.value)
165
+ }
166
+ }
167
+ }
168
+
169
+ /// An enum whose cases are the forms its values take: a case without an associated value is
170
+ /// the string of its name, and a case with one unlabeled associated value is that value.
171
+ /// `tok plugin generate apple` writes its coding in place of these defaults, which throw.
172
+ protocol TokamakUnion: Codable {}
173
+
174
+ /// A `TokamakUnion` whose values are objects that name their case in the field `tag`, with a
175
+ /// struct case's fields.
176
+ protocol TokamakTaggedUnion: TokamakUnion {
177
+ /// The tag field's name, as a string literal, which `tok plugin generate apple` reads.
178
+ static var tag: String { get }
179
+ }
180
+
181
+ extension TokamakUnion {
182
+ init(from decoder: any Decoder) throws {
183
+ throw TokamakPluginError.ungenerated(Self.self)
184
+ }
185
+
186
+ func encode(to encoder: any Encoder) throws {
187
+ throw TokamakPluginError.ungenerated(Self.self)
188
+ }
189
+ }
190
+
191
+ /// The JSON value whose kind a union's generated decoder chooses its case by.
192
+ struct TokamakUnionValue {
193
+ enum Kind {
194
+ case null, boolean, number, array
195
+ case string(String)
196
+ /// An object, with its keys.
197
+ case object(Set<String>)
198
+ }
199
+
200
+ let kind: Kind
201
+ private let decoder: any Decoder
202
+
203
+ init(_ decoder: any Decoder) throws {
204
+ self.decoder = decoder
205
+ let value = try decoder.singleValueContainer()
206
+ if value.decodeNil() {
207
+ kind = .null
208
+ } else if let string = try? value.decode(String.self) {
209
+ kind = .string(string)
210
+ } else if (try? value.decode(Bool.self)) != nil {
211
+ kind = .boolean
212
+ } else if (try? value.decode(Double.self)) != nil {
213
+ kind = .number
214
+ } else if let object = try? decoder.container(keyedBy: TokamakUnionKey.self) {
215
+ kind = .object(Set(object.allKeys.map(\.stringValue)))
216
+ } else {
217
+ kind = .array
218
+ }
219
+ }
220
+
221
+ /// The value as a `T`.
222
+ func decode<T: Decodable>(_ type: T.Type) throws -> T {
223
+ try decoder.singleValueContainer().decode(type)
224
+ }
225
+
226
+ /// The string in the object's field `tag`.
227
+ func tag(_ tag: String) throws -> String {
228
+ try decoder.container(keyedBy: TokamakUnionKey.self).decode(String.self, forKey: .init(tag))
229
+ }
230
+
231
+ /// The error of a value that is none of the union `name`'s forms.
232
+ func mismatch(_ name: String) -> DecodingError {
233
+ .dataCorrupted(
234
+ .init(codingPath: decoder.codingPath, debugDescription: "must be one of \(name)'s forms"))
235
+ }
236
+ }
237
+
238
+ /// Encodes the case `name` of a tagged union as an object with `value`'s fields, if any, and the
239
+ /// field `tag` naming the case.
240
+ func tokamakEncode(
241
+ tag: String, _ name: String, _ value: (any Encodable)? = nil, to encoder: any Encoder
242
+ ) throws {
243
+ try value?.encode(to: encoder)
244
+ var container = encoder.container(keyedBy: TokamakUnionKey.self)
245
+ try container.encode(name, forKey: .init(tag))
246
+ }
247
+
248
+ /// A JSON object's key, as a union's coding reads and writes it.
249
+ struct TokamakUnionKey: CodingKey {
250
+ let stringValue: String
251
+ var intValue: Int? { nil }
252
+
253
+ init(_ stringValue: String) {
254
+ self.stringValue = stringValue
255
+ }
256
+
257
+ init?(stringValue: String) {
258
+ self.stringValue = stringValue
259
+ }
260
+
261
+ init?(intValue: Int) {
262
+ nil
263
+ }
264
+ }
265
+
266
+ extension CodingUserInfoKey {
267
+ /// The resources a `TokamakReference` finds its object in.
268
+ fileprivate static let tokamakResources = CodingUserInfoKey(rawValue: "tokamak.resources")!
269
+ }
270
+
271
+ extension TokamakPluginError {
272
+ /// A `TypeError` naming where the `root` value, such as a call's
273
+ /// arguments, fails to decode.
274
+ init(decoding error: DecodingError, in root: String = "arguments") {
275
+ let (path, problem): ([any CodingKey], String) =
276
+ switch error {
277
+ case .keyNotFound(let key, let context): (context.codingPath + [key], "is missing")
278
+ case .typeMismatch(_, let context), .valueNotFound(_, let context),
279
+ .dataCorrupted(let context):
280
+ (context.codingPath, context.debugDescription)
281
+ @unknown default: ([], "\(error)")
282
+ }
283
+ let location = path.reduce("") { location, key in
284
+ if let index = key.intValue { return "\(location)[\(index)]" }
285
+ return location.isEmpty ? key.stringValue : "\(location).\(key.stringValue)"
286
+ }
287
+ self.init(name: "TypeError", message: "\(location.isEmpty ? root : location) \(problem)")
288
+ }
289
+
290
+ /// A `TokamakUnion` without the coding `tok plugin generate apple` writes.
291
+ static func ungenerated(_ union: Any.Type) -> Self {
292
+ Self(name: "TypeError", message: "run tok plugin generate apple to code \(union)")
293
+ }
294
+
295
+ /// The plugin emitted an event its registration does not list.
296
+ static func unlistedEvent(_ name: String) -> Self {
297
+ Self(name: "TypeError", message: "apple/Exports.swift lists no event \(name)")
298
+ }
299
+
300
+ /// The plugin asked for the reply to an event its registration lists without one.
301
+ static func unlistedReply(_ name: String) -> Self {
302
+ Self(name: "TypeError", message: "apple/Exports.swift lists no reply to event \(name)")
303
+ }
304
+ }
305
+
306
+ /// Decodes a call's JSON arguments, or another `root` value, finding the
307
+ /// native references they name in `resources`.
308
+ func tokamakDecode<T: Decodable>(
309
+ _ type: T.Type, from payload: Data, resources: TokamakResources? = nil,
310
+ in root: String = "arguments"
311
+ ) throws(TokamakPluginError) -> T {
312
+ let decoder = JSONDecoder()
313
+ decoder.userInfo[.tokamakResources] = resources
314
+ do {
315
+ return try decoder.decode(type, from: payload)
316
+ } catch let error as DecodingError {
317
+ throw TokamakPluginError(decoding: error, in: root)
318
+ } catch {
319
+ throw TokamakPluginError(error)
320
+ }
321
+ }
322
+
323
+ /// `value` as a reply's payload: `Data` as it is, and any other value as the
324
+ /// JSON `encoder` writes.
325
+ func tokamakEncode<T: Encodable>(
326
+ _ value: T, with encoder: JSONEncoder = JSONEncoder()
327
+ ) throws(TokamakPluginError) -> Data {
328
+ if let data = value as? Data { return data }
329
+ do {
330
+ return try encoder.encode(value)
331
+ } catch {
332
+ throw TokamakPluginError(name: "DataCloneError", message: "\(error)")
333
+ }
334
+ }
335
+
336
+ /// `event` as the JSON data of the event `name`, whose listeners receive a `T`.
337
+ func tokamakEvent<T: Encodable>(
338
+ _ name: String, _ event: Any, as type: T.Type
339
+ ) throws(TokamakPluginError) -> Data {
340
+ guard let event = event as? T else {
341
+ throw TokamakPluginError(
342
+ name: "TypeError", message: "event \(name) takes \(T.self), not \(Swift.type(of: event))")
343
+ }
344
+ return try tokamakEncode(event)
345
+ }
346
+
347
+ /// Replies with what `method` returns. A synchronous method runs on the
348
+ /// call's executor and has nothing to cancel.
349
+ func tokamakCall<T: Encodable>(
350
+ _ reply: TokamakPluginBytesReply, _ method: () throws -> T
351
+ ) -> (() -> Void)? {
352
+ reply(result { try tokamakEncode(try method()) })
353
+ return nil
354
+ }
355
+
356
+ func tokamakCall(_ reply: TokamakPluginBytesReply, _ method: () throws -> Void) -> (() -> Void)? {
357
+ reply(
358
+ result {
359
+ try method()
360
+ return null
361
+ })
362
+ return nil
363
+ }
364
+
365
+ /// Runs `method` in a task, replying with what it returns. Cancelling the
366
+ /// call cancels the task.
367
+ func tokamakCall<T: Encodable>(
368
+ _ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> T
369
+ ) -> (() -> Void)? {
370
+ task { reply(await awaitResult { try tokamakEncode(try await method()) }) }
371
+ }
372
+
373
+ func tokamakCall(
374
+ _ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> Void
375
+ ) -> (() -> Void)? {
376
+ task {
377
+ reply(
378
+ await awaitResult {
379
+ try await method()
380
+ return null
381
+ })
382
+ }
383
+ }
384
+
385
+ /// Replies with each value `method`'s sequence sends, until it ends or the
386
+ /// subscription is cancelled. A synchronous method runs on the
387
+ /// subscription's executor.
388
+ func tokamakStream<S: AsyncSequence>(
389
+ _ reply: @escaping TokamakPluginBytesReply, _ method: () throws -> S
390
+ ) throws(TokamakPluginError) -> () -> Void where S.Element: Encodable {
391
+ let values = try catching(method)
392
+ return task { await send(values, reply) }
393
+ }
394
+
395
+ func tokamakStream<S: AsyncSequence>(
396
+ _ reply: @escaping TokamakPluginBytesReply, _ method: @escaping () async throws -> S
397
+ ) -> () -> Void where S.Element: Encodable {
398
+ task {
399
+ do {
400
+ await send(try await method(), reply)
401
+ } catch {
402
+ reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
403
+ }
404
+ }
405
+ }
406
+
407
+ /// Retains the object `method` returns in `resources`, replying with its
408
+ /// ID, until the subscription is cancelled. A synchronous method runs on the
409
+ /// subscription's executor.
410
+ func tokamakResource(
411
+ _ resources: TokamakResources, _ reply: @escaping TokamakPluginBytesReply,
412
+ _ method: () throws -> Any
413
+ ) throws(TokamakPluginError) -> () -> Void {
414
+ resources.retain(try catching(method)) {
415
+ reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: false))
416
+ }
417
+ }
418
+
419
+ func tokamakResource(
420
+ _ resources: TokamakResources, _ reply: @escaping TokamakPluginBytesReply,
421
+ _ method: @escaping () async throws -> Any
422
+ ) -> () -> Void {
423
+ let retained = Retained()
424
+ let cancel = task {
425
+ do {
426
+ let object = try await method()
427
+ retained.hold(
428
+ resources.retain(object) {
429
+ reply(TokamakPluginResult($0.flatMap(encodePluginValue), done: false))
430
+ })
431
+ } catch {
432
+ reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
433
+ }
434
+ }
435
+ return {
436
+ cancel()
437
+ retained.end()
438
+ }
439
+ }
440
+
441
+ /// How many values listeners keep: those a slow reader has not taken, or those
442
+ /// held for the first.
443
+ private let listenerCapacity = 64
444
+
445
+ /// The streams of an event's listeners, such as a page's, each of which
446
+ /// receives every value `send` delivers until it ends, keeping the newest 64
447
+ /// that a slow reader has not taken. When `holding`, the newest 64 values sent
448
+ /// while none listen wait for the first listener.
449
+ final class TokamakListeners<Value>: @unchecked Sendable {
450
+ private let lock = NSLock()
451
+ private let holding: Bool
452
+ private var continuations: [UUID: AsyncStream<Value>.Continuation] = [:]
453
+ private var held: [Value] = []
454
+
455
+ init(holding: Bool = false) {
456
+ self.holding = holding
457
+ }
458
+
459
+ /// A new listener's stream.
460
+ // Values are yielded under the lock, so one sent while a listener starts
461
+ // follows the held ones. Yielding only buffers.
462
+ func stream() -> AsyncStream<Value> {
463
+ let (stream, continuation) = AsyncStream.makeStream(
464
+ of: Value.self, bufferingPolicy: .bufferingNewest(listenerCapacity))
465
+ let key = UUID()
466
+ continuation.onTermination = { [weak self] _ in
467
+ self?.lock.withLock { _ = self?.continuations.removeValue(forKey: key) }
468
+ }
469
+ lock.withLock {
470
+ continuations[key] = continuation
471
+ for value in held {
472
+ continuation.yield(value)
473
+ }
474
+ held = []
475
+ }
476
+ return stream
477
+ }
478
+
479
+ func send(_ value: Value) {
480
+ lock.withLock {
481
+ if holding && continuations.isEmpty {
482
+ held.append(value)
483
+ if held.count > listenerCapacity { held.removeFirst() }
484
+ }
485
+ for continuation in continuations.values {
486
+ continuation.yield(value)
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ /// The JSON payload of a method that returns nothing.
493
+ private let null = Data("null".utf8)
494
+
495
+ /// A call's last reply: `payload`'s bytes, or its failure.
496
+ private func result(_ payload: () throws -> Data) -> TokamakPluginResult {
497
+ TokamakPluginResult(Result { () throws(TokamakPluginError) in try catching(payload) }, done: true)
498
+ }
499
+
500
+ private func awaitResult(_ payload: () async throws -> Data) async -> TokamakPluginResult {
501
+ do {
502
+ return TokamakPluginResult(.success(try await payload()), done: true)
503
+ } catch {
504
+ return TokamakPluginResult(.failure(TokamakPluginError(error)), done: true)
505
+ }
506
+ }
507
+
508
+ /// What `body` returns, with what it throws as a `TokamakPluginError`.
509
+ private func catching<T>(_ body: () throws -> T) throws(TokamakPluginError) -> T {
510
+ do {
511
+ return try body()
512
+ } catch {
513
+ throw TokamakPluginError(error)
514
+ }
515
+ }
516
+
517
+ /// Runs `body` in a task, returning its cancellation.
518
+ private func task(_ body: @escaping () async -> Void) -> () -> Void {
519
+ Task { await body() }.cancel
520
+ }
521
+
522
+ /// Replies with each of `values`, then with the end of the subscription.
523
+ private func send<S: AsyncSequence>(
524
+ _ values: S, _ reply: TokamakPluginBytesReply
525
+ ) async where S.Element: Encodable {
526
+ // One encoder serves the stream, whose values it encodes one at a time.
527
+ let encoder = JSONEncoder()
528
+ do {
529
+ for try await value in values {
530
+ reply(TokamakPluginResult(.success(try tokamakEncode(value, with: encoder)), done: false))
531
+ }
532
+ reply(TokamakPluginResult(payload: Data(), failed: false, done: true))
533
+ } catch {
534
+ reply(TokamakPluginResult(.failure(TokamakPluginError(error)), done: true))
535
+ }
536
+ }
537
+
538
+ /// The release of an object a subscription retains, which runs once the
539
+ /// subscription ends, whether the object is retained before or after.
540
+ private final class Retained {
541
+ private let lock = NSLock()
542
+ private var release: (() -> Void)?
543
+ private var ended = false
544
+
545
+ func hold(_ release: @escaping () -> Void) {
546
+ let ended = lock.withLock {
547
+ if !self.ended { self.release = release }
548
+ return self.ended
549
+ }
550
+ if ended { release() }
551
+ }
552
+
553
+ func end() {
554
+ let release = lock.withLock {
555
+ ended = true
556
+ defer { self.release = nil }
557
+ return self.release
558
+ }
559
+ release?()
560
+ }
561
+ }
@@ -140,7 +140,7 @@ final class TokamakHost {
140
140
  #endif
141
141
 
142
142
  /// Delivers the plugin's event `name`, which the Worker's listeners
143
- /// receive as `<plugin>.<name>`, with its encoded `payload`, and calls
143
+ /// receive as `<plugin>.apple.<name>`, with its encoded `payload`, and calls
144
144
  /// `completion` on the main thread with their encoded reply, which is empty
145
145
  /// when none replied. Fails unless they return within `timeout`, which
146
146
  /// includes runtime startup. Call it on the main thread.
@@ -150,33 +150,75 @@ final class TokamakHost {
150
150
  timeout: TimeInterval,
151
151
  completion: @escaping (Result<Data, Error>) -> Void
152
152
  ) {
153
- app.emit("\(plugin).\(name)", payload: payload, timeout: timeout, completion: completion)
153
+ app.emit("\(plugin).apple.\(name)", payload: payload, timeout: timeout, completion: completion)
154
154
  }
155
155
 
156
- /// `emitBytes` with `event` encoded as JSON, calling `completion` with the
157
- /// reply parsed from JSON, or nil when none replied.
156
+ /// Delivers `event` to the Worker's listeners of the plugin's event `name`,
157
+ /// which `apple/Exports.swift` lists, then calls `completion` on the main
158
+ /// thread. Fails unless they return within `timeout`, which includes runtime
159
+ /// startup. Call it on the main thread.
158
160
  func emit(
159
161
  _ name: String,
160
- event: Any,
162
+ event: some Encodable,
161
163
  timeout: TimeInterval,
162
- completion: @escaping (Result<Any?, Error>) -> Void
164
+ completion: @escaping (Result<Void, Error>) -> Void
163
165
  ) {
164
- let payload: Data
165
- switch encodePluginValue(event) {
166
- case .success(let encoded): payload = encoded
167
- case .failure(let error): return completion(.failure(error))
168
- }
169
- emitBytes(name, payload: payload, timeout: timeout) { result in
166
+ send(name, event, timeout: timeout) { completion($0.map { _ in }) }
167
+ }
168
+
169
+ /// `emit` of an event whose listeners may reply, calling `completion` with
170
+ /// the reply as the plugin's generated dispatch decodes it, which is a
171
+ /// `Reply`, or nil when none replied.
172
+ func emit<Reply>(
173
+ _ name: String,
174
+ event: some Encodable,
175
+ reply: Reply.Type,
176
+ timeout: TimeInterval,
177
+ completion: @escaping (Result<Reply?, Error>) -> Void
178
+ ) {
179
+ send(name, event, timeout: timeout) { result in
170
180
  completion(
171
181
  result.flatMap { reply in
172
- Result { () throws(TokamakPluginError) -> Any? in
173
- let value = reply.isEmpty ? NSNull() : try decodePluginValue(reply)
174
- return value is NSNull ? nil : value
175
- }.mapError { $0 }
182
+ Result<Reply?, Error> { try self.decoded(reply, to: name, as: Reply.self) }
176
183
  })
177
184
  }
178
185
  }
179
186
 
187
+ /// Delivers `event` as the plugin's generated dispatch encodes it.
188
+ private func send(
189
+ _ name: String, _ event: Any, timeout: TimeInterval,
190
+ completion: @escaping (Result<Data, Error>) -> Void
191
+ ) {
192
+ let payload: Data
193
+ do throws(TokamakPluginError) {
194
+ payload = try exports(name).eventPayload(name, event)
195
+ } catch {
196
+ return completion(.failure(error))
197
+ }
198
+ emitBytes(name, payload: payload, timeout: timeout, completion: completion)
199
+ }
200
+
201
+ /// The listeners' `reply` to the event `name` as a `Reply`, or nil when none replied.
202
+ private func decoded<Reply>(
203
+ _ reply: Data, to name: String, as type: Reply.Type
204
+ ) throws(TokamakPluginError) -> Reply? {
205
+ guard !reply.isEmpty, let value = try exports(name).eventReply(name, reply) else { return nil }
206
+ guard let reply = value as? Reply else {
207
+ throw TokamakPluginError(
208
+ name: "TypeError",
209
+ message: "event \(name) replies with \(Swift.type(of: value)), not \(Reply.self)")
210
+ }
211
+ return reply
212
+ }
213
+
214
+ /// The plugin, whose generated dispatch encodes the event `name`.
215
+ private func exports(_ name: String) throws(TokamakPluginError) -> any TokamakExports {
216
+ guard let exports = app.plugins[plugin] as? any TokamakExports else {
217
+ throw .unlistedEvent(name)
218
+ }
219
+ return exports
220
+ }
221
+
180
222
  /// Fetches what the app serves at `path`, as its WebView would load it: a
181
223
  /// file the app ships or the Worker's response. Calls `completion` on the
182
224
  /// main thread with the body. Fails unless it arrives within `timeout`,
@@ -22,7 +22,6 @@
22
22
  "pluginKeys": {
23
23
  "class": "string",
24
24
  "plist": "path",
25
- "resources": "paths",
26
- "sources": "paths"
25
+ "resources": "paths"
27
26
  }
28
27
  }
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/macos-x64/tools/tokamak-swift-describe
3
+ size: 34569672 bytes
4
+ sha256: 98f0a79174ac752c21249692eb8143c05c88a9edbef0181d0465aa34a728319d
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/platform-macos-x64",
3
- "version": "0.1.1-beta.128",
3
+ "version": "0.1.1-beta.130",
4
4
  "description": "The tokamak platform pack for macos-x64",
5
5
  "repository": {
6
6
  "type": "git",