@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.0
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.
- package/README.md +216 -4
- package/TileflowNativeAdmission.podspec +34 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
- package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +234 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +80 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
- package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
- package/dist/contract-DWs-lDPo.d.ts +144 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6989 -0
- package/dist/internal/camera.d.ts +65 -0
- package/dist/internal/camera.js +325 -0
- package/dist/internal/hosted-binding.d.ts +18 -0
- package/dist/internal/hosted-binding.js +277 -0
- package/dist/internal/initial-view.d.ts +11 -0
- package/dist/internal/initial-view.js +8 -0
- package/dist/internal/interactions.d.ts +76 -0
- package/dist/internal/interactions.js +794 -0
- package/dist/internal/native-admission-bridge.d.ts +29 -0
- package/dist/internal/native-admission-bridge.js +351 -0
- package/dist/internal/native-admission.d.ts +78 -0
- package/dist/internal/native-admission.js +1614 -0
- package/dist/internal/native-appearance.d.ts +21 -0
- package/dist/internal/native-appearance.js +110 -0
- package/dist/internal/native-configuration-bridge.d.ts +9 -0
- package/dist/internal/native-configuration-bridge.js +343 -0
- package/dist/internal/session.d.ts +125 -0
- package/dist/internal/session.js +887 -0
- package/dist/internal/source-state.d.ts +11 -0
- package/dist/internal/source-state.js +26 -0
- package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
- package/docs/native-admission.md +108 -0
- package/docs/native-configuration.md +210 -0
- package/docs/native-interactions.md +161 -0
- package/docs/native-location.md +148 -0
- package/ios/TFAdmissionBootstrap.h +17 -0
- package/ios/TFAdmissionBootstrap.mm +168 -0
- package/ios/TFAdmissionCatalog.h +13 -0
- package/ios/TFAdmissionCatalog.mm +193 -0
- package/ios/TFAdmissionDocumentScope.h +7 -0
- package/ios/TFAdmissionDocumentScope.mm +45 -0
- package/ios/TFAdmissionEngine.h +57 -0
- package/ios/TFAdmissionEngine.mm +371 -0
- package/ios/TFAdmissionInstallation.h +27 -0
- package/ios/TFAdmissionInstallation.mm +117 -0
- package/ios/TFAdmissionNetwork.h +16 -0
- package/ios/TFAdmissionNetwork.mm +268 -0
- package/ios/TFAdmissionURL.mm +116 -0
- package/ios/TFDocumentBounds.h +9 -0
- package/ios/TFDocumentBounds.mm +19 -0
- package/ios/TFMobileConfiguration.h +23 -0
- package/ios/TFMobileConfiguration.mm +139 -0
- package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
- package/ios/TFNativeDocumentRegistry.h +28 -0
- package/ios/TFNativeDocumentRegistry.mm +193 -0
- package/ios/TFNativeDocumentURL.h +6 -0
- package/ios/TFNativeSurfaceState.h +28 -0
- package/ios/TFNativeSurfaceState.mm +128 -0
- package/ios/TileflowNativeAdmission.h +9 -0
- package/ios/TileflowNativeAdmission.mm +184 -0
- package/ios/TileflowNativeConfiguration.mm +41 -0
- package/ios/TileflowNativeDocuments.h +12 -0
- package/ios/TileflowNativeDocuments.mm +105 -0
- package/ios/TileflowNativeSurface.mm +403 -0
- package/ios/tileflow_native_admission.rb +45 -0
- package/package.json +51 -7
- package/react-native.config.cjs +13 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import "TFAdmissionEngine.h"
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
@interface TFNativeDocumentScope : NSObject
|
|
6
|
+
@property (nonatomic, readonly, copy) NSString *context;
|
|
7
|
+
@property (nonatomic, readonly, copy) TFAdmissionStartGuard active;
|
|
8
|
+
@property (nonatomic, readonly, copy) TFAdmissionDelegate load;
|
|
9
|
+
- (instancetype)initWithContext:(NSString *)context active:(TFAdmissionStartGuard)active load:(TFAdmissionDelegate)load;
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
typedef id<TFAdmissionCancel> _Nonnull (^TFNativeDocumentLoad)(NSURLRequest *request, TFNativeDocumentScope *_Nullable scope, TFAdmissionStartGuard guard, TFAdmissionNetworkCompletion completion);
|
|
13
|
+
|
|
14
|
+
// Calls are serialized on the native scheduler; only liveness is read by the network guard.
|
|
15
|
+
@interface TFNativeDocumentRegistry : NSObject
|
|
16
|
+
- (instancetype)initWithScheduler:(id<TFAdmissionScheduler>)scheduler load:(TFNativeDocumentLoad)load;
|
|
17
|
+
- (NSString *)open:(NSURLRequest *)request maximumBytes:(NSUInteger)maximumBytes scope:(nullable TFNativeDocumentScope *)scope;
|
|
18
|
+
- (NSString *)open:(NSURLRequest *)request maximumBytes:(NSUInteger)maximumBytes scope:(nullable TFNativeDocumentScope *)scope developmentOrigin:(nullable NSString *)developmentOrigin;
|
|
19
|
+
- (void)response:(NSString *)identifier completion:(void (^)(NSDictionary *_Nullable header))completion;
|
|
20
|
+
- (NSDictionary *)chunk:(NSString *)identifier maximumBytes:(NSUInteger)maximumBytes;
|
|
21
|
+
- (void)cancel:(NSString *)identifier;
|
|
22
|
+
- (void)retireContext:(NSString *)context;
|
|
23
|
+
- (void)retireProtected;
|
|
24
|
+
- (void)lifecycle:(BOOL)foreground;
|
|
25
|
+
- (void)close;
|
|
26
|
+
@end
|
|
27
|
+
|
|
28
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#import "TFNativeDocumentRegistry.h"
|
|
2
|
+
#import "TFDocumentBounds.h"
|
|
3
|
+
#import "TFNativeDocumentURL.h"
|
|
4
|
+
#import <atomic>
|
|
5
|
+
|
|
6
|
+
static void TFDocumentInvalid(void) {
|
|
7
|
+
@throw [NSException exceptionWithName:@"TFNativeDocumentInvalid" reason:@"Native document acquisition failed." userInfo:nil];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@implementation TFNativeDocumentScope
|
|
11
|
+
- (instancetype)initWithContext:(NSString *)context active:(TFAdmissionStartGuard)active load:(TFAdmissionDelegate)load {
|
|
12
|
+
if ((self = [super init])) {
|
|
13
|
+
if (!TFAdmissionValidToken(context) || !active || !load) TFDocumentInvalid();
|
|
14
|
+
_context = [context copy]; _active = [active copy]; _load = [load copy];
|
|
15
|
+
}
|
|
16
|
+
return self;
|
|
17
|
+
}
|
|
18
|
+
- (NSString *)description { return @"TFNativeDocumentScope(redacted)"; }
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
@interface TFNativeDocumentWork : NSObject { @public std::atomic_bool live; }
|
|
22
|
+
@property (nonatomic, copy) NSString *identifier;
|
|
23
|
+
@property (nonatomic, copy) NSString *origin;
|
|
24
|
+
@property (nonatomic, copy, nullable) NSString *developmentOrigin;
|
|
25
|
+
@property (nonatomic, copy) NSURLRequest *request;
|
|
26
|
+
@property (nonatomic, strong, nullable) TFNativeDocumentScope *scope;
|
|
27
|
+
@property (nonatomic) NSUInteger maximumBytes;
|
|
28
|
+
@property (nonatomic) NSTimeInterval deadline;
|
|
29
|
+
@property (nonatomic) BOOL nativeFinished;
|
|
30
|
+
@property (nonatomic) BOOL requested;
|
|
31
|
+
@property (nonatomic) NSUInteger offset;
|
|
32
|
+
@property (nonatomic) NSUInteger revision;
|
|
33
|
+
@property (nonatomic) NSUInteger redirects;
|
|
34
|
+
@property (nonatomic, strong, nullable) id<TFAdmissionCancel> cancellation;
|
|
35
|
+
@property (nonatomic, strong, nullable) id<TFAdmissionCancel> timer;
|
|
36
|
+
@property (nonatomic, strong, nullable) NSMutableData *body;
|
|
37
|
+
@property (nonatomic, copy, nullable) NSDictionary *header;
|
|
38
|
+
@property (nonatomic, copy, nullable) void (^waiter)(NSDictionary *_Nullable);
|
|
39
|
+
@end
|
|
40
|
+
@implementation TFNativeDocumentWork
|
|
41
|
+
- (instancetype)init { if ((self = [super init])) live.store(true); return self; }
|
|
42
|
+
- (NSString *)description { return @"TFNativeDocumentWork(redacted)"; }
|
|
43
|
+
@end
|
|
44
|
+
|
|
45
|
+
@interface TFNativeDocumentRegistry () {
|
|
46
|
+
std::atomic_bool _foreground;
|
|
47
|
+
std::atomic_bool _closed;
|
|
48
|
+
}
|
|
49
|
+
@property (nonatomic, strong) id<TFAdmissionScheduler> scheduler;
|
|
50
|
+
@property (nonatomic, copy) TFNativeDocumentLoad load;
|
|
51
|
+
@property (nonatomic, strong) NSMutableDictionary<NSString *, TFNativeDocumentWork *> *work;
|
|
52
|
+
@end
|
|
53
|
+
@implementation TFNativeDocumentRegistry
|
|
54
|
+
- (instancetype)initWithScheduler:(id<TFAdmissionScheduler>)scheduler load:(TFNativeDocumentLoad)load {
|
|
55
|
+
if ((self = [super init])) {
|
|
56
|
+
if (!scheduler || !load) TFDocumentInvalid();
|
|
57
|
+
_scheduler = scheduler; _load = [load copy]; _work = [NSMutableDictionary dictionary];
|
|
58
|
+
_foreground.store(true); _closed.store(false);
|
|
59
|
+
}
|
|
60
|
+
return self;
|
|
61
|
+
}
|
|
62
|
+
- (BOOL)active:(TFNativeDocumentWork *)work {
|
|
63
|
+
@try {
|
|
64
|
+
return work->live.load() && !self->_closed.load() && self->_foreground.load() &&
|
|
65
|
+
[self.scheduler nowMs] < work.deadline && (!work.scope || work.scope.active());
|
|
66
|
+
} @catch (NSException *exception) { return NO; }
|
|
67
|
+
}
|
|
68
|
+
- (NSString *)open:(NSURLRequest *)request maximumBytes:(NSUInteger)maximumBytes scope:(TFNativeDocumentScope *)scope {
|
|
69
|
+
return [self open:request maximumBytes:maximumBytes scope:scope developmentOrigin:nil];
|
|
70
|
+
}
|
|
71
|
+
- (NSString *)open:(NSURLRequest *)request maximumBytes:(NSUInteger)maximumBytes scope:(TFNativeDocumentScope *)scope developmentOrigin:(NSString *)developmentOrigin {
|
|
72
|
+
if (scope && developmentOrigin) TFDocumentInvalid();
|
|
73
|
+
TFNativeDocumentCleanURL(request.URL.absoluteString, developmentOrigin);
|
|
74
|
+
if (self->_closed.load() || !self->_foreground.load() || self.work.count >= 16 || maximumBytes < 1 || maximumBytes > 8388608 ||
|
|
75
|
+
![request.HTTPMethod isEqual:@"GET"] || request.HTTPBody || request.HTTPBodyStream || request.allHTTPHeaderFields.count ||
|
|
76
|
+
(scope && !scope.active())) TFDocumentInvalid();
|
|
77
|
+
TFNativeDocumentWork *work = [TFNativeDocumentWork new];
|
|
78
|
+
work.identifier = NSUUID.UUID.UUIDString;
|
|
79
|
+
work.origin = TFNativeDocumentOrigin(request.URL.absoluteString);
|
|
80
|
+
work.developmentOrigin = developmentOrigin;
|
|
81
|
+
work.maximumBytes = maximumBytes; work.scope = scope; work.deadline = [self.scheduler nowMs] + 30000;
|
|
82
|
+
NSMutableURLRequest *copy = [request mutableCopy];
|
|
83
|
+
copy.timeoutInterval = 30; copy.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
84
|
+
copy.HTTPShouldHandleCookies = NO;
|
|
85
|
+
TFDocumentSetByteLimit(copy, maximumBytes);
|
|
86
|
+
work.request = copy;
|
|
87
|
+
self.work[work.identifier] = work;
|
|
88
|
+
work.timer = [self.scheduler after:30000 perform:^{ [self retireQuietly:work.identifier]; }];
|
|
89
|
+
[self issue:work];
|
|
90
|
+
return work.identifier;
|
|
91
|
+
}
|
|
92
|
+
- (void)issue:(TFNativeDocumentWork *)work {
|
|
93
|
+
if (![self active:work]) { [self retireQuietly:work.identifier]; return; }
|
|
94
|
+
NSUInteger revision = ++work.revision;
|
|
95
|
+
work.nativeFinished = NO;
|
|
96
|
+
@try {
|
|
97
|
+
id<TFAdmissionCancel> cancellation = self.load(work.request, work.scope, ^BOOL { return [self active:work]; }, ^(NSHTTPURLResponse *response, NSData *body) {
|
|
98
|
+
[self.scheduler enqueue:^{
|
|
99
|
+
if (revision != work.revision || work.nativeFinished) return;
|
|
100
|
+
work.nativeFinished = YES;
|
|
101
|
+
[self received:work response:response body:body];
|
|
102
|
+
}];
|
|
103
|
+
});
|
|
104
|
+
if (!cancellation) TFDocumentInvalid();
|
|
105
|
+
work.cancellation = cancellation;
|
|
106
|
+
if (!work->live.load()) [self retireQuietly:work.identifier];
|
|
107
|
+
} @catch (NSException *exception) {
|
|
108
|
+
// A throwing adapter might already have queued work. Retain the bounded
|
|
109
|
+
// reservation until its completion rather than manufacturing capacity.
|
|
110
|
+
[self retireQuietly:work.identifier];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
- (void)received:(TFNativeDocumentWork *)work response:(NSHTTPURLResponse *)response body:(NSData *)body {
|
|
114
|
+
if (![self active:work] || !response || !body || response.statusCode < 100 || response.statusCode > 599 || body.length > work.maximumBytes) {
|
|
115
|
+
[self retireQuietly:work.identifier]; return;
|
|
116
|
+
}
|
|
117
|
+
NSString *url = response.URL.absoluteString;
|
|
118
|
+
@try {
|
|
119
|
+
TFNativeDocumentCleanURL(url, work.developmentOrigin);
|
|
120
|
+
if (![TFNativeDocumentOrigin(url) isEqual:work.origin]) TFDocumentInvalid();
|
|
121
|
+
if (!work.scope && [@[@301, @302, @303, @307, @308] containsObject:@(response.statusCode)]) {
|
|
122
|
+
NSString *location = [response valueForHTTPHeaderField:@"Location"];
|
|
123
|
+
if (++work.redirects > 3 || ![location isKindOfClass:NSString.class] || !location.length || location.length > 2048 || [location containsString:@"\\"]) TFDocumentInvalid();
|
|
124
|
+
NSURL *target = [NSURL URLWithString:location relativeToURL:response.URL].absoluteURL;
|
|
125
|
+
TFNativeDocumentCleanURL(target.absoluteString, work.developmentOrigin);
|
|
126
|
+
if (![TFNativeDocumentOrigin(target.absoluteString) isEqual:work.origin]) TFDocumentInvalid();
|
|
127
|
+
NSMutableURLRequest *next = [work.request mutableCopy]; next.URL = target;
|
|
128
|
+
next.timeoutInterval = MAX(0.001, (work.deadline - [self.scheduler nowMs]) / 1000);
|
|
129
|
+
work.request = next; work.cancellation = nil;
|
|
130
|
+
[self issue:work]; return;
|
|
131
|
+
}
|
|
132
|
+
} @catch (NSException *exception) { [self retireQuietly:work.identifier]; return; }
|
|
133
|
+
work.body = [body mutableCopy];
|
|
134
|
+
work.header = @{@"url": [url copy], @"status": @(response.statusCode)};
|
|
135
|
+
void (^waiter)(NSDictionary *) = work.waiter; work.waiter = nil;
|
|
136
|
+
if (waiter) waiter(work.header);
|
|
137
|
+
}
|
|
138
|
+
- (void)response:(NSString *)identifier completion:(void (^)(NSDictionary *_Nullable))completion {
|
|
139
|
+
TFNativeDocumentWork *work = self.work[identifier];
|
|
140
|
+
if (!work || ![self active:work] || work.requested) { completion(nil); return; }
|
|
141
|
+
work.requested = YES;
|
|
142
|
+
if (work.header) completion(work.header);
|
|
143
|
+
else work.waiter = completion;
|
|
144
|
+
}
|
|
145
|
+
- (NSDictionary *)chunk:(NSString *)identifier maximumBytes:(NSUInteger)maximumBytes {
|
|
146
|
+
TFNativeDocumentWork *work = self.work[identifier];
|
|
147
|
+
if (!work || ![self active:work] || !work.requested || !work.body || maximumBytes < 1 || maximumBytes > 65536) {
|
|
148
|
+
[self retireQuietly:identifier]; TFDocumentInvalid();
|
|
149
|
+
}
|
|
150
|
+
NSUInteger length = MIN(maximumBytes, work.body.length - work.offset);
|
|
151
|
+
NSData *bytes = [work.body subdataWithRange:NSMakeRange(work.offset, length)];
|
|
152
|
+
NSString *encoded = [bytes base64EncodedStringWithOptions:0];
|
|
153
|
+
work.offset += length;
|
|
154
|
+
BOOL last = work.offset == work.body.length;
|
|
155
|
+
if (last) [self cancel:identifier];
|
|
156
|
+
return @{@"bodyBase64": encoded, @"last": @(last)};
|
|
157
|
+
}
|
|
158
|
+
- (void)cancel:(NSString *)identifier {
|
|
159
|
+
if (!TFAdmissionValidToken(identifier)) TFDocumentInvalid();
|
|
160
|
+
TFNativeDocumentWork *work = self.work[identifier];
|
|
161
|
+
if (!work) return;
|
|
162
|
+
work->live.store(false);
|
|
163
|
+
[work.timer cancel]; work.timer = nil;
|
|
164
|
+
void (^waiter)(NSDictionary *) = work.waiter; work.waiter = nil;
|
|
165
|
+
[work.body resetBytesInRange:NSMakeRange(0, work.body.length)]; work.body = nil; work.header = nil;
|
|
166
|
+
@try {
|
|
167
|
+
if (!work.nativeFinished) [work.cancellation cancel];
|
|
168
|
+
work.cancellation = nil;
|
|
169
|
+
} @finally {
|
|
170
|
+
if (work.nativeFinished) [self.work removeObjectForKey:identifier];
|
|
171
|
+
if (waiter) waiter(nil);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
- (void)retireQuietly:(NSString *)identifier {
|
|
175
|
+
@try { [self cancel:identifier]; }
|
|
176
|
+
@catch (NSException *exception) { /* Retain failed physical cleanup; continue logical retirement. */ }
|
|
177
|
+
}
|
|
178
|
+
- (void)retireContext:(NSString *)context {
|
|
179
|
+
for (TFNativeDocumentWork *work in self.work.allValues) if ([work.scope.context isEqual:context]) [self retireQuietly:work.identifier];
|
|
180
|
+
}
|
|
181
|
+
- (void)retireProtected {
|
|
182
|
+
for (TFNativeDocumentWork *work in self.work.allValues) if (work.scope) [self retireQuietly:work.identifier];
|
|
183
|
+
}
|
|
184
|
+
- (void)lifecycle:(BOOL)foreground {
|
|
185
|
+
self->_foreground.store(foreground);
|
|
186
|
+
if (!foreground) for (NSString *identifier in self.work.allKeys) [self retireQuietly:identifier];
|
|
187
|
+
}
|
|
188
|
+
- (void)close {
|
|
189
|
+
self->_closed.store(true);
|
|
190
|
+
for (NSString *identifier in self.work.allKeys) [self retireQuietly:identifier];
|
|
191
|
+
}
|
|
192
|
+
- (NSString *)description { return @"TFNativeDocumentRegistry(redacted)"; }
|
|
193
|
+
@end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
// UI-queue confined. Native object identity, not a delayed JS callback, binds frame evidence.
|
|
6
|
+
@interface TFNativeSurfaceState : NSObject
|
|
7
|
+
@property (nonatomic, readonly) BOOL active;
|
|
8
|
+
@property (nonatomic, readonly) NSUInteger pendingCount;
|
|
9
|
+
@property (nonatomic, readonly, nullable) NSNumber *awaitingSequence;
|
|
10
|
+
- (instancetype)initWithSurface:(NSString *)surface emit:(void (^)(NSDictionary *event))emit;
|
|
11
|
+
- (void)expect:(NSString *)token;
|
|
12
|
+
- (void)layout:(BOOL)visible;
|
|
13
|
+
- (void)loaded:(NSString *)token identity:(id)identity;
|
|
14
|
+
- (NSUInteger)commit:(NSString *)token;
|
|
15
|
+
- (void)request:(NSString *)token;
|
|
16
|
+
- (void)frameStart:(nullable id)identity;
|
|
17
|
+
- (void)frameEnd:(nullable id)identity fully:(BOOL)fully;
|
|
18
|
+
- (NSUInteger)beginCommand:(NSUInteger)command;
|
|
19
|
+
- (void)cancelCommand:(NSUInteger)command;
|
|
20
|
+
- (void)gestureStart:(NSDictionary *)view;
|
|
21
|
+
- (void)gestureChange:(NSDictionary *)view;
|
|
22
|
+
- (void)gestureEnd:(NSDictionary *)view;
|
|
23
|
+
- (void)acknowledge:(NSUInteger)sequence;
|
|
24
|
+
- (void)fail;
|
|
25
|
+
- (void)close;
|
|
26
|
+
@end
|
|
27
|
+
|
|
28
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#import "TFNativeSurfaceState.h"
|
|
2
|
+
#import "TFAdmissionEngine.h"
|
|
3
|
+
|
|
4
|
+
@interface TFNativeSurfaceState ()
|
|
5
|
+
@property (nonatomic, copy) NSString *surface;
|
|
6
|
+
@property (nonatomic, copy) NSString *token;
|
|
7
|
+
@property (nonatomic, copy) void (^emit)(NSDictionary *);
|
|
8
|
+
@property (nonatomic, strong, nullable) id style;
|
|
9
|
+
@property (nonatomic, strong, nullable) id frame;
|
|
10
|
+
@property (nonatomic) NSUInteger frameLayout;
|
|
11
|
+
@property (nonatomic) NSUInteger layoutEpoch;
|
|
12
|
+
@property (nonatomic) NSUInteger committed;
|
|
13
|
+
@property (nonatomic) NSUInteger requested;
|
|
14
|
+
@property (nonatomic) NSUInteger sequence;
|
|
15
|
+
@property (nonatomic) NSUInteger command;
|
|
16
|
+
@property (nonatomic) NSUInteger gestureSequence;
|
|
17
|
+
@property (nonatomic) NSUInteger gesture;
|
|
18
|
+
@property (nonatomic) BOOL visible;
|
|
19
|
+
@property (nonatomic) BOOL closed;
|
|
20
|
+
@property (nonatomic) BOOL failed;
|
|
21
|
+
@property (nonatomic) BOOL reported;
|
|
22
|
+
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *pending;
|
|
23
|
+
@property (nonatomic, readwrite, nullable) NSNumber *awaitingSequence;
|
|
24
|
+
- (NSUInteger)enqueue:(NSString *)kind fields:(NSDictionary *)fields;
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation TFNativeSurfaceState
|
|
28
|
+
- (instancetype)initWithSurface:(NSString *)surface emit:(void (^)(NSDictionary *))emit {
|
|
29
|
+
if ((self = [super init])) {
|
|
30
|
+
_surface = [surface copy]; _emit = [emit copy]; _token = @"";
|
|
31
|
+
_layoutEpoch = 1; _pending = [NSMutableArray array];
|
|
32
|
+
}
|
|
33
|
+
return self;
|
|
34
|
+
}
|
|
35
|
+
- (BOOL)active { return !self.closed && !self.failed; }
|
|
36
|
+
- (NSUInteger)pendingCount { return self.pending.count; }
|
|
37
|
+
- (void)expect:(NSString *)token {
|
|
38
|
+
if (self.closed || !TFAdmissionValidToken(token)) [self invalid];
|
|
39
|
+
self.token = token; self.style = nil; self.frame = nil; self.committed = 0;
|
|
40
|
+
self.requested = 0; self.reported = NO; self.failed = NO; self.gesture = 0;
|
|
41
|
+
}
|
|
42
|
+
- (void)layout:(BOOL)visible {
|
|
43
|
+
if (self.closed) return;
|
|
44
|
+
self.visible = visible;
|
|
45
|
+
if (self.layoutEpoch >= 9007199254740991ULL) { [self fail]; return; }
|
|
46
|
+
self.layoutEpoch++; self.committed = 0; self.requested = 0; self.frame = nil;
|
|
47
|
+
self.reported = NO; self.gesture = 0;
|
|
48
|
+
[self enqueue:@"invalidate" fields:@{}];
|
|
49
|
+
}
|
|
50
|
+
- (void)loaded:(NSString *)token identity:(id)identity {
|
|
51
|
+
if (!self.active || ![token isEqual:self.token] || self.style == identity) return;
|
|
52
|
+
self.style = identity; self.requested = 0; self.frame = nil; self.reported = NO;
|
|
53
|
+
[self enqueue:@"style" fields:@{}];
|
|
54
|
+
}
|
|
55
|
+
- (NSUInteger)commit:(NSString *)token {
|
|
56
|
+
if (!self.active || !self.visible || ![token isEqual:self.token] || !self.style) [self invalid];
|
|
57
|
+
self.committed = self.layoutEpoch; self.requested = 0; self.frame = nil; self.reported = NO;
|
|
58
|
+
return self.committed;
|
|
59
|
+
}
|
|
60
|
+
- (void)request:(NSString *)token {
|
|
61
|
+
if (!self.active || !self.visible || ![token isEqual:self.token] || !self.style ||
|
|
62
|
+
self.committed != self.layoutEpoch || self.reported) [self invalid];
|
|
63
|
+
self.requested = self.layoutEpoch; self.frame = nil;
|
|
64
|
+
}
|
|
65
|
+
- (void)frameStart:(id)identity {
|
|
66
|
+
self.frame = self.active && self.visible && identity == self.style && self.committed == self.layoutEpoch && self.requested == self.layoutEpoch ? identity : nil;
|
|
67
|
+
self.frameLayout = self.layoutEpoch;
|
|
68
|
+
}
|
|
69
|
+
- (void)frameEnd:(id)identity fully:(BOOL)fully {
|
|
70
|
+
id captured = self.frame; self.frame = nil;
|
|
71
|
+
if (!self.active || self.reported || !self.visible || !fully || !captured ||
|
|
72
|
+
captured != identity || captured != self.style || self.frameLayout != self.layoutEpoch || self.committed != self.layoutEpoch || self.requested != self.layoutEpoch) return;
|
|
73
|
+
self.reported = YES; self.requested = 0; [self enqueue:@"render" fields:@{}];
|
|
74
|
+
}
|
|
75
|
+
- (NSUInteger)beginCommand:(NSUInteger)command {
|
|
76
|
+
if (!self.active || !self.visible || !self.style || self.gesture || command <= self.command || command > 9007199254740991ULL) return 0;
|
|
77
|
+
self.command = command; self.committed = 0; self.requested = 0; self.frame = nil; self.reported = NO;
|
|
78
|
+
return [self enqueue:@"invalidate" fields:@{}];
|
|
79
|
+
}
|
|
80
|
+
- (void)cancelCommand:(NSUInteger)command { if (command > self.command && command <= 9007199254740991ULL) self.command = command; }
|
|
81
|
+
- (void)gestureStart:(NSDictionary *)view {
|
|
82
|
+
if (!self.active || !self.visible || !self.style || self.gesture) return;
|
|
83
|
+
if (self.gestureSequence >= 9007199254740991ULL) { [self fail]; return; }
|
|
84
|
+
self.requested = 0; self.frame = nil; self.gesture = ++self.gestureSequence;
|
|
85
|
+
[self enqueue:@"gesture-start" fields:@{@"gesture": @(self.gesture), @"view": view}];
|
|
86
|
+
}
|
|
87
|
+
- (void)gestureChange:(NSDictionary *)view {
|
|
88
|
+
if (self.active && self.gesture) [self enqueue:@"gesture-change" fields:@{@"gesture": @(self.gesture), @"view": view}];
|
|
89
|
+
}
|
|
90
|
+
- (void)gestureEnd:(NSDictionary *)view {
|
|
91
|
+
if (!self.active || !self.gesture) return;
|
|
92
|
+
NSUInteger completed = self.gesture; self.gesture = 0;
|
|
93
|
+
[self enqueue:@"gesture-end" fields:@{@"gesture": @(completed), @"view": view}];
|
|
94
|
+
}
|
|
95
|
+
- (void)fail {
|
|
96
|
+
if (self.closed || self.failed) return;
|
|
97
|
+
self.failed = YES; self.committed = 0; self.requested = 0; self.frame = nil; self.gesture = 0;
|
|
98
|
+
[self enqueue:@"error" fields:@{}];
|
|
99
|
+
}
|
|
100
|
+
- (NSUInteger)enqueue:(NSString *)kind fields:(NSDictionary *)fields {
|
|
101
|
+
if (self.closed || !self.token.length || self.sequence >= 9007199254740991ULL) return 0;
|
|
102
|
+
NSDictionary *last = self.pending.lastObject;
|
|
103
|
+
if (([kind isEqual:@"gesture-change"] || [kind isEqual:@"invalidate"]) && [last[@"kind"] isEqual:kind] &&
|
|
104
|
+
[last[@"style"] isEqual:self.token] && (last[@"gesture"] == fields[@"gesture"] || [last[@"gesture"] isEqual:fields[@"gesture"]])) [self.pending removeLastObject];
|
|
105
|
+
BOOL overflow = self.pending.count >= 32;
|
|
106
|
+
if (overflow) {
|
|
107
|
+
[self.pending removeAllObjects]; self.failed = YES; self.frame = nil; self.committed = 0;
|
|
108
|
+
self.requested = 0; self.gesture = 0; kind = @"error"; fields = @{};
|
|
109
|
+
}
|
|
110
|
+
NSUInteger emitted = ++self.sequence;
|
|
111
|
+
NSMutableDictionary *event = [@{@"surface": self.surface, @"style": self.token, @"layout": @(self.layoutEpoch), @"sequence": @(emitted), @"kind": kind} mutableCopy];
|
|
112
|
+
[event addEntriesFromDictionary:fields]; [self.pending addObject:[event copy]]; [self drain];
|
|
113
|
+
return overflow ? 0 : emitted;
|
|
114
|
+
}
|
|
115
|
+
- (void)drain {
|
|
116
|
+
if (self.closed || self.awaitingSequence || !self.pending.count) return;
|
|
117
|
+
NSDictionary *event = self.pending.firstObject; [self.pending removeObjectAtIndex:0];
|
|
118
|
+
self.awaitingSequence = event[@"sequence"];
|
|
119
|
+
@try { self.emit(event); } @catch (NSException *exception) { [self close]; }
|
|
120
|
+
}
|
|
121
|
+
- (void)acknowledge:(NSUInteger)sequence {
|
|
122
|
+
if (!self.awaitingSequence || self.awaitingSequence.unsignedLongLongValue != sequence) return;
|
|
123
|
+
self.awaitingSequence = nil; [self drain];
|
|
124
|
+
}
|
|
125
|
+
- (void)close { self.closed = YES; self.style = nil; self.frame = nil; self.committed = 0; self.requested = 0; self.gesture = 0; [self.pending removeAllObjects]; self.awaitingSequence = nil; }
|
|
126
|
+
- (NSString *)description { return @"TFNativeSurfaceState"; }
|
|
127
|
+
- (void)invalid { @throw [NSException exceptionWithName:@"TFNativeSurfaceInvalid" reason:@"Native surface operation failed." userInfo:nil]; }
|
|
128
|
+
@end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@class TFNativeDocumentScope;
|
|
5
|
+
@class TileflowNativeDocuments;
|
|
6
|
+
|
|
7
|
+
@interface TileflowNativeAdmission : RCTEventEmitter <RCTBridgeModule>
|
|
8
|
+
- (TFNativeDocumentScope *)documentScopeForInstallation:(NSString *)installation context:(NSString *)context documents:(TileflowNativeDocuments *)documents;
|
|
9
|
+
@end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#import "TileflowNativeAdmission.h"
|
|
2
|
+
#import "TFAdmissionInstallation.h"
|
|
3
|
+
#import "TFAdmissionBootstrap.h"
|
|
4
|
+
#import "TFAdmissionDocumentScope.h"
|
|
5
|
+
#import "TFAdmissionNetwork.h"
|
|
6
|
+
#import "TileflowNativeDocuments.h"
|
|
7
|
+
#import <UIKit/UIKit.h>
|
|
8
|
+
#import <cmath>
|
|
9
|
+
|
|
10
|
+
@interface TileflowNativeAdmission ()
|
|
11
|
+
@property (nonatomic, strong, nullable) TFAdmissionInstallation *installation;
|
|
12
|
+
@property (nonatomic, strong, nullable) TFAdmissionBootstrap *bootstrapRequests;
|
|
13
|
+
@property (nonatomic, strong, nullable) TFAdmissionURLSessionNetwork *bootstrapNetwork;
|
|
14
|
+
@property (nonatomic, weak, nullable) TileflowNativeDocuments *documents;
|
|
15
|
+
@property (nonatomic, copy, nullable) NSString *lastRemoved;
|
|
16
|
+
@property (nonatomic, copy, nullable) NSDictionary *lastRemoval;
|
|
17
|
+
@property (nonatomic) BOOL observing;
|
|
18
|
+
@property (nonatomic) BOOL invalidated;
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
@implementation TileflowNativeAdmission
|
|
22
|
+
RCT_EXPORT_MODULE(TileflowNativeAdmission)
|
|
23
|
+
+ (BOOL)requiresMainQueueSetup { return YES; }
|
|
24
|
+
- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
|
|
25
|
+
- (NSArray<NSString *> *)supportedEvents { return @[@"TileflowNativeAdmissionEvent"]; }
|
|
26
|
+
- (void)startObserving { self.observing = YES; }
|
|
27
|
+
- (void)stopObserving { self.observing = NO; }
|
|
28
|
+
- (instancetype)init {
|
|
29
|
+
if ((self = [super init])) {
|
|
30
|
+
NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
|
|
31
|
+
[center addObserver:self selector:@selector(background:) name:UIApplicationWillResignActiveNotification object:nil];
|
|
32
|
+
[center addObserver:self selector:@selector(foreground:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
|
33
|
+
}
|
|
34
|
+
return self;
|
|
35
|
+
}
|
|
36
|
+
- (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self]; }
|
|
37
|
+
|
|
38
|
+
RCT_REMAP_METHOD(install, installWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
39
|
+
BOOL started = NO;
|
|
40
|
+
@try {
|
|
41
|
+
if (self.invalidated || self.installation || !self.observing) [self invalid];
|
|
42
|
+
started = YES;
|
|
43
|
+
__weak TileflowNativeAdmission *weakSelf = self;
|
|
44
|
+
self.installation = [[TFAdmissionInstallation alloc] initWithEmitter:^(NSDictionary *event) {
|
|
45
|
+
TileflowNativeAdmission *strongSelf = weakSelf;
|
|
46
|
+
if ([event[@"kind"] isEqual:@"retired"]) {
|
|
47
|
+
[strongSelf.bootstrapRequests retire:event[@"context"]];
|
|
48
|
+
[strongSelf.documents retireNativeContext:event[@"context"]];
|
|
49
|
+
} else if ([event[@"kind"] isEqual:@"ownershipLost"]) {
|
|
50
|
+
[strongSelf.bootstrapRequests close];
|
|
51
|
+
[strongSelf.documents retireProtectedDocuments];
|
|
52
|
+
} else if ([event[@"kind"] isEqual:@"lifecycle"]) [strongSelf.bootstrapRequests lifecycle:[event[@"foreground"] boolValue]];
|
|
53
|
+
if (!strongSelf || strongSelf.invalidated || !strongSelf.observing) [NSException raise:@"TFNativeAdmissionListener" format:@"Native admission listener is unavailable"];
|
|
54
|
+
[strongSelf sendEventWithName:@"TileflowNativeAdmissionEvent" body:event];
|
|
55
|
+
}];
|
|
56
|
+
self.bootstrapNetwork = [self createBootstrapNetwork];
|
|
57
|
+
if (!self.installation || !self.bootstrapNetwork) [self invalid];
|
|
58
|
+
// The network guard may run off the main queue. Capture its original
|
|
59
|
+
// engine instead of reading the module's mutable installation pointer.
|
|
60
|
+
TFAdmissionEngine *admission = self.installation.engine;
|
|
61
|
+
self.bootstrapRequests = [[TFAdmissionBootstrap alloc] initWithInstallation:self.installation.identifier scheduler:[TFContinuousAdmissionScheduler new]
|
|
62
|
+
network:self.bootstrapNetwork owns:^BOOL { return [admission isOwner]; }];
|
|
63
|
+
if (!self.bootstrapRequests) [self invalid];
|
|
64
|
+
[admission lifecycle:UIApplication.sharedApplication.applicationState == UIApplicationStateActive];
|
|
65
|
+
if (![admission isOwner]) [self invalid];
|
|
66
|
+
resolve(@{@"installation": self.installation.identifier});
|
|
67
|
+
} @catch (NSException *exception) {
|
|
68
|
+
// Do not roll back an existing installation rejected by the guard.
|
|
69
|
+
if (started) {
|
|
70
|
+
@try { [self shutdown]; } @catch (NSException *cleanupError) { /* No successful acknowledgement. */ }
|
|
71
|
+
}
|
|
72
|
+
[self reject:reject];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
- (TFAdmissionURLSessionNetwork *)createBootstrapNetwork {
|
|
76
|
+
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
|
77
|
+
configuration.protocolClasses = @[];
|
|
78
|
+
configuration.URLCache = nil; configuration.HTTPCookieStorage = nil; configuration.URLCredentialStorage = nil;
|
|
79
|
+
configuration.HTTPShouldSetCookies = NO; configuration.HTTPAdditionalHeaders = nil;
|
|
80
|
+
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
81
|
+
configuration.HTTPMaximumConnectionsPerHost = 4;
|
|
82
|
+
return [[TFAdmissionURLSessionNetwork alloc] initWithConfiguration:configuration followsRedirects:NO responseByteLimit:65536 queueDepth:32 concurrency:4];
|
|
83
|
+
}
|
|
84
|
+
RCT_REMAP_METHOD(registerContext, registerInstallation:(NSString *)identifier registration:(NSDictionary *)registration resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
85
|
+
@try {
|
|
86
|
+
TFAdmissionInstallation *installation = [self current:identifier];
|
|
87
|
+
if (![registration isKindOfClass:NSDictionary.class] || registration.count > 2) [self invalid];
|
|
88
|
+
NSString *mapId = registration[@"mapId"] == NSNull.null ? nil : registration[@"mapId"];
|
|
89
|
+
NSArray *resources = registration[@"resources"];
|
|
90
|
+
if (![resources isKindOfClass:NSArray.class] || resources.count > 128) [self invalid];
|
|
91
|
+
NSString *context = [installation.engine registerMap:mapId resources:resources];
|
|
92
|
+
@try {
|
|
93
|
+
if (!self.bootstrapRequests) [self invalid];
|
|
94
|
+
[self.bootstrapRequests registerContext:context mapId:mapId];
|
|
95
|
+
} @catch (NSException *exception) { [installation.engine retire:context]; @throw; }
|
|
96
|
+
resolve(@{@"context": context, @"generation": @1});
|
|
97
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
98
|
+
}
|
|
99
|
+
RCT_REMAP_METHOD(extendContext, extendInstallation:(NSString *)identifier context:(NSString *)context resources:(NSArray<NSDictionary *> *)resources resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
100
|
+
@try {
|
|
101
|
+
TFAdmissionInstallation *installation = [self current:identifier];
|
|
102
|
+
if (!TFAdmissionValidToken(context) || ![resources isKindOfClass:NSArray.class] || resources.count > 128) [self invalid];
|
|
103
|
+
resolve(@{@"resources": @([installation.engine extendContext:context resources:resources])});
|
|
104
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
105
|
+
}
|
|
106
|
+
- (TFNativeDocumentScope *)documentScopeForInstallation:(NSString *)identifier context:(NSString *)context documents:(TileflowNativeDocuments *)documents {
|
|
107
|
+
if (!documents || !TFAdmissionValidToken(context)) [self invalid];
|
|
108
|
+
TFAdmissionInstallation *installation = [self current:identifier];
|
|
109
|
+
if (self.documents && self.documents != documents) [self invalid];
|
|
110
|
+
self.documents = documents;
|
|
111
|
+
return TFCreateAdmissionDocumentScope(installation.engine, context);
|
|
112
|
+
}
|
|
113
|
+
RCT_REMAP_METHOD(retireContext, retireInstallation:(NSString *)identifier context:(NSString *)context resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
114
|
+
@try {
|
|
115
|
+
if (!TFAdmissionValidToken(context)) [self invalid];
|
|
116
|
+
if ([self.installation.identifier isEqual:identifier]) {
|
|
117
|
+
[self.bootstrapRequests retire:context]; [self.installation.engine retire:context];
|
|
118
|
+
} else if (![self.lastRemoved isEqual:identifier]) [self invalid];
|
|
119
|
+
resolve(@{@"retired": @YES});
|
|
120
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
121
|
+
}
|
|
122
|
+
RCT_REMAP_METHOD(bootstrap, bootstrapInstallation:(NSString *)identifier context:(NSString *)context request:(NSString *)request url:(NSString *)url credential:(NSString *)credential body:(NSString *)body resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
123
|
+
@try {
|
|
124
|
+
[self current:identifier];
|
|
125
|
+
if (!self.bootstrapRequests) [self invalid];
|
|
126
|
+
[self.bootstrapRequests startContext:context request:request url:url credential:credential body:body completion:^(NSDictionary *reply) {
|
|
127
|
+
if (reply) resolve(reply);
|
|
128
|
+
else reject(@"NATIVE_ADMISSION_UNAVAILABLE", @"Native resource admission failed", nil);
|
|
129
|
+
}];
|
|
130
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
131
|
+
}
|
|
132
|
+
RCT_REMAP_METHOD(cancelBootstrap, cancelBootstrapInstallation:(NSString *)identifier context:(NSString *)context request:(NSString *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
133
|
+
@try {
|
|
134
|
+
if (!TFAdmissionValidToken(context) || !TFAdmissionValidToken(request)) [self invalid];
|
|
135
|
+
if ([self.installation.identifier isEqual:identifier]) [self.bootstrapRequests cancelContext:context request:request];
|
|
136
|
+
else if (![self.lastRemoved isEqual:identifier]) [self invalid];
|
|
137
|
+
resolve(@{@"cancelled": @YES});
|
|
138
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
139
|
+
}
|
|
140
|
+
RCT_REMAP_METHOD(completeBatch, completeInstallation:(NSString *)identifier context:(NSString *)context generation:(double)generation batch:(NSString *)batch results:(NSArray<NSDictionary *> *)results resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
141
|
+
@try {
|
|
142
|
+
TFAdmissionInstallation *installation = [self current:identifier];
|
|
143
|
+
if (!TFAdmissionValidToken(context) || !TFAdmissionValidToken(batch) || generation != 1.0 || ![results isKindOfClass:NSArray.class] || results.count > 8) [self invalid];
|
|
144
|
+
// Bound the envelope before the engine holds any authority. This data
|
|
145
|
+
// is ephemeral bridge input, never a log, event, error or snapshot.
|
|
146
|
+
if (![NSJSONSerialization isValidJSONObject:results]) [self invalid];
|
|
147
|
+
NSData *wire = [NSJSONSerialization dataWithJSONObject:results options:0 error:nil];
|
|
148
|
+
if (!wire || wire.length > 524288) [self invalid];
|
|
149
|
+
NSUInteger accepted = [installation.engine completeContext:context generation:1 batch:batch results:results];
|
|
150
|
+
resolve(@{@"accepted": @(accepted)});
|
|
151
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
152
|
+
}
|
|
153
|
+
RCT_REMAP_METHOD(remove, removeInstallation:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
154
|
+
@try {
|
|
155
|
+
if (!self.installation && [self.lastRemoved isEqual:identifier]) { resolve(self.lastRemoval); return; }
|
|
156
|
+
if (![self.installation.identifier isEqual:identifier]) [self invalid];
|
|
157
|
+
resolve([self shutdown]);
|
|
158
|
+
} @catch (NSException *exception) { [self reject:reject]; }
|
|
159
|
+
}
|
|
160
|
+
- (NSDictionary *)shutdown {
|
|
161
|
+
if (!self.installation) return self.lastRemoval ?: @{@"removed": @NO, @"ownershipLost": @NO};
|
|
162
|
+
[self.bootstrapRequests close]; [self.bootstrapNetwork close];
|
|
163
|
+
NSDictionary *ack = [self.installation remove];
|
|
164
|
+
self.lastRemoved = self.installation.identifier; self.lastRemoval = ack;
|
|
165
|
+
self.installation = nil; self.bootstrapRequests = nil; self.bootstrapNetwork = nil;
|
|
166
|
+
return ack;
|
|
167
|
+
}
|
|
168
|
+
- (TFAdmissionInstallation *)current:(NSString *)identifier {
|
|
169
|
+
if (self.invalidated || ![self.installation.identifier isEqual:identifier] || ![self.installation.engine isOwner]) [self invalid];
|
|
170
|
+
return self.installation;
|
|
171
|
+
}
|
|
172
|
+
- (void)background:(NSNotification *)notification { [self.installation.engine lifecycle:NO]; }
|
|
173
|
+
- (void)foreground:(NSNotification *)notification { [self.installation.engine lifecycle:YES]; }
|
|
174
|
+
- (void)invalidate {
|
|
175
|
+
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
176
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
177
|
+
self.invalidated = YES;
|
|
178
|
+
@try { [self shutdown]; } @catch (NSException *exception) { /* Retain failed cleanup for retry. */ }
|
|
179
|
+
});
|
|
180
|
+
[super invalidate];
|
|
181
|
+
}
|
|
182
|
+
- (void)reject:(RCTPromiseRejectBlock)reject { reject(@"NATIVE_ADMISSION_UNAVAILABLE", @"Native resource admission failed", nil); }
|
|
183
|
+
- (void)invalid { [NSException raise:@"TFNativeAdmissionInvalid" format:@"Invalid native admission"]; }
|
|
184
|
+
@end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTInvalidating.h>
|
|
3
|
+
#import "TFMobileConfiguration.h"
|
|
4
|
+
|
|
5
|
+
@interface TileflowNativeConfiguration : NSObject <RCTBridgeModule, RCTInvalidating>
|
|
6
|
+
@end
|
|
7
|
+
|
|
8
|
+
@implementation TileflowNativeConfiguration {
|
|
9
|
+
BOOL _invalidated;
|
|
10
|
+
}
|
|
11
|
+
RCT_EXPORT_MODULE(TileflowNativeConfiguration)
|
|
12
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
13
|
+
|
|
14
|
+
RCT_REMAP_METHOD(readConfiguration,
|
|
15
|
+
readConfigurationWithResolver:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
17
|
+
@synchronized (self) {
|
|
18
|
+
if (_invalidated) {
|
|
19
|
+
reject(@"NATIVE_CONFIGURATION_UNAVAILABLE", @"Native application configuration is unavailable.", nil);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
static TFMobileConfigurationCache *applicationConfiguration;
|
|
23
|
+
static dispatch_once_t once;
|
|
24
|
+
dispatch_once(&once, ^{ applicationConfiguration = [TFMobileConfigurationCache new]; });
|
|
25
|
+
TFMobileConfiguration *configuration = [applicationConfiguration read:^{
|
|
26
|
+
NSBundle *bundle = NSBundle.mainBundle;
|
|
27
|
+
return [TFMobileConfiguration fromInfo:bundle.infoDictionary localized:bundle.localizedInfoDictionary error:nil];
|
|
28
|
+
} error:nil];
|
|
29
|
+
if (!configuration) {
|
|
30
|
+
reject(@"NATIVE_CONFIGURATION_INVALID", @"Native application configuration is invalid.", nil);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// One bounded private promise, never constants, notifications or renderer events.
|
|
34
|
+
resolve(@{@"apiOrigin": configuration.apiOrigin, @"credential": configuration.credential});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (void)invalidate {
|
|
39
|
+
@synchronized (self) { _invalidated = YES; }
|
|
40
|
+
}
|
|
41
|
+
@end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTInvalidating.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
// Private preparation channel. Methods are serialized with admission on the main queue.
|
|
7
|
+
@interface TileflowNativeDocuments : NSObject <RCTBridgeModule, RCTInvalidating>
|
|
8
|
+
- (void)retireNativeContext:(NSString *)context;
|
|
9
|
+
- (void)retireProtectedDocuments;
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
NS_ASSUME_NONNULL_END
|