@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.1

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.
Files changed (86) hide show
  1. package/README.md +216 -4
  2. package/TileflowNativeAdmission.podspec +34 -0
  3. package/android/build.gradle +43 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
  6. package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
  7. package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
  8. package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
  9. package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
  10. package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
  11. package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
  12. package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
  13. package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
  14. package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
  15. package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
  16. package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
  17. package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
  18. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
  19. package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
  20. package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
  21. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +236 -0
  22. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
  23. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
  24. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +82 -0
  25. package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
  26. package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
  27. package/dist/contract-DWs-lDPo.d.ts +144 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +6989 -0
  30. package/dist/internal/camera.d.ts +65 -0
  31. package/dist/internal/camera.js +325 -0
  32. package/dist/internal/hosted-binding.d.ts +18 -0
  33. package/dist/internal/hosted-binding.js +277 -0
  34. package/dist/internal/initial-view.d.ts +11 -0
  35. package/dist/internal/initial-view.js +8 -0
  36. package/dist/internal/interactions.d.ts +76 -0
  37. package/dist/internal/interactions.js +794 -0
  38. package/dist/internal/native-admission-bridge.d.ts +29 -0
  39. package/dist/internal/native-admission-bridge.js +351 -0
  40. package/dist/internal/native-admission.d.ts +78 -0
  41. package/dist/internal/native-admission.js +1614 -0
  42. package/dist/internal/native-appearance.d.ts +21 -0
  43. package/dist/internal/native-appearance.js +110 -0
  44. package/dist/internal/native-configuration-bridge.d.ts +9 -0
  45. package/dist/internal/native-configuration-bridge.js +343 -0
  46. package/dist/internal/session.d.ts +125 -0
  47. package/dist/internal/session.js +887 -0
  48. package/dist/internal/source-state.d.ts +11 -0
  49. package/dist/internal/source-state.js +26 -0
  50. package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
  51. package/docs/native-admission.md +108 -0
  52. package/docs/native-configuration.md +210 -0
  53. package/docs/native-interactions.md +161 -0
  54. package/docs/native-location.md +148 -0
  55. package/ios/TFAdmissionBootstrap.h +17 -0
  56. package/ios/TFAdmissionBootstrap.mm +168 -0
  57. package/ios/TFAdmissionCatalog.h +13 -0
  58. package/ios/TFAdmissionCatalog.mm +193 -0
  59. package/ios/TFAdmissionDocumentScope.h +7 -0
  60. package/ios/TFAdmissionDocumentScope.mm +45 -0
  61. package/ios/TFAdmissionEngine.h +57 -0
  62. package/ios/TFAdmissionEngine.mm +371 -0
  63. package/ios/TFAdmissionInstallation.h +27 -0
  64. package/ios/TFAdmissionInstallation.mm +117 -0
  65. package/ios/TFAdmissionNetwork.h +16 -0
  66. package/ios/TFAdmissionNetwork.mm +268 -0
  67. package/ios/TFAdmissionURL.mm +116 -0
  68. package/ios/TFDocumentBounds.h +9 -0
  69. package/ios/TFDocumentBounds.mm +19 -0
  70. package/ios/TFMobileConfiguration.h +23 -0
  71. package/ios/TFMobileConfiguration.mm +139 -0
  72. package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
  73. package/ios/TFNativeDocumentRegistry.h +28 -0
  74. package/ios/TFNativeDocumentRegistry.mm +193 -0
  75. package/ios/TFNativeDocumentURL.h +6 -0
  76. package/ios/TFNativeSurfaceState.h +28 -0
  77. package/ios/TFNativeSurfaceState.mm +128 -0
  78. package/ios/TileflowNativeAdmission.h +9 -0
  79. package/ios/TileflowNativeAdmission.mm +184 -0
  80. package/ios/TileflowNativeConfiguration.mm +41 -0
  81. package/ios/TileflowNativeDocuments.h +12 -0
  82. package/ios/TileflowNativeDocuments.mm +105 -0
  83. package/ios/TileflowNativeSurface.mm +403 -0
  84. package/ios/tileflow_native_admission.rb +45 -0
  85. package/package.json +51 -7
  86. package/react-native.config.cjs +13 -0
@@ -0,0 +1,148 @@
1
+ # Application-owned foreground location
2
+
3
+ `@tileflow/react-native` does not own device location. A Tileflow `Map` renders normally without a
4
+ location permission, provider, hook or native location module. The application decides whether to
5
+ request foreground permission, which provider to use, when observation starts and stops, what fix is
6
+ displayed, whether recentering is offered, and what status or error UI is appropriate.
7
+
8
+ This guide describes a source-checkout recipe built only from the existing public `Map`, controlled
9
+ camera and annotation contracts. It is not a new Tileflow runtime API and it does not add a location
10
+ provider to the package.
11
+
12
+ ## Boundary
13
+
14
+ The package deliberately does not provide any of the following:
15
+
16
+ - a Tileflow location component, hook, provider or permission API;
17
+ - an iOS or Android location permission declaration;
18
+ - a dependency on Expo Location, a geolocation package or another location provider;
19
+ - a background task, background permission or location-history store;
20
+ - a credential/session field for device location;
21
+ - a location analytics event or precise-coordinate telemetry path;
22
+ - a GeoIP fallback for device location;
23
+ - implicit permission requests or tracking when `Map` mounts.
24
+
25
+ The application must configure any native permission strings, manifest entries and provider setup
26
+ required by the provider it chooses. Those application settings are separate from Tileflow's native
27
+ Hosted credential configuration.
28
+
29
+ ## Source-checkout recipe
30
+
31
+ `../harness/ForegroundLocationExample.tsx` is a compile-only example owned by this package. Its
32
+ provider boundary is injected:
33
+
34
+ ```ts
35
+ export type ApplicationForegroundLocationAdapter = Readonly<{
36
+ requestPermission(): Promise<
37
+ 'granted-precise' | 'granted-approximate' | 'denied' | 'unavailable'
38
+ >;
39
+ observe(listener: (update: ApplicationLocationObservation) => void): () => void;
40
+ }>;
41
+ ```
42
+
43
+ No concrete provider is imported. The adapter is application code: it translates the provider's
44
+ permission and foreground observation APIs into this small recipe-specific shape. The example and
45
+ its state helper live under `harness/`, are absent from package exports, and are excluded from
46
+ packed runtime files.
47
+
48
+ ## Explicit application state
49
+
50
+ The recipe models these states without unmounting the Map:
51
+
52
+ | State | Meaning | Location annotation |
53
+ | --------------------- | ------------------------------------------------------------ | ---------------------------- |
54
+ | `idle` | The application has not requested permission. | none |
55
+ | `requesting` | An explicit application action started a permission request. | none |
56
+ | `granted-precise` | Foreground policy allows precise observation. | one marker after a valid fix |
57
+ | `granted-approximate` | Foreground policy allows approximate observation. | one marker after a valid fix |
58
+ | `denied` | The permission request was denied. | none |
59
+ | `unavailable` | Permission/provider setup failed or emitted invalid data. | none |
60
+ | `revoked` | A previously granted observation reported revocation. | removed immediately |
61
+
62
+ Construction is inert: it neither requests permission nor starts observation. The example's **Use
63
+ my location** control is the only permission-request trigger. A rejected provider promise is reduced
64
+ to the application-level `unavailable` state; raw provider exceptions are not forwarded into Map
65
+ props or Tileflow callbacks.
66
+
67
+ ## Validate before Map props
68
+
69
+ The adapter output is treated as untrusted runtime data even though the TypeScript adapter is typed.
70
+ The recipe accepts only a plain record with exactly `longitude`, `latitude` and `accuracy` data
71
+ properties. Longitude must be finite and within `[-180, 180]`, latitude finite and within
72
+ `[-90, 90]`, and accuracy finite and nonnegative. Accessors, extra provider fields, invalid
73
+ prototypes and non-finite values fail closed.
74
+
75
+ Only a validated fix is converted to an annotation:
76
+
77
+ ```ts
78
+ {
79
+ id: 'application-foreground-location',
80
+ kind: 'marker',
81
+ ariaLabel: 'Current location',
82
+ coordinate: [longitude, latitude],
83
+ data: {accuracyMeters: accuracy, precision: 'precise'},
84
+ }
85
+ ```
86
+
87
+ Approximate permission uses the same stable ID and an `Approximate current location` label. The
88
+ stable ID lets the existing annotation reconciler update one marker host instead of recreating a
89
+ new marker for every fix. Revocation, denial or provider failure removes the annotation by returning
90
+ an empty annotation collection.
91
+
92
+ The marker uses the existing accessible Tileflow annotation wrapper. This recipe does not add a
93
+ location-specific native marker component.
94
+
95
+ ## Foreground observation lifetime
96
+
97
+ Observation starts only when both conditions are true:
98
+
99
+ 1. the application is in the foreground; and
100
+ 2. its current permission state is `granted-precise` or `granted-approximate`.
101
+
102
+ `AppState` is application-side orchestration in the example. Moving to background synchronously
103
+ retires the current observation token and calls the provider cleanup. Returning to foreground
104
+ starts a new observation only if the last application permission policy still allows it. Denied,
105
+ revoked and unavailable states do not restart observation automatically.
106
+
107
+ The React effect owns a replayable `mount()` epoch. Its cleanup immediately retires the current
108
+ observation and any pending permission/provider callback without terminally disposing the
109
+ controller. A StrictMode setup replay may therefore mount the same controller again; if the retained
110
+ settled permission state is granted and the application is foregrounded, observation restarts
111
+ without another permission prompt. If cleanup interrupted an in-flight permission request, that
112
+ request is retired and the last settled application state is restored instead of accepting its late
113
+ result. A real unmount or adapter replacement uses the same cleanup boundary, so late callbacks are
114
+ inert even though no timing heuristic distinguishes them from a replay. The recipe does not persist
115
+ fixes or observation history.
116
+
117
+ `dispose()` remains a separate terminal operation for an owner that will never mount again. The
118
+ React example does not use terminal disposal for ordinary effect cleanup.
119
+
120
+ ## Recenter is explicit
121
+
122
+ A new fix changes only the application-owned annotation. It does **not** change camera state.
123
+
124
+ The example keeps the existing complete controlled `Map` view. An explicit **Recenter** action copies
125
+ the latest validated coordinate into that controlled view while preserving zoom, bearing and pitch.
126
+ The recipe uses no camera animation API or duration, so it introduces no animated movement for
127
+ reduced-motion users. Applications that later add their own animated camera affordance must apply
128
+ their own reduced-motion policy before doing so.
129
+
130
+ Gestures continue to update the same controlled view through `onViewChange`; receiving another
131
+ location fix never overwrites that user-driven camera state.
132
+
133
+ ## Privacy and product scope
134
+
135
+ The sample keeps only the latest in-memory fix needed for its marker. It does not store history,
136
+ send coordinates to analytics, derive a device fix from GeoIP, start background tracking or attach
137
+ location to Tileflow sessions/admission. Status and error presentation are application UI.
138
+
139
+ Location also does not change the interaction presentation boundary. Tileflow annotations and
140
+ semantic targets remain primitives and data; the application still owns any popup, callout,
141
+ tooltip, sheet, panel, modal or other selection presentation.
142
+
143
+ ## Qualification
144
+
145
+ The source files describe the intended application contract; they are not evidence that a provider,
146
+ permission prompt, simulator/device scenario, native build, package publication or Hosted service
147
+ was executed. Typecheck the source-checkout harness and qualify the chosen provider in the
148
+ application that owns its permission and privacy policy.
@@ -0,0 +1,17 @@
1
+ #import "TFAdmissionEngine.h"
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+ // All registry mutations use the admission scheduler. Native networking reads
5
+ // only immutable request data, continuous deadlines, and atomic liveness.
6
+ @interface TFAdmissionBootstrap : NSObject
7
+ - (instancetype)initWithInstallation:(NSString *)installation scheduler:(id<TFAdmissionScheduler>)scheduler
8
+ network:(id<TFAdmissionNetwork>)network owns:(TFAdmissionStartGuard)owns;
9
+ - (void)registerContext:(NSString *)context mapId:(nullable NSString *)mapId;
10
+ - (void)startContext:(NSString *)context request:(NSString *)request url:(NSString *)url
11
+ credential:(NSString *)credential body:(NSString *)body completion:(void (^)(NSDictionary *_Nullable reply))completion;
12
+ - (void)cancelContext:(NSString *)context request:(NSString *)request;
13
+ - (void)retire:(NSString *)context;
14
+ - (void)lifecycle:(BOOL)foreground;
15
+ - (void)close;
16
+ @end
17
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,168 @@
1
+ #import "TFAdmissionBootstrap.h"
2
+ #import <atomic>
3
+
4
+ static void TFBootstrapInvalid(void) {
5
+ [NSException raise:@"TFNativeBootstrapInvalid" format:@"Invalid native bootstrap"];
6
+ }
7
+ static BOOL TFBootstrapString(id value, NSUInteger limit) { return [value isKindOfClass:NSString.class] && [(NSString *)value length] <= limit; }
8
+ static NSTextCheckingResult *TFBootstrapMatch(NSString *value, NSString *pattern) {
9
+ NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
10
+ NSTextCheckingResult *match = [expression firstMatchInString:value options:0 range:NSMakeRange(0, value.length)];
11
+ return match && NSEqualRanges(match.range, NSMakeRange(0, value.length)) ? match : nil;
12
+ }
13
+
14
+ @interface TFBootstrapFlag : NSObject { @public std::atomic_bool value; }
15
+ @end
16
+ @implementation TFBootstrapFlag
17
+ - (instancetype)init { if ((self = [super init])) value.store(true); return self; }
18
+ @end
19
+
20
+ @class TFBootstrapWork;
21
+ @interface TFBootstrapContext : NSObject
22
+ @property (nonatomic, copy, nullable) NSString *mapId;
23
+ @property (nonatomic, copy, nullable) NSString *endpoint;
24
+ @property (nonatomic, copy, nullable) NSString *credential;
25
+ @property (nonatomic) unsigned long long lastRequest;
26
+ @property (nonatomic) TFBootstrapFlag *alive;
27
+ @property (nonatomic) NSMutableDictionary<NSString *, TFBootstrapWork *> *pending;
28
+ @end
29
+ @implementation TFBootstrapContext
30
+ - (instancetype)init { if ((self = [super init])) { _alive = [TFBootstrapFlag new]; _pending = [NSMutableDictionary dictionary]; } return self; }
31
+ - (NSString *)description { return @"TFBootstrapContext(redacted)"; }
32
+ @end
33
+
34
+ @interface TFBootstrapWork : NSObject
35
+ @property (nonatomic, copy) NSString *identifier;
36
+ @property (nonatomic) TFBootstrapContext *context;
37
+ @property (nonatomic) TFBootstrapFlag *alive;
38
+ @property (nonatomic) NSTimeInterval deadline;
39
+ @property (nonatomic, copy, nullable) void (^completion)(NSDictionary *_Nullable);
40
+ @property (nonatomic, nullable) id<TFAdmissionCancel> network;
41
+ @property (nonatomic, nullable) id<TFAdmissionCancel> timer;
42
+ @end
43
+ @implementation TFBootstrapWork
44
+ - (NSString *)description { return @"TFBootstrapWork(redacted)"; }
45
+ @end
46
+
47
+ @interface TFAdmissionBootstrap () {
48
+ std::atomic_bool _active;
49
+ std::atomic_bool _foreground;
50
+ }
51
+ @property (nonatomic, copy) NSString *installation;
52
+ @property (nonatomic) id<TFAdmissionScheduler> scheduler;
53
+ @property (nonatomic) id<TFAdmissionNetwork> network;
54
+ @property (nonatomic, copy) TFAdmissionStartGuard owns;
55
+ @property (nonatomic) NSMutableDictionary<NSString *, TFBootstrapContext *> *contexts;
56
+ @property (nonatomic) NSUInteger pendingCount;
57
+ @end
58
+
59
+ @implementation TFAdmissionBootstrap
60
+ - (instancetype)initWithInstallation:(NSString *)installation scheduler:(id<TFAdmissionScheduler>)scheduler network:(id<TFAdmissionNetwork>)network owns:(TFAdmissionStartGuard)owns {
61
+ if ((self = [super init])) {
62
+ _installation = [installation copy]; _scheduler = scheduler; _network = network; _owns = [owns copy];
63
+ _contexts = [NSMutableDictionary dictionary]; _active.store(true); _foreground.store(true);
64
+ }
65
+ return self;
66
+ }
67
+ - (NSString *)description { return @"TFAdmissionBootstrap(redacted)"; }
68
+ - (BOOL)validContext:(NSString *)context { return TFAdmissionValidToken(context) && [context hasPrefix:[self.installation stringByAppendingString:@"."]]; }
69
+ - (unsigned long long)sequence:(NSString *)request {
70
+ NSString *prefix = [self.installation stringByAppendingString:@"."];
71
+ if (!TFBootstrapString(request, 96) || ![request hasPrefix:prefix]) TFBootstrapInvalid();
72
+ NSString *suffix = [request substringFromIndex:prefix.length];
73
+ if (!TFBootstrapMatch(suffix, @"[1-9][0-9]{0,15}")) TFBootstrapInvalid();
74
+ unsigned long long value = suffix.longLongValue;
75
+ if (value > 9007199254740991ULL) TFBootstrapInvalid();
76
+ return value;
77
+ }
78
+ - (BOOL)owned { @try { return self.owns(); } @catch (NSException *exception) { return NO; } }
79
+ - (void)registerContext:(NSString *)context mapId:(NSString *)mapId {
80
+ if (!_active.load() || ![self owned] || ![self validContext:context] || self.contexts[context] || self.contexts.count >= 16 ||
81
+ (mapId && (!TFBootstrapString(mapId, 64) || !TFBootstrapMatch(mapId, @"map_[A-Za-z0-9_-]{16}")))) TFBootstrapInvalid();
82
+ TFBootstrapContext *binding = [TFBootstrapContext new]; binding.mapId = mapId;
83
+ self.contexts[context] = binding;
84
+ }
85
+ - (void)startContext:(NSString *)contextId request:(NSString *)requestId url:(NSString *)url credential:(NSString *)credential body:(NSString *)body completion:(void (^)(NSDictionary *))completion {
86
+ if (![self validContext:contextId]) TFBootstrapInvalid();
87
+ TFBootstrapContext *context = self.contexts[contextId];
88
+ if (!_active.load() || !_foreground.load() || ![self owned] || !context || !context.alive->value.load() || !context.mapId || self.pendingCount >= 32) TFBootstrapInvalid();
89
+ unsigned long long sequence = [self sequence:requestId];
90
+ if (sequence <= context.lastRequest || !TFBootstrapString(credential, 58) || !TFBootstrapMatch(credential, @"tf_public_[0-9a-f]{48}") || !TFBootstrapString(body, 2048)) TFBootstrapInvalid();
91
+ NSTextCheckingResult *match = TFBootstrapMatch(body, @"\\{\"mapId\":\"(map_[A-Za-z0-9_-]{16})\",\"sessionId\":\"([A-Za-z0-9._:-]{1,255})\",\"surfaceId\":\"([a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?)\"\\}");
92
+ if (!match || ![[body substringWithRange:[match rangeAtIndex:1]] isEqual:context.mapId]) TFBootstrapInvalid();
93
+ for (NSUInteger index = 2; index <= 3; index++) {
94
+ NSString *value = [body substringWithRange:[match rangeAtIndex:index]];
95
+ if ([value hasPrefix:@"tf_native_"] || TFBootstrapMatch(value, @"tf_public_[0-9a-f]{48}")) TFBootstrapInvalid();
96
+ }
97
+ NSString *clean = TFAdmissionCleanURL(url);
98
+ if (![clean isEqual:[TFAdmissionOrigin(clean) stringByAppendingString:@"/v1/sessions/start"]] ||
99
+ (context.endpoint && (![context.endpoint isEqual:clean] || ![context.credential isEqual:credential]))) TFBootstrapInvalid();
100
+ // The controller supplies the initial validated binding; no later request
101
+ // may change its exact endpoint, port, or credential for this Map context.
102
+ context.endpoint = clean; context.credential = credential; context.lastRequest = sequence;
103
+ TFBootstrapWork *work = [TFBootstrapWork new];
104
+ work.identifier = requestId; work.context = context; work.alive = [TFBootstrapFlag new];
105
+ work.deadline = [self.scheduler nowMs] + 30000; work.completion = completion;
106
+ context.pending[requestId] = work; self.pendingCount++;
107
+ work.timer = [self.scheduler after:30000 perform:^{ [self finish:work reply:nil]; }];
108
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:clean]
109
+ cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
110
+ request.HTTPMethod = @"POST"; request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
111
+ request.HTTPShouldHandleCookies = NO;
112
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
113
+ [request setValue:@"no-store" forHTTPHeaderField:@"Cache-Control"];
114
+ [request setValue:credential forHTTPHeaderField:@"X-Tileflow-Mobile-Client"];
115
+ @try {
116
+ work.network = [self.network start:request mayStart:^BOOL { return [self mayStart:work]; } completion:^(NSHTTPURLResponse *response, NSData *bytes) {
117
+ [self.scheduler enqueue:^{
118
+ NSString *cacheControl = @"";
119
+ for (id name in response.allHeaderFields) {
120
+ if ([name isKindOfClass:NSString.class] && [(NSString *)name caseInsensitiveCompare:@"Cache-Control"] == NSOrderedSame) cacheControl = response.allHeaderFields[name];
121
+ }
122
+ BOOL valid = [self mayStart:work] && [response isKindOfClass:NSHTTPURLResponse.class] && response.statusCode >= 100 && response.statusCode <= 599 &&
123
+ TFBootstrapString(cacheControl, 1024) && [bytes isKindOfClass:NSData.class] && bytes.length <= 65536;
124
+ NSDictionary *reply = valid ? @{@"status": @(response.statusCode), @"cacheControl": cacheControl, @"bodyBase64": [bytes base64EncodedStringWithOptions:0]} : nil;
125
+ [self finish:work reply:reply];
126
+ }];
127
+ }];
128
+ if (!work.alive->value.load()) [work.network cancel];
129
+ } @catch (NSException *exception) { [self finish:work reply:nil]; }
130
+ }
131
+ - (BOOL)mayStart:(TFBootstrapWork *)work {
132
+ return _active.load() && _foreground.load() && work.alive->value.load() && work.context.alive->value.load() && [self.scheduler nowMs] < work.deadline && [self owned];
133
+ }
134
+ - (void)finish:(TFBootstrapWork *)work reply:(NSDictionary *)reply {
135
+ if (!work.alive->value.exchange(false)) return;
136
+ [work.context.pending removeObjectForKey:work.identifier]; self.pendingCount--;
137
+ [work.timer cancel]; work.timer = nil;
138
+ [work.network cancel]; work.network = nil;
139
+ void (^completion)(NSDictionary *) = work.completion; work.completion = nil;
140
+ @try { if (completion) completion(reply); } @catch (NSException *exception) { /* Consumers cannot reopen cancelled work. */ }
141
+ }
142
+ - (void)cancelContext:(NSString *)contextId request:(NSString *)requestId {
143
+ if (![self validContext:contextId]) TFBootstrapInvalid();
144
+ unsigned long long sequence = [self sequence:requestId];
145
+ TFBootstrapContext *context = self.contexts[contextId];
146
+ if (!context) return;
147
+ // Cancellation may arrive first. This bounded high-water mark prevents
148
+ // replay without accumulating retired-request tombstones.
149
+ context.lastRequest = MAX(context.lastRequest, sequence);
150
+ TFBootstrapWork *work = context.pending[requestId];
151
+ if (work) [self finish:work reply:nil];
152
+ }
153
+ - (void)retire:(NSString *)contextId {
154
+ TFBootstrapContext *context = self.contexts[contextId];
155
+ if (!context) return;
156
+ [self.contexts removeObjectForKey:contextId]; context.alive->value.store(false);
157
+ context.credential = nil; context.endpoint = nil;
158
+ for (TFBootstrapWork *work in context.pending.allValues) [self finish:work reply:nil];
159
+ }
160
+ - (void)lifecycle:(BOOL)foreground {
161
+ _foreground.store(foreground);
162
+ if (!foreground) for (TFBootstrapContext *context in self.contexts.allValues) for (TFBootstrapWork *work in context.pending.allValues) [self finish:work reply:nil];
163
+ }
164
+ - (void)close {
165
+ _active.store(false);
166
+ for (NSString *context in self.contexts.allKeys) [self retire:context];
167
+ }
168
+ @end
@@ -0,0 +1,13 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ // Resource identity only; the session controller remains the admission authority.
6
+ @interface TFAdmissionCatalog : NSObject
7
+ @property (nonatomic, readonly, copy) NSDictionary<NSString *, NSDictionary *> *resources;
8
+ - (instancetype)initWithResources:(NSArray<NSDictionary *> *)resources;
9
+ - (TFAdmissionCatalog *)extending:(NSArray<NSDictionary *> *)resources;
10
+ - (nullable NSDictionary *)find:(NSString *)url;
11
+ @end
12
+
13
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,193 @@
1
+ #import "TFAdmissionCatalog.h"
2
+ #import "TFAdmissionEngine.h"
3
+ #import <cmath>
4
+
5
+ static void TFCatalogInvalid(void) {
6
+ @throw [NSException exceptionWithName:@"TFAdmissionCatalogInvalid" reason:@"Invalid native resource catalog" userInfo:nil];
7
+ }
8
+ static BOOL TFText(id value, NSUInteger maximum) {
9
+ return [value isKindOfClass:NSString.class] && [(NSString *)value length] > 0 && [(NSString *)value length] <= maximum;
10
+ }
11
+ static NSRegularExpression *TFPattern(NSString *pattern) {
12
+ NSRegularExpression *result = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
13
+ if (!result) TFCatalogInvalid();
14
+ return result;
15
+ }
16
+ static BOOL TFMatches(NSString *pattern, NSString *value) {
17
+ return [TFPattern(pattern) numberOfMatchesInString:value options:0 range:NSMakeRange(0, value.length)] == 1;
18
+ }
19
+ static NSDictionary<NSString *, NSString *> *TFSlots(void) {
20
+ NSString *decimal = @"(?:0|[1-9][0-9]{0,9})";
21
+ NSString *coordinate = @"-?(?:0|[1-9][0-9]{0,7})(?:\\.[0-9]{1,16})?";
22
+ return @{
23
+ @"z": @"(?:0|[1-9][0-9]?)", @"x": decimal, @"y": decimal,
24
+ @"ratio": @"(?:@2x|@3x)?", @"quadkey": @"[0-3]{0,30}", @"prefix": @"[0-9a-f]{2}",
25
+ @"bbox-epsg-3857": [@[coordinate, coordinate, coordinate, coordinate] componentsJoinedByString:@","],
26
+ @"fontstack": @"(?:[A-Za-z0-9_.~,+-]|%[0-9a-fA-F]{2}){1,768}",
27
+ @"range": [NSString stringWithFormat:@"%@-%@", decimal, decimal]
28
+ };
29
+ }
30
+
31
+ @interface TFAdmissionCatalogRule : NSObject
32
+ @property (nonatomic, copy) NSDictionary *resource;
33
+ @property (nonatomic, nullable) NSRegularExpression *pattern;
34
+ @property (nonatomic, copy) NSArray<NSString *> *names;
35
+ @end
36
+ @implementation TFAdmissionCatalogRule
37
+ - (NSString *)description { return @"TFAdmissionCatalogRule(redacted)"; }
38
+ @end
39
+
40
+ static TFAdmissionCatalogRule *TFRule(NSDictionary *input) {
41
+ if (![input isKindOfClass:NSDictionary.class] || input.count > 5 || !TFText(input[@"url"], 1920) ||
42
+ ![@[@"style", @"tilejson", @"tile", @"sprite", @"glyph", @"font"] containsObject:input[@"scope"]]) TFCatalogInvalid();
43
+ NSString *url = [input[@"url"] copy], *scope = [input[@"scope"] copy];
44
+ NSString *tileset = input[@"tilesetId"], *family = input[@"template"];
45
+ if (([scope isEqual:@"tile"] || [scope isEqual:@"tilejson"]) && !tileset) TFCatalogInvalid();
46
+ if (tileset && (!TFText(tileset, 255) || !TFMatches(@"\\A[A-Za-z0-9._:-]{1,255}\\z", tileset) ||
47
+ [tileset.lowercaseString containsString:@"tf_native_"] || [tileset.lowercaseString containsString:@"tf_public_"])) TFCatalogInvalid();
48
+ NSMutableDictionary *resource = [@{@"url": url, @"scope": scope} mutableCopy];
49
+ if (tileset) resource[@"tilesetId"] = [tileset copy];
50
+ TFAdmissionCatalogRule *rule = [TFAdmissionCatalogRule new];
51
+ if (!family) {
52
+ if (input[@"fontStacks"] || [url containsString:@"{"] || [url containsString:@"}"]) TFCatalogInvalid();
53
+ TFAdmissionCleanURL(url);
54
+ rule.resource = [resource copy]; rule.names = @[];
55
+ return rule;
56
+ }
57
+ if (![@[@"tile", @"glyphs"] containsObject:family] || ![url hasPrefix:@"https://"]) TFCatalogInvalid();
58
+ NSRange path = [url rangeOfString:@"/" options:0 range:NSMakeRange(8, url.length - 8)];
59
+ if (path.location == NSNotFound || [[url substringToIndex:path.location] rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"{}"]].location != NSNotFound) TFCatalogInvalid();
60
+ NSRegularExpression *placeholder = TFPattern(@"\\{([a-z0-9-]+)\\}");
61
+ NSArray<NSTextCheckingResult *> *matches = [placeholder matchesInString:url options:0 range:NSMakeRange(0, url.length)];
62
+ if (!matches.count || matches.count > 16) TFCatalogInvalid();
63
+ NSString *masked = [placeholder stringByReplacingMatchesInString:url options:0 range:NSMakeRange(0, url.length) withTemplate:@"native-template-slot"];
64
+ if ([masked containsString:@"{"] || [masked containsString:@"}"]) TFCatalogInvalid();
65
+ TFAdmissionCleanURL(masked);
66
+ NSMutableArray<NSString *> *names = [NSMutableArray array];
67
+ NSMutableString *pattern = [NSMutableString stringWithString:@"\\A"];
68
+ NSDictionary *slots = TFSlots();
69
+ NSUInteger cursor = 0;
70
+ for (NSTextCheckingResult *match in matches) {
71
+ NSString *name = [url substringWithRange:[match rangeAtIndex:1]];
72
+ if (!slots[name] || (names.count && match.range.location == cursor)) TFCatalogInvalid();
73
+ BOOL glyphSlot = [name isEqual:@"fontstack"] || [name isEqual:@"range"];
74
+ if ([family isEqual:@"glyphs"] != glyphSlot) TFCatalogInvalid();
75
+ [pattern appendString:[NSRegularExpression escapedPatternForString:[url substringWithRange:NSMakeRange(cursor, match.range.location - cursor)]]];
76
+ [pattern appendFormat:@"(%@)", slots[name]];
77
+ [names addObject:name]; cursor = NSMaxRange(match.range);
78
+ }
79
+ [pattern appendString:[NSRegularExpression escapedPatternForString:[url substringFromIndex:cursor]]];
80
+ [pattern appendString:@"\\z"];
81
+ if ([family isEqual:@"tile"]) {
82
+ if (![scope isEqual:@"tile"] || input[@"fontStacks"]) TFCatalogInvalid();
83
+ } else {
84
+ NSArray *stacks = input[@"fontStacks"];
85
+ if (![scope isEqual:@"glyph"] || ![names containsObject:@"fontstack"] || ![names containsObject:@"range"] ||
86
+ ![stacks isKindOfClass:NSArray.class] || !stacks.count || stacks.count > 16) TFCatalogInvalid();
87
+ NSMutableArray *snapshot = [NSMutableArray array];
88
+ for (NSString *stack in stacks) {
89
+ if (!TFText(stack, 256) || [snapshot containsObject:stack] ||
90
+ ![stack isEqual:[stack stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]] ||
91
+ [stack rangeOfCharacterFromSet:NSCharacterSet.controlCharacterSet].location != NSNotFound ||
92
+ [stack rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\\/?#%&="]].location != NSNotFound ||
93
+ [stack.lowercaseString containsString:@"tf_native_"] || [stack.lowercaseString containsString:@"tf_public_"]) TFCatalogInvalid();
94
+ for (NSString *part in [stack componentsSeparatedByString:@","]) {
95
+ if (!part.length || ![part isEqual:[part stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]]) TFCatalogInvalid();
96
+ }
97
+ [snapshot addObject:[stack copy]];
98
+ }
99
+ resource[@"fontStacks"] = [snapshot copy];
100
+ }
101
+ resource[@"template"] = [family copy];
102
+ rule.resource = [resource copy]; rule.pattern = TFPattern(pattern); rule.names = [names copy];
103
+ return rule;
104
+ }
105
+
106
+ static BOOL TFRuleMatches(TFAdmissionCatalogRule *rule, NSString *url) {
107
+ if (!rule.pattern) return [rule.resource[@"url"] isEqual:url];
108
+ NSTextCheckingResult *match = [rule.pattern firstMatchInString:url options:0 range:NSMakeRange(0, url.length)];
109
+ if (!match) return NO;
110
+ NSMutableDictionary<NSString *, NSString *> *values = [NSMutableDictionary dictionary];
111
+ for (NSUInteger index = 0; index < rule.names.count; index++) {
112
+ NSString *name = rule.names[index], *value = [url substringWithRange:[match rangeAtIndex:index + 1]];
113
+ if (values[name] && ![values[name] isEqual:value]) return NO;
114
+ values[name] = value;
115
+ }
116
+ NSUInteger zoom = values[@"z"] ? [values[@"z"] integerValue] : 30;
117
+ if (zoom > 30) return NO;
118
+ for (NSString *name in @[@"x", @"y"]) if (values[name] && [values[name] longLongValue] >= (1LL << zoom)) return NO;
119
+ if (values[@"quadkey"] && values[@"z"] && values[@"quadkey"].length != zoom) return NO;
120
+ if (values[@"bbox-epsg-3857"]) {
121
+ NSArray *numbers = [values[@"bbox-epsg-3857"] componentsSeparatedByString:@","];
122
+ for (NSString *number in numbers) if (!std::isfinite(number.doubleValue) || std::abs(number.doubleValue) > 20037508.343) return NO;
123
+ if ([numbers[0] doubleValue] >= [numbers[2] doubleValue] || [numbers[1] doubleValue] >= [numbers[3] doubleValue]) return NO;
124
+ }
125
+ if (values[@"range"]) {
126
+ NSArray *pair = [values[@"range"] componentsSeparatedByString:@"-"];
127
+ long long start = [pair[0] longLongValue], end = [pair[1] longLongValue];
128
+ if (start % 256 != 0 || end != start + 255 || end > 1114111) return NO;
129
+ }
130
+ if (values[@"fontstack"]) {
131
+ NSString *stack = values[@"fontstack"].stringByRemovingPercentEncoding;
132
+ if (!stack || ![rule.resource[@"fontStacks"] containsObject:stack]) return NO;
133
+ }
134
+ return YES;
135
+ }
136
+
137
+ static NSDictionary *TFMergeExtension(NSDictionary *previous, NSDictionary *next) {
138
+ if ([previous isEqual:next]) return previous;
139
+ if (![previous[@"scope"] isEqual:next[@"scope"]] ||
140
+ !((previous[@"tilesetId"] == nil && next[@"tilesetId"] == nil) || [previous[@"tilesetId"] isEqual:next[@"tilesetId"]]) ||
141
+ !((previous[@"template"] == nil && next[@"template"] == nil) || [previous[@"template"] isEqual:next[@"template"]]) ||
142
+ ![previous[@"scope"] isEqual:@"glyph"] || ![previous[@"template"] isEqual:@"glyphs"]) TFCatalogInvalid();
143
+ NSArray *before = previous[@"fontStacks"], *after = next[@"fontStacks"];
144
+ if (![before isKindOfClass:NSArray.class] || ![after isKindOfClass:NSArray.class] || after.count > 16) TFCatalogInvalid();
145
+ for (NSString *stack in before) if (![after containsObject:stack]) TFCatalogInvalid();
146
+ return next;
147
+ }
148
+
149
+ @interface TFAdmissionCatalog ()
150
+ @property (nonatomic, readwrite, copy) NSDictionary<NSString *, NSDictionary *> *resources;
151
+ @property (nonatomic, copy) NSArray<TFAdmissionCatalogRule *> *rules;
152
+ @end
153
+ @implementation TFAdmissionCatalog
154
+ - (instancetype)initWithResources:(NSArray<NSDictionary *> *)resources {
155
+ if ((self = [super init])) {
156
+ @try {
157
+ if (![resources isKindOfClass:NSArray.class] || resources.count > 128) TFCatalogInvalid();
158
+ NSMutableDictionary *snapshots = [NSMutableDictionary dictionary];
159
+ NSMutableArray *rules = [NSMutableArray array];
160
+ for (NSDictionary *resource in resources) {
161
+ TFAdmissionCatalogRule *rule = TFRule(resource);
162
+ NSString *url = rule.resource[@"url"];
163
+ if (snapshots[url]) TFCatalogInvalid();
164
+ snapshots[url] = rule.resource; [rules addObject:rule];
165
+ }
166
+ _resources = [snapshots copy]; _rules = [rules copy];
167
+ } @catch (NSException *exception) { TFCatalogInvalid(); }
168
+ }
169
+ return self;
170
+ }
171
+ - (TFAdmissionCatalog *)extending:(NSArray<NSDictionary *> *)resources {
172
+ TFAdmissionCatalog *incoming = [[TFAdmissionCatalog alloc] initWithResources:resources];
173
+ NSMutableDictionary *merged = [self.resources mutableCopy];
174
+ for (NSString *url in incoming.resources) {
175
+ NSDictionary *resource = incoming.resources[url];
176
+ merged[url] = merged[url] ? TFMergeExtension(merged[url], resource) : resource;
177
+ }
178
+ return [[TFAdmissionCatalog alloc] initWithResources:merged.allValues];
179
+ }
180
+ - (NSDictionary *)find:(NSString *)url {
181
+ @try { TFAdmissionCleanURL(url); } @catch (NSException *exception) { return nil; }
182
+ NSDictionary *found = nil;
183
+ for (TFAdmissionCatalogRule *rule in self.rules) {
184
+ if (!TFRuleMatches(rule, url)) continue;
185
+ if (found) TFCatalogInvalid();
186
+ NSMutableDictionary *identity = [@{@"url": url, @"scope": rule.resource[@"scope"]} mutableCopy];
187
+ if (rule.resource[@"tilesetId"]) identity[@"tilesetId"] = rule.resource[@"tilesetId"];
188
+ found = [identity copy];
189
+ }
190
+ return found;
191
+ }
192
+ - (NSString *)description { return @"TFAdmissionCatalog(redacted)"; }
193
+ @end
@@ -0,0 +1,7 @@
1
+ #import "TFNativeDocumentRegistry.h"
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ FOUNDATION_EXPORT TFNativeDocumentScope *TFCreateAdmissionDocumentScope(TFAdmissionEngine *engine, NSString *context);
6
+
7
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,45 @@
1
+ #import "TFAdmissionDocumentScope.h"
2
+ #import <atomic>
3
+
4
+ @interface TFAdmissionDocumentOperation : NSObject <TFAdmissionCancel> {
5
+ std::atomic_bool _finished;
6
+ }
7
+ @property (nonatomic, strong, nullable) id<TFAdmissionCancel> request;
8
+ @property (nonatomic, copy, nullable) TFAdmissionNetworkCompletion completion;
9
+ - (void)finish:(nullable NSHTTPURLResponse *)response body:(nullable NSData *)body;
10
+ @end
11
+ @implementation TFAdmissionDocumentOperation
12
+ - (instancetype)init { if ((self = [super init])) _finished.store(false); return self; }
13
+ - (void)finish:(NSHTTPURLResponse *)response body:(NSData *)body {
14
+ if (_finished.exchange(true)) return;
15
+ TFAdmissionNetworkCompletion completion = self.completion; self.completion = nil;
16
+ if (completion) completion(response, body);
17
+ }
18
+ - (void)cancel {
19
+ @try { [self.request cancel]; }
20
+ @finally {
21
+ // The admission network owns physical cleanup; this is the document's logical result.
22
+ [self finish:nil body:nil];
23
+ }
24
+ }
25
+ - (NSString *)description { return @"TFAdmissionDocumentOperation(redacted)"; }
26
+ @end
27
+
28
+ TFNativeDocumentScope *TFCreateAdmissionDocumentScope(TFAdmissionEngine *engine, NSString *context) {
29
+ TFAdmissionStartGuard active = [engine contextGuard:context];
30
+ return [[TFNativeDocumentScope alloc] initWithContext:context active:active load:^id<TFAdmissionCancel>(NSURLRequest *request, TFAdmissionNetworkCompletion completion) {
31
+ NSString *url = TFAdmissionCleanURL(request.URL.absoluteString);
32
+ NSMutableURLRequest *tagged = [request mutableCopy];
33
+ tagged.URL = [NSURL URLWithString:[url stringByAppendingFormat:@"%@__tf_native_context=%@", [url containsString:@"?"] ? @"&" : @"?", context]];
34
+ TFAdmissionDocumentOperation *operation = [TFAdmissionDocumentOperation new];
35
+ operation.completion = completion;
36
+ operation.request = [engine request:tagged response:^(NSHTTPURLResponse *response, NSData *body) {
37
+ [operation finish:response body:body];
38
+ } failure:^{ [operation finish:nil body:nil]; } delegate:^id<TFAdmissionCancel>(NSURLRequest *request, TFAdmissionNetworkCompletion completion) {
39
+ // Unprotected preparation uses the independent credential-free document channel.
40
+ completion(nil, nil);
41
+ return [TFAdmissionDocumentOperation new];
42
+ }];
43
+ return operation;
44
+ }];
45
+ }
@@ -0,0 +1,57 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @protocol TFAdmissionCancel <NSObject>
6
+ - (void)cancel;
7
+ @end
8
+
9
+ @protocol TFAdmissionScheduler <NSObject>
10
+ - (NSTimeInterval)nowMs;
11
+ - (void)enqueue:(dispatch_block_t)block;
12
+ - (id<TFAdmissionCancel>)after:(NSTimeInterval)milliseconds perform:(dispatch_block_t)block;
13
+ @end
14
+
15
+ typedef BOOL (^TFAdmissionStartGuard)(void);
16
+ typedef void (^TFAdmissionNetworkCompletion)(NSHTTPURLResponse *_Nullable response, NSData *_Nullable body);
17
+ typedef id<TFAdmissionCancel> _Nonnull (^TFAdmissionDelegate)(NSURLRequest *request, TFAdmissionNetworkCompletion completion);
18
+
19
+ @protocol TFAdmissionNetwork <NSObject>
20
+ - (id<TFAdmissionCancel>)start:(NSURLRequest *)request
21
+ mayStart:(TFAdmissionStartGuard)guard
22
+ completion:(TFAdmissionNetworkCompletion)completion;
23
+ @end
24
+
25
+ // Internal per-installation registry. Only the JavaScript controller admits
26
+ // commercial requests; native sequence values identify transport messages.
27
+ @interface TFAdmissionEngine : NSObject
28
+ - (instancetype)initWithInstallation:(NSString *)installation
29
+ scheduler:(id<TFAdmissionScheduler>)scheduler
30
+ network:(id<TFAdmissionNetwork>)network
31
+ owns:(TFAdmissionStartGuard)owns
32
+ emit:(void (^)(NSDictionary *event))emit;
33
+ - (NSString *)registerMap:(nullable NSString *)mapId resources:(NSArray<NSDictionary *> *)resources;
34
+ - (NSUInteger)extendContext:(NSString *)context resources:(NSArray<NSDictionary *> *)resources;
35
+ - (TFAdmissionStartGuard)contextGuard:(NSString *)context;
36
+ - (id<TFAdmissionCancel>)request:(NSURLRequest *)request
37
+ response:(void (^)(NSHTTPURLResponse *response, NSData *body))response
38
+ failure:(dispatch_block_t)failure
39
+ delegate:(TFAdmissionDelegate)delegate;
40
+ - (NSUInteger)completeContext:(NSString *)context generation:(NSUInteger)generation
41
+ batch:(NSString *)batch results:(NSArray<NSDictionary *> *)results;
42
+ - (void)retire:(NSString *)context;
43
+ - (void)lifecycle:(BOOL)foreground;
44
+ - (void)close;
45
+ - (BOOL)isOwner;
46
+ @end
47
+
48
+ FOUNDATION_EXPORT NSString *const TFAdmissionGrantHeader;
49
+ FOUNDATION_EXPORT BOOL TFAdmissionHasReservedContext(NSString *url);
50
+ FOUNDATION_EXPORT NSDictionary<NSString *, NSString *> *TFAdmissionStripContext(NSString *url);
51
+ FOUNDATION_EXPORT NSString *TFAdmissionCleanURL(NSString *url);
52
+ FOUNDATION_EXPORT NSString *TFAdmissionOrigin(NSString *url);
53
+ FOUNDATION_EXPORT BOOL TFAdmissionStyleMatchesMap(NSString *url, NSString *mapId);
54
+ FOUNDATION_EXPORT BOOL TFAdmissionValidToken(id value);
55
+ FOUNDATION_EXPORT NSError *TFAdmissionSafeError(void);
56
+
57
+ NS_ASSUME_NONNULL_END