├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE ├── README.md ├── SWWebView ├── .swiftlint.yml ├── Default-568h@2x.png ├── SWWebView-JSTestSuite │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── ViewController.swift │ └── js-tests │ │ └── tests.js ├── SWWebView.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── SWWebView.xcworkspace │ └── contents.xcworkspacedata ├── SWWebView │ ├── Bridge │ │ └── SWWebViewBridge.swift │ ├── CommandBridge │ │ ├── EventStream.swift │ │ ├── MessagePortHandler.swift │ │ ├── Messaging │ │ │ ├── MessagePortAction.swift │ │ │ └── MessagePortWrapper.swift │ │ ├── ServiceWorkerCommands.swift │ │ ├── ServiceWorkerContainerCommands.swift │ │ └── ServiceWorkerRegistrationCommands.swift │ ├── Info.plist │ ├── Interop │ │ ├── SWURLSchemeTaskOutputPipe.swift │ │ ├── SWWebViewContainerDelegate.swift │ │ └── SWWebViewCoordinator.swift │ ├── SWWebView.h │ ├── SWWebView.swift │ ├── SWWebViewNavigationDelegate.swift │ ├── SubstituteWKComponents │ │ ├── SWNavigationAction.swift │ │ ├── SWNavigationResponse.swift │ │ └── SWURLSchemeTask.swift │ ├── ToJSONExtensions │ │ ├── ServiceWorker.swift │ │ ├── ServiceWorkerContainer.swift │ │ ├── ServiceWorkerRegistration.swift │ │ ├── ToJSON.swift │ │ └── WorkerInstallationError.swift │ └── js-dist │ │ ├── runtime.js │ │ └── runtime.js.map ├── SWWebViewTests │ └── js-tests │ │ └── tests.js └── js-src │ ├── .gobble │ └── 02-include │ │ └── 1 │ │ ├── mocha.css │ │ └── mocha.js │ ├── .vscode │ └── settings.json │ ├── gobblefile.js │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── rollup.test.config.js │ ├── src │ ├── boot.ts │ ├── components │ │ ├── service-worker-container.ts │ │ ├── service-worker-registration.ts │ │ └── service-worker.ts │ ├── definitions │ │ ├── eventsource.d.ts │ │ ├── eventtarget.d.ts │ │ └── swwebview-settings.d.ts │ ├── enum │ │ └── enums.ts │ ├── event-stream.ts │ ├── fetch-graft.ts │ ├── handlers │ │ ├── messageport-manager.ts │ │ └── transferrable-converter.ts │ ├── responses │ │ └── api-responses.ts │ └── util │ │ ├── api-request.ts │ │ ├── console-interceptor.js │ │ └── streaming-xhr.ts │ ├── tests │ ├── app-only │ │ ├── fetch-grafts.ts │ │ └── http-hooks.ts │ ├── fixtures │ │ ├── blank.html │ │ ├── cache-file.txt │ │ ├── cache-file2.txt │ │ ├── exec-worker.js │ │ ├── script-to-import.js │ │ ├── script-to-import2.js │ │ ├── subscope │ │ │ └── blank.html │ │ ├── test-message-reply-worker.js │ │ ├── test-register-worker.js │ │ ├── test-response-worker.js │ │ ├── test-take-control-worker.js │ │ └── test-worker-that-fails.js │ ├── test-bootstrap.ts │ ├── tests.d.ts │ ├── tests.html │ ├── universal │ │ ├── cache-storage.ts │ │ ├── cache.ts │ │ ├── console-interceptor.ts │ │ ├── service-worker-container.ts │ │ └── service-worker.ts │ └── util │ │ ├── exec-in-worker.ts │ │ ├── sw-lifecycle.ts │ │ ├── unregister-everything.ts │ │ └── with-iframe.ts │ └── tsconfig.json ├── ServiceWorker ├── .swiftlint.yml ├── ServiceWorker.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── ServiceWorker │ ├── DatabaseMigration │ │ └── Migration.swift │ ├── Events │ │ ├── ConstructableEvent.swift │ │ ├── Event.swift │ │ ├── EventListenerProtocol.swift │ │ ├── EventTarget.swift │ │ ├── ExtendableEvent.swift │ │ ├── FetchEvent.swift │ │ ├── JSEventListener.swift │ │ └── SwiftEventListener.swift │ ├── ExecutionEnvironment │ │ ├── ConsoleMirror.swift │ │ ├── EvaluateScriptCall.swift │ │ ├── ServiceWorkerExecutionEnvironment.swift │ │ └── TimeoutManager.swift │ ├── Fetch │ │ ├── FetchCORSRestrictions.swift │ │ ├── FetchHeaders.swift │ │ ├── FetchRequest.swift │ │ ├── FetchResponse.swift │ │ ├── FetchSession.swift │ │ ├── FetchTask.swift │ │ ├── HttpStatusCodes.swift │ │ └── Response │ │ │ ├── FetchResponseProtocol.swift │ │ │ ├── FetchResponseProxy.swift │ │ │ └── ResponseType.swift │ ├── GlobalEventLog │ │ └── GlobalEventLog.swift │ ├── GlobalScope │ │ ├── Clients │ │ │ ├── Client.swift │ │ │ ├── Clients.swift │ │ │ └── WindowClient.swift │ │ ├── Location │ │ │ ├── JSURL.swift │ │ │ ├── LocationBase.swift │ │ │ ├── URLSearchParams.swift │ │ │ └── WorkerLocation.swift │ │ ├── ServiceWorkerGlobalScope.swift │ │ └── ServiceWorkerGlobalScopeDelegate.swift │ ├── Info.plist │ ├── Interop │ │ ├── Cache │ │ │ ├── Cache.swift │ │ │ ├── CacheMatchOptions.swift │ │ │ ├── CacheStorage.swift │ │ │ └── CacheStorageProviderDelegate.swift │ │ ├── Clients │ │ │ ├── ClientMatchAllOptions.swift │ │ │ ├── ClientProtocol.swift │ │ │ ├── ClientType.swift │ │ │ ├── ServiceWorkerClientsDelegate.swift │ │ │ ├── WindowClientProtocol.swift │ │ │ └── WindowClientVisibilityState.swift │ │ ├── Registration │ │ │ └── ServiceWorkerRegistrationProtocol.swift │ │ └── ServiceWorkerDelegate.swift │ ├── JS │ │ ├── GlobalVariableProvider.swift │ │ ├── JSArrayBuffer.swift │ │ ├── JSArrayBufferStream.swift │ │ ├── JSContextPromise.swift │ │ └── PromiseToJSPromise.swift │ ├── Messaging │ │ ├── ExtendableMessageEvent.swift │ │ ├── MessageChannel.swift │ │ ├── MessagePort.swift │ │ ├── MessagePortTarget.swift │ │ └── Transferable.swift │ ├── SQLite │ │ ├── SQLiteBlobReadStream.swift │ │ ├── SQLiteBlobStreamPointer.swift │ │ ├── SQLiteBlobWriteStream.swift │ │ ├── SQLiteConnection.swift │ │ ├── SQLiteDataType.swift │ │ └── SQLiteResultSet.swift │ ├── ServiceWorker.h │ ├── ServiceWorker.swift │ ├── ServiceWorkerInstallState.swift │ ├── Stream │ │ ├── InputStreamImplementation.swift │ │ ├── OutputStreamImplementation.swift │ │ ├── PassthroughStream.swift │ │ └── StreamPipe.swift │ ├── Util │ │ ├── ErrorMessage.swift │ │ ├── PromiseFulfillExtensions.swift │ │ └── SharedLogInterface.swift │ ├── WebSQL │ │ ├── WebSQLDatabase.swift │ │ ├── WebSQLResultRows.swift │ │ ├── WebSQLResultSet.swift │ │ └── WebSQLTransaction.swift │ └── js-src │ │ ├── package.json │ │ └── yarn.lock ├── ServiceWorkerTests │ ├── Bootstrap.swift │ ├── Cache │ │ └── CacheTests.swift │ ├── Clients │ │ └── ClientsTests.swift │ ├── DatabaseMigration │ │ └── Migrate.swift │ ├── Events │ │ ├── EventTargetTests.swift │ │ ├── ExtendableEventTests.swift │ │ └── FetchEventTests.swift │ ├── Fetch │ │ ├── ConstructableFetchResponseTests.swift │ │ ├── FetchHeadersTests.swift │ │ ├── FetchOperation.swift │ │ ├── FetchOperationCORS.swift │ │ ├── FetchPerformance.swift │ │ ├── FetchRequestTests.swift │ │ └── FetchResponse.swift │ ├── GlobalScope │ │ ├── ConsoleMirrorTests.swift │ │ ├── ExecutionTests.swift │ │ ├── GlobalScopeTests.swift │ │ ├── ImportScriptsTests.swift │ │ ├── TimeoutTests.swift │ │ └── URLTests.swift │ ├── IndexedDB │ │ └── IndexedDBTests.swift │ ├── Info.plist │ ├── JS │ │ └── JSPromise.swift │ ├── Messaging │ │ ├── MessageChannelTests.swift │ │ └── MessagePortTests.swift │ ├── PerformanceTests.swift │ ├── SQLiteTests.swift │ ├── ServiceWorkerTests.swift │ ├── Stream │ │ ├── PassthroughStream.swift │ │ └── StreamPipeTests.swift │ ├── WebSQL │ │ └── WebSQLConnection.swift │ └── ZZZZ_TestEndChecks.swift ├── js-dist │ └── indexeddbshim.js └── js-src │ ├── package-lock.json │ └── package.json ├── ServiceWorkerContainer ├── .swiftlint.yml ├── ServiceWorkerContainer.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── ServiceWorkerContainer.xcworkspace │ └── contents.xcworkspacedata ├── ServiceWorkerContainer │ ├── Cache │ │ ├── SQLiteCache.swift │ │ └── SQLiteCacheStorage.swift │ ├── DatabaseMigrations │ │ ├── cache │ │ │ ├── 1_initial_cache_table.sql │ │ │ └── 2_cache_entries_table.sql │ │ └── core │ │ │ ├── 1_worker_tables.sql │ │ │ ├── 2_imported_scripts.sql │ │ │ └── 3_add_content_hashes.sql │ ├── Events │ │ └── WorkerInstallationError.swift │ ├── Factories │ │ ├── WorkerFactory.swift │ │ └── WorkerRegistrationFactory.swift │ ├── Info.plist │ ├── Interop │ │ └── ServiceWorkerStorageProvider.swift │ ├── Registration │ │ ├── RegistrationWorkerSlots.swift │ │ ├── ServiceWorkerRegistration.swift │ │ └── ServiceWorkerRegistrationOptions.swift │ ├── ServiceWorkerContainer.h │ ├── ServiceWorkerContainer.swift │ └── Storage │ │ ├── CoreDatabase.swift │ │ ├── DBConnectionPool.swift │ │ ├── DatabaseType.swift │ │ └── SharedResources.swift └── ServiceWorkerContainerTests │ ├── Bootstrap.swift │ ├── Info.plist │ ├── Interop │ └── ImportScriptsTests.swift │ ├── ServiceWorkerContainerTests.swift │ └── ServiceWorkerRegistrationTests.swift └── TestUtilities ├── GlobalContextMessingAround.swift ├── PromiseAssert.swift ├── ServiceWorkerContainerExtensions.swift ├── ServiceWorkerExtensions.swift └── TestWeb.swift /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | Carthage 3 | node_modules 4 | .DS_Store 5 | .rpt2_cache 6 | .gobble 7 | .gobble-build 8 | 9 | ### Xcode ### 10 | build/ 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | *.xccheckout 21 | *.moved-aside 22 | DerivedData 23 | *.xcuserstate 24 | 25 | 26 | ### Objective-C ### 27 | # Xcode 28 | # 29 | build/ 30 | *.pbxuser 31 | !default.pbxuser 32 | *.mode1v3 33 | !default.mode1v3 34 | *.mode2v3 35 | !default.mode2v3 36 | *.perspectivev3 37 | !default.perspectivev3 38 | xcuserdata 39 | *.xccheckout 40 | *.moved-aside 41 | DerivedData 42 | *.hmap 43 | *.ipa 44 | *.xcuserstate 45 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "mxcl/PromiseKit" ~> 4.4 2 | github "swisspol/GCDWebServer" ~> 3.2.5 3 | github "1024jp/GzipSwift" "swift4" 4 | github "ccgus/fmdb" 5 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "1024jp/GzipSwift" "72e16b884600b98ac77a871daecaea34aec87f8e" 2 | github "ccgus/fmdb" "2.7.2" 3 | github "mxcl/PromiseKit" "4.4.0" 4 | github "swisspol/GCDWebServer" "3.4.1" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Guardian Mobile Innovation Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SWWebView/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - identifier_name 3 | - line_length 4 | opt_in_rules: 5 | - force_unwrapping 6 | included: 7 | - SWWebView 8 | #force_unwrapping: error 9 | -------------------------------------------------------------------------------- /SWWebView/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gdnmobilelab/SWWebView/cab7bd6b0c7a6a5d9a610133f71f418322e42e0f/SWWebView/Default-568h@2x.png -------------------------------------------------------------------------------- /SWWebView/SWWebView-JSTestSuite/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | var controller: ViewController? 8 | 9 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 10 | // Override point for customization after application launch. 11 | 12 | self.window = UIWindow(frame: UIScreen.main.bounds) 13 | self.controller = ViewController() 14 | self.window!.rootViewController = self.controller 15 | self.window!.makeKeyAndVisible() 16 | return true 17 | } 18 | 19 | func applicationWillResignActive(_: UIApplication) { 20 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 21 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 22 | } 23 | 24 | func applicationDidEnterBackground(_: UIApplication) { 25 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 26 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 27 | } 28 | 29 | func applicationWillEnterForeground(_: UIApplication) { 30 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 31 | } 32 | 33 | func applicationDidBecomeActive(_: UIApplication) { 34 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 35 | } 36 | 37 | func applicationWillTerminate(_: UIApplication) { 38 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SWWebView/SWWebView-JSTestSuite/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SWWebView/SWWebView-JSTestSuite/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SWWebView/SWWebView-JSTestSuite/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UIRequiredDeviceCapabilities 29 | 30 | armv7 31 | 32 | UISupportedInterfaceOrientations 33 | 34 | UIInterfaceOrientationPortrait 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /SWWebView/SWWebView-JSTestSuite/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SWWebView 3 | import WebKit 4 | import GCDWebServers 5 | import ServiceWorkerContainer 6 | import ServiceWorker 7 | import PromiseKit 8 | 9 | class ViewController: UIViewController { 10 | 11 | var coordinator: SWWebViewCoordinator? 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | self.addStubs() 17 | let config = WKWebViewConfiguration() 18 | 19 | Log.info = { NSLog($0) } 20 | 21 | let storageURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) 22 | .appendingPathComponent("testapp-db", isDirectory: true) 23 | 24 | do { 25 | if FileManager.default.fileExists(atPath: storageURL.path) { 26 | try FileManager.default.removeItem(at: storageURL) 27 | } 28 | try FileManager.default.createDirectory(at: storageURL, withIntermediateDirectories: true, attributes: nil) 29 | } catch { 30 | fatalError() 31 | } 32 | 33 | self.coordinator = SWWebViewCoordinator(storageURL: storageURL) 34 | 35 | let swView = SWWebView(frame: self.view.frame, configuration: config) 36 | // This will move to a delegate method eventually 37 | swView.serviceWorkerPermittedDomains.append("localhost:4567") 38 | swView.containerDelegate = self.coordinator! 39 | self.view.addSubview(swView) 40 | 41 | var url = URLComponents(string: "sw://localhost:4567/tests.html")! 42 | URLCache.shared.removeAllCachedResponses() 43 | NSLog("Loading \(url.url!.absoluteString)") 44 | swView.load(URLRequest(url: url.url!)) 45 | } 46 | 47 | func addStubs() { 48 | SWWebViewBridge.routes["/ping"] = { _, _ in 49 | 50 | Promise(value: [ 51 | "pong": true 52 | ]) 53 | } 54 | 55 | SWWebViewBridge.routes["/ping-with-body"] = { _, json in 56 | 57 | var responseText = json?["value"] as? String ?? "no body found" 58 | 59 | return Promise(value: [ 60 | "pong": responseText 61 | ]) 62 | } 63 | } 64 | 65 | override func didReceiveMemoryWarning() { 66 | super.didReceiveMemoryWarning() 67 | // Dispose of any resources that can be recreated. 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SWWebView/SWWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SWWebView/SWWebView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/CommandBridge/MessagePortHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | import ServiceWorker 4 | import PromiseKit 5 | 6 | class MessagePortHandler { 7 | 8 | static func proxyMessage(eventStream: EventStream, json: AnyObject?) throws -> Promise { 9 | 10 | return firstly { () -> Promise in 11 | 12 | guard let portID = json?["id"] as? String else { 13 | throw ErrorMessage("No MessagePort ID provided") 14 | } 15 | 16 | guard let message = json?["message"] as AnyObject? else { 17 | throw ErrorMessage("No message provided") 18 | } 19 | 20 | guard let savedPort = MessagePortWrapper.activePorts.first(where: { $0.eventStream == eventStream && $0.id == portID }) else { 21 | throw ErrorMessage("No MessagePort with this ID") 22 | } 23 | 24 | savedPort.port?.postMessage(message) 25 | 26 | return Promise(value: nil) 27 | } 28 | } 29 | 30 | // static func map(transferables: [AnyObject], in container: ServiceWorkerContainer) throws -> [SWMessagePort] { 31 | // 32 | // return try transferables.map { item in 33 | // 34 | // guard let serializedInfo = item["__hybridSerialized"] as? [String: AnyObject] else { 35 | // throw ErrorMessage("This does not appear to be a serialized object") 36 | // } 37 | // 38 | // if serializedInfo["type"] as? String != "MessagePort" { 39 | // throw ErrorMessage("Only support MessagePort transfers right now") 40 | // } 41 | // 42 | // guard let portID = serializedInfo["id"] as? String else { 43 | // throw ErrorMessage("Did not provide a MessagePort ID") 44 | // } 45 | // 46 | // guard let existing = MessagePortWrapper.activePorts.first(where: { $0.eventStream.container == container && $0.id == portID })?.port else { 47 | // throw ErrorMessage("The specified port does not exist") 48 | // } 49 | // 50 | // return existing 51 | // } 52 | // } 53 | } 54 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/CommandBridge/Messaging/MessagePortAction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MessagePortAction: ToJSON { 4 | 5 | enum ActionType: String { 6 | case message 7 | case close 8 | } 9 | 10 | let type: ActionType 11 | let data: Any? 12 | let id: String 13 | let portIDs: [String] 14 | 15 | init(type: ActionType, id: String, data: Any?, portIds: [String] = []) { 16 | self.type = type 17 | self.data = data 18 | self.id = id 19 | self.portIDs = portIds 20 | } 21 | 22 | func toJSONSuitableObject() -> Any { 23 | return [ 24 | "type": self.type.rawValue, 25 | "data": self.data, 26 | "id": self.id, 27 | "portIDs": self.portIDs 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/CommandBridge/Messaging/MessagePortWrapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | 4 | class MessagePortWrapper: NSObject, MessagePortTarget { 5 | 6 | // We keep a reference to this because the MessagePort itself only 7 | // has a weak reference to this as its target. We keep a strong 8 | // reference until the port has been closed, at which point we remove it. 9 | static var activePorts = Set() 10 | 11 | let id: String 12 | let eventStream: EventStream 13 | weak var port: SWMessagePort? 14 | 15 | let started = true 16 | 17 | func start() { 18 | // We don't implement this. 19 | } 20 | 21 | func close() { 22 | MessagePortWrapper.activePorts.remove(self) 23 | let close = MessagePortAction(type: .close, id: self.id, data: nil) 24 | self.eventStream.sendUpdate(identifier: "messageport", object: close) 25 | } 26 | 27 | init(_ port: SWMessagePort, in eventStream: EventStream) { 28 | self.id = UUID().uuidString 29 | self.eventStream = eventStream 30 | super.init() 31 | port.targetPort = self 32 | port.start() 33 | self.port = port 34 | MessagePortWrapper.activePorts.insert(self) 35 | } 36 | 37 | func receiveMessage(_ evt: ExtendableMessageEvent) { 38 | 39 | // A MessagePort message can, in turn, send its own ports. If that's happened 40 | // we need to create new wrappers for these ports too. 41 | 42 | let wrappedPorts = evt.ports.map { MessagePortWrapper($0, in: self.eventStream).id } 43 | 44 | let message = MessagePortAction(type: .message, id: self.id, data: evt.data, portIds: wrappedPorts) 45 | 46 | self.eventStream.sendUpdate(identifier: "messageport", object: message) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/CommandBridge/ServiceWorkerContainerCommands.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | import ServiceWorkerContainer 4 | import ServiceWorker 5 | import PromiseKit 6 | 7 | class ServiceWorkerContainerCommands { 8 | 9 | static func getRegistration(eventStream: EventStream, json: AnyObject?) throws -> Promise? { 10 | 11 | var scope: URL? 12 | if let scopeString = json?["scope"] as? String { 13 | 14 | guard let specifiedScope = URL(string: scopeString) else { 15 | throw ErrorMessage("Did not understand passed in scope argument") 16 | } 17 | 18 | scope = specifiedScope 19 | } 20 | 21 | return eventStream.container.getRegistration(scope) 22 | .then { reg in 23 | reg?.toJSONSuitableObject() 24 | } 25 | } 26 | 27 | static func getRegistrations(eventStream: EventStream, json _: AnyObject?) throws -> Promise? { 28 | return eventStream.container.getRegistrations() 29 | .then { regs in 30 | regs.map { $0.toJSONSuitableObject() } 31 | } 32 | } 33 | 34 | static func register(eventStream: EventStream, json: AnyObject?) throws -> Promise? { 35 | 36 | guard let workerURLString = json?["url"] as? String else { 37 | throw ErrorMessage("URL must be provided") 38 | } 39 | 40 | guard let workerURL = URL(string: workerURLString, relativeTo: eventStream.container.url) else { 41 | throw ErrorMessage("Could not parse URL") 42 | } 43 | 44 | var options: ServiceWorkerRegistrationOptions? 45 | 46 | if let specifiedScope = json?["scope"] as? String { 47 | 48 | guard let specifiedScopeURL = URL(string: specifiedScope, relativeTo: eventStream.container.url) else { 49 | throw ErrorMessage("Could not parse scope URL") 50 | } 51 | options = ServiceWorkerRegistrationOptions(scope: specifiedScopeURL) 52 | } 53 | 54 | return eventStream.container.register(workerURL: workerURL, options: options) 55 | .then { result in 56 | result.toJSONSuitableObject() 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/CommandBridge/ServiceWorkerRegistrationCommands.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | import ServiceWorkerContainer 4 | import ServiceWorker 5 | import PromiseKit 6 | 7 | class ServiceWorkerRegistrationCommands { 8 | 9 | static func unregister(eventStream: EventStream, json: AnyObject?) throws -> Promise? { 10 | 11 | guard let registrationID = json?["id"] as? String else { 12 | throw ErrorMessage("Must provide registration ID in JSON body") 13 | } 14 | 15 | return eventStream.container.getRegistrations() 16 | .then { registrations in 17 | 18 | guard let registration = registrations.first(where: { $0.id == registrationID }) else { 19 | throw ErrorMessage("Registration does not exist") 20 | } 21 | 22 | return registration.unregister() 23 | } 24 | .then { 25 | [ 26 | "success": true 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/Interop/SWURLSchemeTaskOutputPipe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | 4 | class SWURLSchemeTaskOutputStream: OutputStreamImplementation { 5 | 6 | let task: SWURLSchemeTask 7 | let statusCode: Int 8 | let headers: [String: String] 9 | 10 | init(task: SWURLSchemeTask, statusCode: Int, headers: [String: String]) throws { 11 | 12 | self.task = task 13 | self.statusCode = statusCode 14 | self.headers = headers 15 | 16 | super.init(toMemory: ()) 17 | } 18 | 19 | convenience init(task: SWURLSchemeTask, response: FetchResponseProtocol) throws { 20 | 21 | var headers: [String: String] = [:] 22 | response.headers.keys().forEach { key in 23 | if let value = response.headers.get(key) { 24 | headers[key] = value 25 | } 26 | } 27 | 28 | try self.init(task: task, statusCode: response.status, headers: headers) 29 | } 30 | 31 | override func write(_ buffer: UnsafePointer, maxLength len: Int) -> Int { 32 | let data = Data(bytes: buffer, count: len) 33 | 34 | do { 35 | try self.task.didReceive(data) 36 | return len 37 | } catch { 38 | self.throwError(error) 39 | return -1 40 | } 41 | } 42 | 43 | override var hasSpaceAvailable: Bool { 44 | return true 45 | } 46 | 47 | override func open() { 48 | do { 49 | try self.task.didReceiveHeaders(statusCode: self.statusCode, headers: self.headers) 50 | self.emitEvent(event: .openCompleted) 51 | self.emitEvent(event: .hasSpaceAvailable) 52 | } catch { 53 | self.throwError(error) 54 | } 55 | } 56 | 57 | override func close() { 58 | do { 59 | try self.task.didFinish() 60 | } catch { 61 | self.throwError(error) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/Interop/SWWebViewContainerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | import ServiceWorker 4 | 5 | @objc public protocol SWWebViewContainerDelegate { 6 | 7 | @objc func container(_: SWWebView, getContainerFor: URL) -> ServiceWorkerContainer? 8 | @objc func container(_: SWWebView, createContainerFor: URL) throws -> ServiceWorkerContainer 9 | @objc func container(_: SWWebView, freeContainer: ServiceWorkerContainer) 10 | } 11 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/SWWebView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SWWebView.h 3 | // SWWebView 4 | // 5 | // Created by alastair.coote on 07/08/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SWWebView. 12 | FOUNDATION_EXPORT double SWWebViewVersionNumber; 13 | 14 | //! Project version string for SWWebView. 15 | FOUNDATION_EXPORT const unsigned char SWWebViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/SubstituteWKComponents/SWNavigationAction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | 4 | /// We can't manually create WKNavigationActions, so instead we have to do this 5 | class SWNavigationAction: WKNavigationAction { 6 | 7 | fileprivate let _request: URLRequest 8 | override var request: URLRequest { 9 | return self._request 10 | } 11 | 12 | fileprivate let _sourceFrame: WKFrameInfo 13 | override var sourceFrame: WKFrameInfo { 14 | return self._sourceFrame 15 | } 16 | 17 | fileprivate let _targetFrame: WKFrameInfo? 18 | override var targetFrame: WKFrameInfo? { 19 | return self._targetFrame 20 | } 21 | 22 | fileprivate let _navigationType: WKNavigationType 23 | override var navigationType: WKNavigationType { 24 | return self._navigationType 25 | } 26 | 27 | init(request: URLRequest, sourceFrame: WKFrameInfo, targetFrame: WKFrameInfo?, navigationType: WKNavigationType) { 28 | self._request = request 29 | self._sourceFrame = sourceFrame 30 | self._targetFrame = targetFrame 31 | self._navigationType = navigationType 32 | super.init() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/SubstituteWKComponents/SWNavigationResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WebKit 3 | 4 | class SWNavigationResponse: WKNavigationResponse { 5 | 6 | fileprivate let _canShowMIMEType: Bool 7 | override var canShowMIMEType: Bool { 8 | return self._canShowMIMEType 9 | } 10 | 11 | fileprivate let _isForMainFrame: Bool 12 | override var isForMainFrame: Bool { 13 | return self._isForMainFrame 14 | } 15 | 16 | fileprivate let _response: URLResponse 17 | override var response: URLResponse { 18 | return self._response 19 | } 20 | 21 | init(response: URLResponse, isForMainFrame: Bool, canShowMIMEType: Bool) { 22 | self._response = response 23 | self._isForMainFrame = isForMainFrame 24 | self._canShowMIMEType = canShowMIMEType 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/ToJSONExtensions/ServiceWorker.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | 4 | extension ServiceWorker: ToJSON { 5 | 6 | func toJSONSuitableObject() -> Any { 7 | return [ 8 | "id": self.id, 9 | "installState": self.state.rawValue, 10 | "scriptURL": self.url.sWWebviewSuitableAbsoluteString 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/ToJSONExtensions/ServiceWorkerContainer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | 4 | extension ServiceWorkerContainer: ToJSON { 5 | func toJSONSuitableObject() -> Any { 6 | return [ 7 | "readyRegistration": (self.readyRegistration as? ServiceWorkerRegistration)?.toJSONSuitableObject(), 8 | "controller": self.controller?.toJSONSuitableObject() 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/ToJSONExtensions/ServiceWorkerRegistration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | 4 | extension ServiceWorkerRegistration: ToJSON { 5 | func toJSONSuitableObject() -> Any { 6 | 7 | return [ 8 | "id": self.id, 9 | "scope": self.scope.sWWebviewSuitableAbsoluteString, 10 | "active": self.active?.toJSONSuitableObject(), 11 | "waiting": self.waiting?.toJSONSuitableObject(), 12 | "installing": self.installing?.toJSONSuitableObject(), 13 | "redundant": self.redundant?.toJSONSuitableObject(), 14 | "unregistered": self.unregistered 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/ToJSONExtensions/ToJSON.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol ToJSON { 4 | func toJSONSuitableObject() -> Any 5 | } 6 | 7 | extension URL { 8 | var sWWebviewSuitableAbsoluteString: String? { 9 | guard var components = URLComponents(url: self, resolvingAgainstBaseURL: true) else { 10 | return nil 11 | } 12 | components.scheme = SWWebView.ServiceWorkerScheme 13 | 14 | return components.url?.absoluteString ?? nil 15 | } 16 | 17 | init?(swWebViewString: String) { 18 | guard var urlComponents = URLComponents(string: swWebViewString) else { 19 | return nil 20 | } 21 | 22 | urlComponents.scheme = urlComponents.host == "localhost" ? "http" : "https" 23 | 24 | if let url = urlComponents.url { 25 | self = url 26 | } else { 27 | return nil 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SWWebView/SWWebView/ToJSONExtensions/WorkerInstallationError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | 4 | extension WorkerInstallationError: ToJSON { 5 | 6 | func toJSONSuitableObject() -> Any { 7 | return [ 8 | "error": String(describing: self.error), 9 | "worker": self.worker.toJSONSuitableObject() 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SWWebView/js-src/.gobble/02-include/1/mocha.css: -------------------------------------------------------------------------------- 1 | /Users/alastair.coote/Projects/hybrid-reboot/SWWebView/js-src/node_modules/mocha/mocha.css -------------------------------------------------------------------------------- /SWWebView/js-src/.gobble/02-include/1/mocha.js: -------------------------------------------------------------------------------- 1 | /Users/alastair.coote/Projects/hybrid-reboot/SWWebView/js-src/node_modules/mocha/mocha.js -------------------------------------------------------------------------------- /SWWebView/js-src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "prettier.tabWidth": 4 4 | } -------------------------------------------------------------------------------- /SWWebView/js-src/gobblefile.js: -------------------------------------------------------------------------------- 1 | const gobble = require("gobble"); 2 | const rollupConfig = require("./rollup.config"); 3 | const rollupTestConfig = require("./rollup.test.config"); 4 | 5 | rollupConfig.entry = "boot.ts"; 6 | rollupConfig.dest = "lib.js"; 7 | rollupConfig.banner = `;if (window.swwebviewSettings) {`; 8 | rollupConfig.footer = ";};"; 9 | //livereload doesn't seem to work otherwise? 10 | rollupConfig.format = "iife"; 11 | 12 | module.exports = gobble([ 13 | gobble("tests").include(["tests.html", "fixtures/**"]), 14 | gobble("node_modules/mocha").include(["mocha.js", "mocha.css"]), 15 | gobble("src").transform("rollup", rollupConfig), 16 | gobble("tests").transform("rollup", rollupTestConfig) 17 | ]); 18 | -------------------------------------------------------------------------------- /SWWebView/js-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swwebview-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rollup -c -i ./src/boot.ts -o ../SWWebView/js-dist/runtime.js", 8 | "build-tests": "rollup -c rollup.test.config.js -i ./tests/test-bootstrap.ts -o ../SWWebView-JSTestSuite/js-tests/tests.js", 9 | "test-watch": "gobble" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@types/chai": "^4.0.2", 15 | "chai": "^4.1.1", 16 | "event-target": "^0.1.0", 17 | "eventtarget": "^0.1.0", 18 | "gobble-cli": "^0.8.0", 19 | "rollup": "^0.45.2", 20 | "rollup-plugin-commonjs": "^8.1.0", 21 | "rollup-plugin-node-resolve": "^3.0.0", 22 | "rollup-plugin-typescript": "^0.8.1", 23 | "rollup-plugin-typescript2": "^0.5.0", 24 | "rollup-watch": "^4.3.1", 25 | "tiny-emitter": "git+https://github.com/alastaircoote/tiny-emitter-w3cish.git", 26 | "typescript": "^2.4.2" 27 | }, 28 | "devDependencies": { 29 | "@types/expect": "^1.20.1", 30 | "@types/mocha": "^2.2.41", 31 | "assert": "^1.4.1", 32 | "expect": "^1.20.2", 33 | "expect.js": "^0.3.1", 34 | "gobble-rollup": "^0.40.0", 35 | "mocha": "^3.5.0", 36 | "recursive-readdir-sync": "^1.0.6", 37 | "rollup-plugin-replace": "^1.1.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SWWebView/js-src/rollup.config.js: -------------------------------------------------------------------------------- 1 | const typescriptPlugin = require("rollup-plugin-typescript"); 2 | const commonjs = require("rollup-plugin-commonjs"); 3 | const nodeResolve = require("rollup-plugin-node-resolve"); 4 | const typescript = require("typescript"); 5 | 6 | module.exports = { 7 | format: "iife", 8 | plugins: [ 9 | typescriptPlugin({ 10 | typescript: typescript, 11 | include: [__dirname + "/**/*.ts", __dirname + "/.gobble/**/*.ts"] 12 | }), 13 | commonjs({ 14 | namedExports: { 15 | chai: ["assert"], 16 | "tiny-emitter": ["EventEmitter"] 17 | } 18 | }), 19 | nodeResolve({ 20 | browser: true, 21 | preferBuiltins: false 22 | }) 23 | ], 24 | sourceMap: true, 25 | moduleName: "swwebview", 26 | external: ["swwebview-settings"], 27 | globals: { 28 | "swwebview-settings": "swwebviewSettings" 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /SWWebView/js-src/rollup.test.config.js: -------------------------------------------------------------------------------- 1 | let baseConfig = Object.assign({}, require("./rollup.config.js")); 2 | const fs = require("fs"); 3 | const recursiveReadSync = require("recursive-readdir-sync"); 4 | const path = require("path"); 5 | const replace = require("rollup-plugin-replace"); 6 | 7 | function loadAllTests() { 8 | return { 9 | name: "TestLoader", 10 | load: id => { 11 | if (id !== "all-tests") { 12 | return; 13 | } 14 | 15 | let allTests = recursiveReadSync( 16 | path.join(__dirname, "tests", "app-only") 17 | ) 18 | .concat( 19 | recursiveReadSync( 20 | path.join(__dirname, "tests", "universal") 21 | ) 22 | ) 23 | .filter(file => path.extname(file) === ".ts"); 24 | 25 | let imports = allTests 26 | .map((filename, idx) => `import '${filename}';`) 27 | .join("\n"); 28 | 29 | return imports; 30 | }, 31 | resolveId: function(imported, importee) { 32 | if (imported === "all-tests") { 33 | return "all-tests"; 34 | } 35 | } 36 | }; 37 | } 38 | baseConfig.entry = "test-bootstrap.ts"; 39 | baseConfig.dest = "tests.js"; 40 | baseConfig.plugins.push(loadAllTests()); 41 | baseConfig.format = "umd"; 42 | 43 | module.exports = baseConfig; 44 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/boot.ts: -------------------------------------------------------------------------------- 1 | import "./fetch-graft"; 2 | import { apiRequest } from "./util/api-request"; 3 | import "./components/service-worker-container"; 4 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/components/service-worker-registration.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "tiny-emitter"; 2 | import { ServiceWorkerRegistrationAPIResponse } from "../responses/api-responses"; 3 | import { apiRequest } from "../util/api-request"; 4 | import { BooleanSuccessResponse } from "../responses/api-responses"; 5 | import { eventStream } from "../event-stream"; 6 | import { ServiceWorkerImplementation } from "./service-worker"; 7 | 8 | const existingRegistrations: ServiceWorkerRegistrationImplementation[] = []; 9 | 10 | export class ServiceWorkerRegistrationImplementation extends EventEmitter 11 | implements ServiceWorkerRegistration { 12 | active: ServiceWorker | null; 13 | installing: ServiceWorker | null; 14 | waiting: ServiceWorker | null; 15 | 16 | pushManager: PushManager; 17 | scope: string; 18 | id: string; 19 | sync: SyncManager; 20 | 21 | constructor(opts: ServiceWorkerRegistrationAPIResponse) { 22 | super(); 23 | this.scope = opts.scope; 24 | this.id = opts.id; 25 | this.updateFromResponse(opts); 26 | } 27 | 28 | static getOrCreate(opts: ServiceWorkerRegistrationAPIResponse) { 29 | let registration = existingRegistrations.find(reg => reg.id == opts.id); 30 | if (!registration) { 31 | if (opts.unregistered === true) { 32 | throw new Error( 33 | "Trying to create an unregistered registration" 34 | ); 35 | } 36 | console.info("Creating new registration:", opts.id, opts); 37 | registration = new ServiceWorkerRegistrationImplementation(opts); 38 | existingRegistrations.push(registration); 39 | } 40 | return registration; 41 | } 42 | 43 | updateFromResponse(opts: ServiceWorkerRegistrationAPIResponse) { 44 | if (opts.unregistered === true) { 45 | console.info("Removing inactive registration:", opts.id); 46 | // Remove from our array of existing registrations, as we don't 47 | // want to refer to this again. 48 | let idx = existingRegistrations.indexOf(this); 49 | existingRegistrations.splice(idx, 1); 50 | return; 51 | } 52 | 53 | this.active = opts.active 54 | ? ServiceWorkerImplementation.getOrCreate(opts.active, this) 55 | : null; 56 | this.installing = opts.installing 57 | ? ServiceWorkerImplementation.getOrCreate(opts.installing, this) 58 | : null; 59 | this.waiting = opts.waiting 60 | ? ServiceWorkerImplementation.getOrCreate(opts.waiting, this) 61 | : null; 62 | } 63 | 64 | onupdatefound: () => void; 65 | 66 | getNotifications() { 67 | throw new Error("not yet"); 68 | } 69 | 70 | showNotification( 71 | title: string, 72 | options?: NotificationOptions 73 | ): Promise { 74 | throw new Error("not yet"); 75 | } 76 | 77 | unregister(): Promise { 78 | return apiRequest< 79 | BooleanSuccessResponse 80 | >("/ServiceWorkerRegistration/unregister", { 81 | id: this.id 82 | }).then(response => { 83 | return response.success; 84 | }); 85 | } 86 | 87 | update(): Promise { 88 | throw new Error("not yet"); 89 | } 90 | } 91 | 92 | eventStream.addEventListener< 93 | ServiceWorkerRegistrationAPIResponse 94 | >("serviceworkerregistration", e => { 95 | console.log("reg update", e.data); 96 | let reg = existingRegistrations.find(r => r.id == e.data.id); 97 | if (reg) { 98 | reg.updateFromResponse(e.data); 99 | } else { 100 | console.info( 101 | "Received update for non-existent registration", 102 | e.data.id 103 | ); 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/components/service-worker.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "tiny-emitter"; 2 | import { ServiceWorkerRegistrationImplementation } from "./service-worker-registration"; 3 | import { 4 | ServiceWorkerAPIResponse, 5 | WorkerInstallErrorAPIResponse, 6 | PostMessageResponse 7 | } from "../responses/api-responses"; 8 | import { eventStream } from "../event-stream"; 9 | import { apiRequest } from "../util/api-request"; 10 | import { serializeTransferables } from "../handlers/transferrable-converter"; 11 | import { addProxy } from "../handlers/messageport-manager"; 12 | 13 | const existingWorkers: ServiceWorkerImplementation[] = []; 14 | 15 | export class ServiceWorkerImplementation extends EventEmitter 16 | implements ServiceWorker { 17 | id: string; 18 | scriptURL: string; 19 | state: ServiceWorkerState; 20 | onstatechange: (e) => void; 21 | onerror: (Error) => void; 22 | 23 | private registration: ServiceWorkerRegistrationImplementation; 24 | 25 | constructor( 26 | opts: ServiceWorkerAPIResponse, 27 | registration: ServiceWorkerRegistrationImplementation 28 | ) { 29 | super(); 30 | this.updateFromAPIResponse(opts); 31 | this.registration = registration; 32 | this.id = opts.id; 33 | 34 | this.addEventListener("statechange", e => { 35 | if (this.onstatechange) { 36 | this.onstatechange(e); 37 | } 38 | }); 39 | } 40 | 41 | updateFromAPIResponse(opts: ServiceWorkerAPIResponse) { 42 | this.scriptURL = opts.scriptURL; 43 | let oldState = this.state; 44 | this.state = opts.installState; 45 | 46 | if (oldState !== this.state) { 47 | let evt = new CustomEvent("statechange"); 48 | this.dispatchEvent(evt); 49 | } 50 | } 51 | 52 | postMessage(msg: any, transfer: any[] = []) { 53 | apiRequest("/ServiceWorker/postMessage", { 54 | id: this.id, 55 | registrationID: this.registration.id, 56 | message: serializeTransferables(msg, transfer), 57 | transferCount: transfer.length 58 | }).then(response => { 59 | // Register MessagePort proxies for all the transferables we just sent. 60 | response.transferred.forEach((id, idx) => 61 | addProxy(transfer[idx], id) 62 | ); 63 | }); 64 | } 65 | 66 | static get(opts: ServiceWorkerAPIResponse) { 67 | return existingWorkers.find(w => w.id === opts.id); 68 | } 69 | 70 | static getOrCreate( 71 | opts: ServiceWorkerAPIResponse, 72 | registration: ServiceWorkerRegistrationImplementation 73 | ) { 74 | let existing = this.get(opts); 75 | if (existing) { 76 | return existing; 77 | } else { 78 | let newWorker = new ServiceWorkerImplementation(opts, registration); 79 | existingWorkers.push(newWorker); 80 | return newWorker; 81 | } 82 | } 83 | } 84 | 85 | eventStream.addEventListener("serviceworker", e => { 86 | let existingWorker = ServiceWorkerImplementation.get(e.data); 87 | console.info("Worker update:", e.data); 88 | if (existingWorker) { 89 | existingWorker.updateFromAPIResponse(e.data); 90 | } 91 | }); 92 | 93 | eventStream.addEventListener< 94 | WorkerInstallErrorAPIResponse 95 | >("workerinstallerror", e => { 96 | console.error( 97 | `Worker installation failed: ${e.data.error} (in ${e.data.worker 98 | .scriptURL})` 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/definitions/eventsource.d.ts: -------------------------------------------------------------------------------- 1 | declare class EventSource extends EventTarget { 2 | constructor(url: string); 3 | } 4 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/definitions/eventtarget.d.ts: -------------------------------------------------------------------------------- 1 | declare module "event-target" { 2 | class EventTarget { 3 | addEventListener(type: string, listener: (T) => void): void; 4 | removeEventListener(type: string, listener: (T) => void): void; 5 | dispatchEvent(ev: Event); 6 | } 7 | export default EventTarget; 8 | } 9 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/definitions/swwebview-settings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "swwebview-settings" { 2 | interface SWWebViewSettings { 3 | API_REQUEST_METHOD: string; 4 | SW_PROTOCOL: string; 5 | GRAFTED_REQUEST_HEADER: string; 6 | EVENT_STREAM_PATH: string; 7 | } 8 | 9 | var settings: SWWebViewSettings; 10 | export = settings; 11 | } 12 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/enum/enums.ts: -------------------------------------------------------------------------------- 1 | export enum ServiceWorkerInstallState { 2 | downloading = 0, 3 | installing = 1, 4 | installed = 2, 5 | activating = 3, 6 | activated = 4, 7 | redundant = 5 8 | } 9 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/event-stream.ts: -------------------------------------------------------------------------------- 1 | import { StreamingXHR } from "./util/streaming-xhr"; 2 | import { EVENT_STREAM_PATH } from "swwebview-settings"; 3 | 4 | let eventsURL = new URL(EVENT_STREAM_PATH, window.location.href); 5 | eventsURL.searchParams.append( 6 | "path", 7 | window.location.pathname + window.location.search 8 | ); 9 | 10 | export const eventStream = new StreamingXHR(eventsURL.href); 11 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/fetch-graft.ts: -------------------------------------------------------------------------------- 1 | import { SW_PROTOCOL, GRAFTED_REQUEST_HEADER } from "swwebview-settings"; 2 | 3 | // We can't read POST bodies in native code, so we're doing the super-gross: 4 | // putting it in a custom header. Hoping we can get rid of this nonsense soon. 5 | 6 | const originalFetch = fetch; 7 | 8 | function graftedFetch(request: RequestInfo, opts?: RequestInit) { 9 | if (!opts || !opts.body) { 10 | // no body, so none of this matters 11 | return originalFetch(request, opts); 12 | } 13 | 14 | let url = request instanceof Request ? request.url : request; 15 | let resolvedURL = new URL(url, window.location.href); 16 | 17 | if (resolvedURL.protocol !== SW_PROTOCOL + ":") { 18 | // if we're not fetching on the SW protocol, then this 19 | // doesn't matter. 20 | return originalFetch(request, opts); 21 | } 22 | 23 | opts.headers = opts.headers || {}; 24 | opts.headers[GRAFTED_REQUEST_HEADER] = opts.body; 25 | 26 | return originalFetch(request, opts); 27 | } 28 | 29 | (graftedFetch as any).__bodyGrafted = true; 30 | 31 | if ((originalFetch as any).__bodyGrafted !== true) { 32 | (window as any).fetch = graftedFetch; 33 | 34 | const originalSend = XMLHttpRequest.prototype.send; 35 | const originalOpen = XMLHttpRequest.prototype.open; 36 | 37 | XMLHttpRequest.prototype.open = function(method, url) { 38 | let resolvedURL = new URL(url, window.location.href); 39 | if (resolvedURL.protocol === SW_PROTOCOL + ":") { 40 | this._graftBody = true; 41 | } 42 | originalOpen.apply(this, arguments); 43 | }; 44 | 45 | XMLHttpRequest.prototype.send = function(data) { 46 | if (data && this._graftBody === true) { 47 | this.setRequestHeader(GRAFTED_REQUEST_HEADER, data); 48 | } 49 | originalSend.apply(this, arguments); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/handlers/messageport-manager.ts: -------------------------------------------------------------------------------- 1 | import { apiRequest } from "../util/api-request"; 2 | import { eventStream } from "../event-stream"; 3 | import { MessagePortAction } from "../responses/api-responses"; 4 | import { serializeTransferables } from "./transferrable-converter"; 5 | 6 | class MessagePortProxy { 7 | port: MessagePort; 8 | id: string; 9 | 10 | constructor(port: MessagePort, id: string) { 11 | this.port = port; 12 | this.id = id; 13 | this.port.addEventListener("message", this.receiveMessage.bind(this)); 14 | this.port.start(); 15 | } 16 | 17 | receiveMessage(e: MessageEvent) { 18 | console.log("! GOT MESSAGE", e); 19 | apiRequest("/MessagePort/proxyMessage", { 20 | id: this.id, 21 | message: e.data 22 | }).catch(err => { 23 | console.error("Failed to proxy MessagePort message", err); 24 | }); 25 | } 26 | } 27 | 28 | let currentProxies = new Map(); 29 | 30 | export function addProxy(port: MessagePort, id: string) { 31 | currentProxies.set(id, new MessagePortProxy(port, id)); 32 | } 33 | 34 | eventStream.addEventListener("messageport", e => { 35 | // Any message we receive must be associated with an existing port proxy 36 | 37 | let existingProxy = currentProxies.get(e.data.id); 38 | if (!existingProxy) { 39 | throw new Error( 40 | `Tried to send ${e.data.type} to MessagePort that does not exist` 41 | ); 42 | } 43 | 44 | if (e.data.type == "message") { 45 | // A message can send along new MessagePorts of its own, so if it has, 46 | // we need to map the wrapper IDs sent through with new MessagePorts. 47 | 48 | let ports = e.data.portIDs.map(id => { 49 | let channel = new MessageChannel(); 50 | addProxy(channel.port2, id); 51 | return channel.port1; 52 | }); 53 | 54 | existingProxy.port.postMessage(e.data.data, ports); 55 | } else { 56 | // is close. Remove from collection, free up for garbage collection. 57 | console.info( 58 | "Closing existing MessagePort based on native garbage collection." 59 | ); 60 | currentProxies.delete(e.data.id); 61 | existingProxy.port.close(); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/handlers/transferrable-converter.ts: -------------------------------------------------------------------------------- 1 | export function serializeTransferables(message: any, transferables: any[]) { 2 | // Messages can pass transferables, but those transferables can also exist 3 | // within the message. We need to replace any instances of those transferables 4 | // with serializable objects. 5 | 6 | if (transferables.indexOf(message) > -1) { 7 | return { 8 | __transferable: { 9 | index: transferables.indexOf(message) 10 | } 11 | }; 12 | } else if (message instanceof Array) { 13 | return message.map(m => serializeTransferables(m, transferables)); 14 | } else if ( 15 | typeof message == "number" || 16 | typeof message == "string" || 17 | typeof message == "boolean" 18 | ) { 19 | return message; 20 | } else { 21 | let obj = {}; 22 | Object.keys(message).forEach(key => { 23 | obj[key] = serializeTransferables(message[key], transferables); 24 | }); 25 | return obj; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/responses/api-responses.ts: -------------------------------------------------------------------------------- 1 | export interface ServiceWorkerAPIResponse { 2 | id: string; 3 | scriptURL: string; 4 | installState: ServiceWorkerState; 5 | } 6 | 7 | export interface ServiceWorkerContainerAPIResponse { 8 | readyRegistration?: ServiceWorkerRegistrationAPIResponse; 9 | controller?: ServiceWorkerAPIResponse; 10 | } 11 | 12 | export interface ServiceWorkerRegistrationAPIResponse { 13 | scope: string; 14 | id: string; 15 | unregistered: boolean; 16 | active?: ServiceWorkerAPIResponse; 17 | waiting?: ServiceWorkerAPIResponse; 18 | installing?: ServiceWorkerAPIResponse; 19 | redundant?: ServiceWorkerAPIResponse; 20 | } 21 | 22 | export interface BooleanSuccessResponse { 23 | success: boolean; 24 | } 25 | 26 | export interface WorkerInstallErrorAPIResponse { 27 | error: string; 28 | worker: ServiceWorkerAPIResponse; 29 | } 30 | 31 | export interface PostMessageResponse { 32 | transferred: string[]; 33 | } 34 | 35 | export interface MessagePortAction { 36 | id: string; 37 | type: "message" | "close"; 38 | data: any; 39 | portIDs: string[]; 40 | } 41 | 42 | export interface PromiseReturn { 43 | promiseIndex: number; 44 | error?: string; 45 | response?: any; 46 | } 47 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/util/api-request.ts: -------------------------------------------------------------------------------- 1 | import { API_REQUEST_METHOD } from "swwebview-settings"; 2 | import { eventStream } from "../event-stream"; 3 | import { PromiseReturn } from "../responses/api-responses"; 4 | 5 | export class APIError extends Error { 6 | response: Response; 7 | 8 | constructor(message: string, response: Response) { 9 | super(message); 10 | this.response = response; 11 | } 12 | } 13 | 14 | interface StoredPromise { 15 | fulfill: (any) => void; 16 | reject: (Error) => void; 17 | } 18 | 19 | let storedPromises = new Map(); 20 | 21 | export function apiRequest(path: string, body: any = undefined): Promise { 22 | return eventStream.ready.then(() => { 23 | return new Promise((fulfill, reject) => { 24 | let i = 0; 25 | while (storedPromises.has(i)) { 26 | i++; 27 | } 28 | storedPromises.set(i, { fulfill, reject }); 29 | 30 | (window as any).webkit.messageHandlers["SWWebView"].postMessage({ 31 | streamID: eventStream.id, 32 | promiseIndex: i, 33 | path: path, 34 | body: body 35 | }); 36 | }); 37 | 38 | // return fetch(path, { 39 | // method: API_REQUEST_METHOD, 40 | // body: body === undefined ? undefined : JSON.stringify(body), 41 | // headers: { 42 | // "Content-Type": "application/json" 43 | // } 44 | // }); 45 | }); 46 | // .then(res => { 47 | // if (res.ok === false) { 48 | // if (res.status === 500) { 49 | // return res.json().then(errorJSON => { 50 | // throw new Error(errorJSON.error); 51 | // }); 52 | // } 53 | // throw new APIError( 54 | // "Received a non-200 response to API request", 55 | // res 56 | // ); 57 | // } 58 | // return res.json(); 59 | // }); 60 | } 61 | 62 | eventStream.addEventListener("promisereturn", e => { 63 | let promise = storedPromises.get(e.data.promiseIndex); 64 | if (!promise) { 65 | throw new Error("Trying to resolve a Promise that doesn't exist"); 66 | } 67 | storedPromises.delete(e.data.promiseIndex); 68 | if (e.data.error) { 69 | promise.reject(new Error(e.data.error)); 70 | } 71 | promise.fulfill(e.data.response); 72 | }); 73 | -------------------------------------------------------------------------------- /SWWebView/js-src/src/util/console-interceptor.js: -------------------------------------------------------------------------------- 1 | // This isn't actually included in the final JS bundle - instead the code is used in 2 | // the ServiceWorker bundle to mirror console messages in the native log. But we 3 | // include it here so we can run some tests on it. 4 | 5 | export default function(funcToCall) { 6 | let levels = ["debug", "info", "warn", "error", "log"]; 7 | let originalConsole = console; 8 | 9 | let levelProxy = { 10 | apply: function(target, thisArg, argumentsList) { 11 | // send to original console logging function 12 | target.apply(thisArg, argumentsList); 13 | 14 | let level = levels.find(l => originalConsole[l] == target); 15 | 16 | funcToCall(level, argumentsList); 17 | } 18 | }; 19 | 20 | let interceptors = levels.map( 21 | l => new Proxy(originalConsole[l], levelProxy) 22 | ); 23 | 24 | return new Proxy(originalConsole, { 25 | get: function(target, name) { 26 | let idx = levels.indexOf(name); 27 | if (idx === -1) { 28 | // not intercepted 29 | return target[name]; 30 | } 31 | return interceptors[idx]; 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/app-only/fetch-grafts.ts: -------------------------------------------------------------------------------- 1 | // import { assert } from "chai"; 2 | // import { API_REQUEST_METHOD } from "swwebview-settings"; 3 | // import { describeIfApp } from "../test-bootstrap"; 4 | // import { eventStream } from "../../src/event-stream"; 5 | 6 | // describeIfApp("Fetch grafts", () => { 7 | // xit("Grafts fetch bodies", () => { 8 | // return eventStream.ready 9 | // .then(() => { 10 | // return fetch("/ping-with-body", { 11 | // method: API_REQUEST_METHOD, 12 | // body: JSON.stringify({ value: "test-string" }) 13 | // }); 14 | // }) 15 | // .then(res => res.json()) 16 | // .then(json => { 17 | // assert.equal(json.pong, "test-string"); 18 | // }); 19 | // }); 20 | 21 | // xit("Grafts XMLHttpRequest bodies", done => { 22 | // return eventStream.ready.then(() => { 23 | // var xhttp = new XMLHttpRequest(); 24 | // xhttp.onreadystatechange = function() { 25 | // if (this.readyState == 4 && this.status == 200) { 26 | // try { 27 | // let data = JSON.parse(this.responseText); 28 | // assert.equal(data.pong, "test-string"); 29 | // } catch (error) { 30 | // done(error); 31 | // } 32 | // done(); 33 | // } 34 | // }; 35 | // xhttp.open( 36 | // API_REQUEST_METHOD, 37 | // getFullAPIURL("/ping-with-body"), 38 | // true 39 | // ); 40 | // xhttp.send(JSON.stringify({ value: "test-string" })); 41 | // }); 42 | // }); 43 | // }); 44 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/app-only/http-hooks.ts: -------------------------------------------------------------------------------- 1 | // import { apiRequest, APIError } from "../../src/util/api-request"; 2 | // import { assert } from "chai"; 3 | // import { StreamingXHR } from "../../src/util/streaming-xhr"; 4 | // import { describeIfApp } from "../test-bootstrap"; 5 | // import { API_REQUEST_METHOD, EVENT_STREAM_PATH } from "swwebview-settings"; 6 | 7 | // describeIfApp("Basic HTTP hooks for Service Worker API", () => { 8 | // it("Returns 404 when trying to access a command we don't know", () => { 9 | // return apiRequest("/does_not_exist").catch((error: APIError) => { 10 | // assert.equal(error.message, "Route not found"); 11 | // }); 12 | // }); 13 | 14 | // it("Returns JSON response when URL is known", () => { 15 | // return apiRequest("/ping").then(json => { 16 | // assert.equal(json.pong, true); 17 | // }); 18 | // }); 19 | 20 | // it("Can use a streaming XHR request", function(done) { 21 | // let stream = new StreamingXHR(EVENT_STREAM_PATH + "?path=/"); 22 | // stream.open(); 23 | // stream.addEventListener( 24 | // "serviceworkercontainer", 25 | // (ev: MessageEvent) => { 26 | // assert.equal(ev.data.readyRegistration, null); 27 | // stream.close(); 28 | // done(); 29 | // } 30 | // ); 31 | // // stream.addEventListener("test-event2", (ev: MessageEvent) => { 32 | // // assert.equal(ev.data.test, "hello2"); 33 | // // assert.equal(receivedFirstEvent, true); 34 | // // stream.close(); 35 | // // done(); 36 | // // }); 37 | // // stream.addEventListener("error", (ev: ErrorEvent) => { 38 | // // done(ev.error); 39 | // // }); 40 | // }); 41 | // }); 42 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/blank.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/cache-file.txt: -------------------------------------------------------------------------------- 1 | this is cached content -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/cache-file2.txt: -------------------------------------------------------------------------------- 1 | this is the second cached file -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/exec-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("message", e => { 2 | if (e.data.action === "exec") { 3 | let func = new Function(e.data.js); 4 | e.waitUntil( 5 | Promise.resolve() 6 | .then(() => { 7 | // allows us to catch errors 8 | return Promise.resolve(func()); 9 | }) 10 | .then(response => { 11 | e.ports[0].postMessage({ response }); 12 | }) 13 | .catch(error => { 14 | e.ports[0].postMessage({ error: error.message }); 15 | }) 16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/script-to-import.js: -------------------------------------------------------------------------------- 1 | self.testValue = "set"; 2 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/script-to-import2.js: -------------------------------------------------------------------------------- 1 | self.testValue = "set again"; 2 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/subscope/blank.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/test-message-reply-worker.js: -------------------------------------------------------------------------------- 1 | function sendReply(e) { 2 | e.target.postMessage("response2"); 3 | e.target.onmessage = undefined; 4 | } 5 | 6 | self.addEventListener("message", e => { 7 | let newChannel = new MessageChannel(); 8 | newChannel.port2.onmessage = sendReply; 9 | e.ports[0].postMessage("response", [newChannel.port1]); 10 | }); 11 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/test-register-worker.js: -------------------------------------------------------------------------------- 1 | // self.addEventListener("install", e => { 2 | // e.waitUntil( 3 | // new Promise((fulfill, reject) => { 4 | // // small delay so we can check different install states 5 | // setTimeout(fulfill, 1000); 6 | // }) 7 | // ); 8 | // }); 9 | console.info("Registered"); 10 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/test-response-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("fetch", e => { 2 | let requestURL = new URL(e.request.url); 3 | // console.log(requestURL.searchParams.); 4 | 5 | let responseJSON = { 6 | success: true, 7 | queryValue: requestURL.searchParams.get("test") 8 | }; 9 | 10 | let response = new Response(JSON.stringify(responseJSON), { 11 | headers: { 12 | "content-type": "application/json" 13 | } 14 | }); 15 | 16 | e.respondWith(response); 17 | }); 18 | 19 | self.addEventListener("install", e => { 20 | self.skipWaiting(); 21 | }); 22 | 23 | self.addEventListener("activate", e => { 24 | e.waitUntil(self.clients.claim()); 25 | }); 26 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/test-take-control-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("activate", e => { 2 | e.waitUntil(self.clients.claim()); 3 | }); 4 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/fixtures/test-worker-that-fails.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("install", () => { 2 | throw new Error("no"); 3 | }); 4 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/test-bootstrap.ts: -------------------------------------------------------------------------------- 1 | export function describeIfApp(desc, target) { 2 | if (!(window as any).swwebviewSettings) { 3 | xdescribe(desc, target); 4 | } else { 5 | describe(desc, target); 6 | } 7 | } 8 | // This is generated by the rollup plugin 9 | import "all-tests"; 10 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/tests.d.ts: -------------------------------------------------------------------------------- 1 | declare module "all-tests" { 2 | export default function(): void; 3 | } 4 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/universal/cache-storage.ts: -------------------------------------------------------------------------------- 1 | import { execInWorker } from "../util/exec-in-worker"; 2 | import { waitUntilWorkerIsActivated } from "../util/sw-lifecycle"; 3 | import { assert } from "chai"; 4 | import { unregisterEverything } from "../util/unregister-everything"; 5 | 6 | describe("CacheStorage", () => { 7 | afterEach(() => { 8 | return navigator.serviceWorker 9 | .getRegistration("/fixtures/") 10 | .then(reg => { 11 | return execInWorker( 12 | reg.active!, 13 | ` 14 | return caches.keys().then(keys => { 15 | return Promise.all(keys.map(k => caches.delete(k))); 16 | }); 17 | ` 18 | ); 19 | }) 20 | .then(() => { 21 | return unregisterEverything(); 22 | }); 23 | }); 24 | 25 | it("should open a cache and record it in list of keys", () => { 26 | return navigator.serviceWorker 27 | .register("/fixtures/exec-worker.js") 28 | .then(reg => { 29 | return waitUntilWorkerIsActivated(reg.installing!); 30 | }) 31 | .then(worker => { 32 | return execInWorker( 33 | worker, 34 | ` 35 | return caches.open("test-cache") 36 | .then(() => { 37 | return caches.keys() 38 | }) 39 | ` 40 | ); 41 | }) 42 | .then((response: string[]) => { 43 | assert.equal(response.length, 1); 44 | assert.equal(response[0], "test-cache"); 45 | }); 46 | }); 47 | 48 | it("should return correct values for has()", () => { 49 | return navigator.serviceWorker 50 | .register("/fixtures/exec-worker.js") 51 | .then(reg => { 52 | return waitUntilWorkerIsActivated(reg.installing!); 53 | }) 54 | .then(worker => { 55 | return execInWorker( 56 | worker, 57 | "return caches.has('test-cache')" 58 | ).then(response => { 59 | assert.equal(response, false); 60 | return execInWorker( 61 | worker, 62 | "return caches.open('test-cache').then(() => caches.has('test-cache'))" 63 | ); 64 | }); 65 | }) 66 | .then(response2 => { 67 | assert.equal(response2, true); 68 | }); 69 | }); 70 | 71 | it("should delete() successfully", () => { 72 | return navigator.serviceWorker 73 | .register("/fixtures/exec-worker.js") 74 | .then(reg => { 75 | return waitUntilWorkerIsActivated(reg.installing!); 76 | }) 77 | .then(worker => { 78 | return execInWorker( 79 | worker, 80 | `return caches.open('test-cache') 81 | .then(() => caches.delete('test-cache')) 82 | .then(() => caches.has('test-cache'))` 83 | ); 84 | }) 85 | .then(response2 => { 86 | assert.equal(response2, false); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/universal/console-interceptor.ts: -------------------------------------------------------------------------------- 1 | import createConsoleInterceptor from "../../src/util/console-interceptor"; 2 | import { assert } from "chai"; 3 | 4 | describe("Console Interceptor", () => { 5 | it("Should intercept messages", function(done) { 6 | let interceptor = createConsoleInterceptor((level, args) => { 7 | assert.equal(level, "log"); 8 | assert.equal(args[0], "hello"); 9 | done(); 10 | }); 11 | 12 | interceptor.log("hello"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/util/exec-in-worker.ts: -------------------------------------------------------------------------------- 1 | export function execInWorker(worker: ServiceWorker, js: string) { 2 | return new Promise((fulfill, reject) => { 3 | let channel = new MessageChannel(); 4 | 5 | worker.postMessage( 6 | { 7 | action: "exec", 8 | js: js, 9 | port: channel.port1 10 | }, 11 | [channel.port1] 12 | ); 13 | 14 | channel.port2.onmessage = function(e: MessageEvent) { 15 | if (e.data.error) { 16 | reject(new Error(e.data.error)); 17 | } 18 | fulfill(e.data.response); 19 | }; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/util/sw-lifecycle.ts: -------------------------------------------------------------------------------- 1 | export function waitUntilWorkerIsActivated( 2 | worker: ServiceWorker 3 | ): Promise { 4 | return new Promise((fulfill, reject) => { 5 | let listener = function(e) { 6 | if (worker.state !== "activated") return; 7 | worker.removeEventListener("statechange", listener); 8 | fulfill(worker); 9 | }; 10 | worker.addEventListener("statechange", listener); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/util/unregister-everything.ts: -------------------------------------------------------------------------------- 1 | export function unregisterEverything() { 2 | return navigator.serviceWorker 3 | .getRegistrations() 4 | .then((regs: ServiceWorkerRegistration[]) => { 5 | console.groupCollapsed("Unregister calls"); 6 | console.info("Unregistering:" + regs.map(r => r.scope).join(", ")); 7 | let mapped = regs.map(r => r.unregister()); 8 | 9 | return Promise.all(mapped); 10 | }) 11 | .then(() => { 12 | console.groupEnd(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /SWWebView/js-src/tests/util/with-iframe.ts: -------------------------------------------------------------------------------- 1 | export function withIframe( 2 | src: string = "/fixtures/blank.html", 3 | cb: (Window) => Promise | void 4 | ): Promise { 5 | return new Promise((fulfill, reject) => { 6 | let iframe = document.createElement("iframe"); 7 | 8 | iframe.onload = () => { 9 | fulfill( 10 | Promise.resolve(cb(iframe.contentWindow)) 11 | .then(() => { 12 | return iframe.contentWindow.navigator.serviceWorker.getRegistrations(); 13 | }) 14 | .then((regs: ServiceWorkerRegistration[]) => { 15 | let mapped = regs.map(r => r.unregister()); 16 | return Promise.all(mapped); 17 | }) 18 | .then(() => { 19 | return new Promise((fulfill, reject) => { 20 | setTimeout(() => { 21 | // No idea why this has to be in a timeout, but the promise stops 22 | // if it isn't. 23 | document.body.removeChild(iframe); 24 | // setTimeout(() => { 25 | fulfill(); 26 | // }, 10); 27 | }, 1); 28 | }); 29 | }) 30 | ); 31 | }; 32 | 33 | iframe.src = src; 34 | iframe.style.display = "none"; 35 | document.body.appendChild(iframe); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /SWWebView/js-src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "strictNullChecks": true, 5 | "lib": ["dom", "es6"], 6 | "moduleResolution": "node" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorker/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - identifier_name 3 | - line_length 4 | opt_in_rules: 5 | - force_unwrapping 6 | included: 7 | - ServiceWorker 8 | #force_unwrapping: error 9 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/ConstructableEvent.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | @objc protocol ConstructableEventExports: Event, JSExport { 4 | init(type: String) 5 | } 6 | 7 | /// There are probably better ways of doing this, but this is a specific class allowing 8 | /// client code to create events (i.e. new Event('test')) - originally this was a base 9 | /// class every other Event extended, but that required they all implement the constructor 10 | /// used here - which we don't want to do, in the case of things like FetchEvent. 11 | @objc class ConstructableEvent: NSObject, ConstructableEventExports { 12 | let type: String 13 | 14 | required init(type: String) { 15 | self.type = type 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/Event.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// At their core, events are very simple and only need a type attribute. There 5 | /// are others in JS (like bubbles etc) that we can add later, but for now this 6 | /// is all we need. 7 | @objc public protocol Event: JSExport { 8 | var type: String { get } 9 | } 10 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/EventListenerProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// We have different types of event listener (JSEventLister and SwiftEventListener) 4 | /// so we ensure that both adhere to this same protocol, allowing them to be used 5 | /// interchangably. 6 | protocol EventListener { 7 | func dispatch(_: Event) 8 | var eventName: String { get } 9 | } 10 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/FetchEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | import PromiseKit 4 | 5 | @objc protocol FetchEventExports: Event, JSExport { 6 | func respondWith(_: JSValue) 7 | var request: FetchRequest { get } 8 | } 9 | 10 | /// Similar to an ExtendableEvent, but a FetchEvent (https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent) 11 | /// exposes respondWith() instead of waitUntil(), because we want to be able to return a FetchResponse after 12 | /// resolving promises. 13 | @objc public class FetchEvent: NSObject, FetchEventExports { 14 | 15 | /// All FetchEvents must have a FetchRequest attached for the worker to respond to. 16 | let request: FetchRequest 17 | 18 | fileprivate var respondValue: JSValue? 19 | public let type = "fetch" 20 | 21 | func respondWith(_ val: JSValue) { 22 | 23 | if self.respondValue != nil { 24 | 25 | // Unlike waitUntil(), you can only call respondWith() once. So we throw an error if the client 26 | // code tries twice. 27 | 28 | let err = JSValue(newErrorFromMessage: "respondWith() has already been called", in: val.context) 29 | val.context.exception = err 30 | return 31 | } 32 | 33 | guard let resolved = val.context 34 | // It is possible to call e.respondWith(new Response("blah")) rather than return a promise. So we 35 | // quickly call Promise.resolve() to ensure whatever we've been provided is a promise. 36 | .evaluateScript("(val) => Promise.resolve(val)") 37 | .call(withArguments: [val]) else { 38 | 39 | // There shouldn't (AFAIK?) be any reason for Promise.resolve() to fail, but you never know. 40 | 41 | let err = JSValue(newErrorFromMessage: "Could not call Promise.resolve() on provided value", in: val.context) 42 | val.context.exception = err 43 | return 44 | } 45 | 46 | self.respondValue = resolved 47 | } 48 | 49 | public init(request: FetchRequest) { 50 | self.request = request 51 | super.init() 52 | } 53 | 54 | public func resolve(in _: ServiceWorker) throws -> Promise { 55 | 56 | guard let promise = self.respondValue else { 57 | 58 | // if e.respondWith() was never called that's perfectly valid - we resolve the 59 | // promise with no FetchResponse having been provided. 60 | 61 | return Promise(value: nil) 62 | } 63 | 64 | guard let exec = ServiceWorkerExecutionEnvironment.contexts.object(forKey: promise.context) else { 65 | 66 | // It's possible that someone might try using this in a JSContext that is not a ServiceWorker. 67 | // So we need to double-check that we do actually have a ServiceWorkerExecutionEnvironment, and 68 | // thus a specific thread, to resolve this promise to. 69 | 70 | return Promise(error: ErrorMessage("Could not get execution environment for this JSContext")) 71 | } 72 | 73 | // The JSContextPromise resolve() handles the cast from JSValue -> FetchResponseProtocol. It'll 74 | // throw if provided anything that isn't compatible. 75 | 76 | return JSContextPromise(jsValue: promise, thread: exec.thread).resolve() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/JSEventListener.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// A wrapper around a JavaScript function, stored as a JSValue, to be called when 5 | /// an event is dispatched. Also contains a reference to the correct thread to 6 | /// run the function on. 7 | class JSEventListener: EventListener { 8 | let eventName: String 9 | let funcToRun: JSValue 10 | let targetThread: Thread 11 | 12 | init(name: String, funcToRun: JSValue, thread: Thread) { 13 | self.eventName = name 14 | self.funcToRun = funcToRun 15 | self.targetThread = thread 16 | } 17 | 18 | func dispatch(_ event: Event) { 19 | self.funcToRun.perform(#selector(JSValue.call(withArguments:)), on: self.targetThread, with: [event], waitUntilDone: true) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Events/SwiftEventListener.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// A native version of JSEventListener, allowing us to attach swift closures to 5 | /// an EventTarget. It's a generic, but inherits from NSObject so that it can be 6 | /// stored in an Obj-C compatible array. 7 | class SwiftEventListener: NSObject, EventListener { 8 | let eventName: String 9 | let callback: (T) -> Void 10 | 11 | init(name: String, _ callback: @escaping (T) -> Void) { 12 | self.eventName = name 13 | self.callback = callback 14 | super.init() 15 | } 16 | 17 | func dispatch(_ event: Event) { 18 | 19 | // Because JSEventListeners are not type-specific like Swift ones are, we can't 20 | // strictly enforce type safety. If the event received is not the expected type 21 | // (e.g. received a FetchEvent when we were expecting an ExtendableEvent) the 22 | // event is not dispatched, and a warning is logged. 23 | 24 | if let specificEvent = event as? T { 25 | self.callback(specificEvent) 26 | } else { 27 | Log.warn?("Dispatched event \(event), but this listener is for type \(T.self)") 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/ExecutionEnvironment/EvaluateScriptCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PromiseKit 3 | import JavaScriptCore 4 | 5 | extension ServiceWorkerExecutionEnvironment { 6 | 7 | class PromiseWrappedCall: NSObject { 8 | internal let fulfill: (Any?) -> Void 9 | internal let reject: (Error) -> Void 10 | internal let promise: Promise 11 | 12 | override init() { 13 | (self.promise, self.fulfill, self.reject) = Promise.pending() 14 | } 15 | 16 | func resolve() -> Promise { 17 | return self.promise 18 | } 19 | 20 | func resolveVoid() -> Promise { 21 | return self.promise.then { _ in () } 22 | } 23 | } 24 | 25 | @objc enum EvaluateReturnType: Int { 26 | case void 27 | case object 28 | case promise 29 | } 30 | 31 | @objc internal class EvaluateScriptCall: NSObject { 32 | let script: String 33 | let url: URL? 34 | let returnType: EvaluateReturnType 35 | let fulfill: (Any?) -> Void 36 | let reject: (Error) -> Void 37 | 38 | init(script: String, url: URL?, passthrough: PromisePassthrough, returnType: EvaluateReturnType = .object) { 39 | self.script = script 40 | self.url = url 41 | self.returnType = returnType 42 | self.fulfill = passthrough.fulfill 43 | self.reject = passthrough.reject 44 | super.init() 45 | } 46 | } 47 | 48 | @objc internal class WithJSContextCall: PromiseWrappedCall { 49 | typealias FuncType = (JSContext) throws -> Void 50 | let funcToRun: FuncType 51 | init(_ funcToRun: @escaping FuncType) { 52 | self.funcToRun = funcToRun 53 | } 54 | } 55 | 56 | @objc internal class DispatchEventCall: PromiseWrappedCall { 57 | let event: Event 58 | init(_ event: Event) { 59 | self.event = event 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Fetch/FetchCORSRestrictions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Not really much to this, just a place to store CORS stuff. Not using a tuple because 4 | /// there will be more - Access-Control-Max-Age and so on might need be factored in. 5 | struct FetchCORSRestrictions { 6 | let isCrossDomain: Bool 7 | let allowedHeaders: [String] 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Fetch/HttpStatusCodes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Can't actually remember where I originally got these from now, but a source is here: 4 | // https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 5 | 6 | let HttpStatusCodes: [Int: String] = [ 7 | 100: "Continue", 8 | 101: "Switching Protocols", 9 | 102: "Processing", 10 | 200: "OK", 11 | 201: "Created", 12 | 202: "Accepted", 13 | 203: "Non-Authoritative Information", 14 | 204: "No Content", 15 | 205: "Reset Content", 16 | 206: "Partial Content", 17 | 207: "Multi-Status", 18 | 208: "Already Reported", 19 | 226: "IM Used", 20 | 300: "Multiple Choices", 21 | 301: "Moved Permanently", 22 | 302: "Found", 23 | 303: "See Other", 24 | 304: "Not Modified", 25 | 305: "Use Proxy", 26 | 306: "(Unused)", 27 | 307: "Temporary Redirect", 28 | 308: "Permanent Redirect", 29 | 400: "Bad Request", 30 | 401: "Unauthorized", 31 | 402: "Payment Required", 32 | 403: "Forbidden", 33 | 404: "Not Found", 34 | 405: "Method Not Allowed", 35 | 406: "Not Acceptable", 36 | 407: "Proxy Authentication Required", 37 | 408: "Request Timeout", 38 | 409: "Conflict", 39 | 410: "Gone", 40 | 411: "Length Required", 41 | 412: "Precondition Failed", 42 | 413: "Payload Too Large", 43 | 414: "URI Too Long", 44 | 415: "Unsupported Media Type", 45 | 416: "Range Not Satisfiable", 46 | 417: "Expectation Failed", 47 | 421: "Misdirected Request", 48 | 422: "Unprocessable Entity", 49 | 423: "Locked", 50 | 424: "Failed Dependency", 51 | 425: "Unassigned", 52 | 426: "Upgrade Required", 53 | 427: "Unassigned", 54 | 428: "Precondition Required", 55 | 429: "Too Many Requests", 56 | 430: "Unassigned", 57 | 431: "Request Header Fields Too Large", 58 | 451: "Unavailable For Legal Reasons", 59 | 500: "Internal Server Error", 60 | 501: "Not Implemented", 61 | 502: "Bad Gateway", 62 | 503: "Service Unavailable", 63 | 504: "Gateway Timeout", 64 | 505: "HTTP Version Not Supported", 65 | 506: "Variant Also Negotiates", 66 | 507: "Insufficient Storage", 67 | 508: "Loop Detected", 68 | 509: "Unassigned", 69 | 510: "Not Extended", 70 | 511: "Network Authentication Required" 71 | ] 72 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Fetch/Response/FetchResponseProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | import PromiseKit 4 | 5 | /// These are the FetchProtocol components that we expose to our JS environment 6 | @objc public protocol FetchResponseJSExports: JSExport { 7 | var headers: FetchHeaders { get } 8 | var statusText: String { get } 9 | var ok: Bool { get } 10 | var redirected: Bool { get } 11 | var bodyUsed: Bool { get } 12 | var status: Int { get } 13 | 14 | @objc(type) 15 | var responseTypeString: String { get } 16 | 17 | @objc(url) 18 | var urlString: String { get } 19 | 20 | // func getReader() throws -> ReadableStream 21 | func json() -> JSValue? 22 | func text() -> JSValue? 23 | func arrayBuffer() -> JSValue? 24 | 25 | @objc(clone) 26 | func cloneResponseExports() -> FetchResponseJSExports? 27 | 28 | init?(body: JSValue, options: [String: Any]?) 29 | } 30 | 31 | /// Then, in addition to the above, these are the elements we make available natively. 32 | /// I think FetchResponseProxy is now the only class that implements these, so in theory 33 | /// we could flatten this out. 34 | public protocol FetchResponseProtocol: FetchResponseJSExports { 35 | func clone() throws -> FetchResponseProtocol 36 | var internalResponse: FetchResponse { get } 37 | var responseType: ResponseType { get } 38 | func text() -> Promise 39 | func data() -> Promise 40 | func json() -> Promise 41 | var streamPipe: StreamPipe? { get } 42 | var url: URL? { get } 43 | } 44 | 45 | /// Just to add a little complication to the mix, this is a special case for caching. Obj-C 46 | /// can't represent the Promises in FetchResponseProtocol, so we have a special-case, Obj-C 47 | /// compatible protocol that lets us get to the inner fetch response. 48 | @objc public protocol CacheableFetchResponse: FetchResponseJSExports { 49 | var internalResponse: FetchResponse { get } 50 | } 51 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Fetch/Response/ResponseType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// As outlined here: https://developer.mozilla.org/en-US/docs/Web/API/Response/type 4 | public enum ResponseType: String { 5 | case Basic = "basic" 6 | case CORS = "cors" 7 | case Error = "error" 8 | case Opaque = "opaque" 9 | 10 | // default isn't mentioned on MDN, but it's what Chrome uses when you create new Response() 11 | case Default = "default" 12 | 13 | // not part of the spec, we just use this ourselves. 14 | case Internal = "internal-response" 15 | } 16 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalEventLog/GlobalEventLog.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Our webview representations of the API need to be able to reflect changes made natively. 4 | /// So we have a "log" that classes like ServiceWorkerContainer push to, allowing us to 5 | /// listen and forward these details to our webview. Made this very quickly, it should 6 | /// almost definitely actually be using NSNotificationCenter or similar. 7 | public class GlobalEventLog { 8 | 9 | // We only keep weak references to our listeners because if the object containing 10 | // the listener is disregarded, the listener should be as well. 11 | fileprivate static var listeners = NSHashTable.weakObjects() 12 | 13 | public static func addListener(_ toRun: @escaping (T) -> Void) -> Listener { 14 | let wrapper = Listener(toRun) 15 | listeners.add(wrapper) 16 | return wrapper 17 | } 18 | 19 | public static func removeListener(_ listener: Listener) { 20 | self.listeners.remove(listener) 21 | } 22 | 23 | public static func notifyChange(_ target: T) { 24 | self.listeners.allObjects.forEach { listener in 25 | 26 | if let correctType = listener as? Listener { 27 | correctType.funcToRun(target) 28 | } 29 | } 30 | } 31 | } 32 | 33 | // Using NSObject to get around generic issues. Bad? Maybe. 34 | public class Listener: NSObject { 35 | 36 | let funcToRun: (T) -> Void 37 | 38 | init(_ funcToRun: @escaping (T) -> Void) { 39 | self.funcToRun = funcToRun 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalScope/Clients/Client.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc protocol ClientExports: JSExport { 5 | func postMessage(_ toSend: JSValue, _ transferrable: [JSValue]) 6 | var id: String { get } 7 | var type: String { get } 8 | var url: String { get } 9 | } 10 | 11 | /// An implementation of the Client API: https://developer.mozilla.org/en-US/docs/Web/API/Client 12 | /// mostly a wrapper around an external class that implements ClientProtocol. 13 | @objc class Client: NSObject, ClientExports { 14 | 15 | // We keep track of the client objects we've made before now, so that we 16 | // pass the same instances back into JSContexts where relevant. That means 17 | // they'll pass equality checks etc. 18 | // We don't want strong references though - if the JSContext is done with 19 | // a reference it doesn't have anything to compare to, so it can be garbage collected. 20 | fileprivate static var existingClients = NSHashTable.weakObjects() 21 | 22 | static func getOrCreate(from wrapper: T) -> Client { 23 | 24 | return self.existingClients.allObjects.first(where: { $0.clientInstance.id == wrapper.id }) ?? { 25 | 26 | let newClient = { () -> Client in 27 | 28 | // We could pass back either a Client or the more specific WindowClient - we need 29 | // our bridging class to match the protocol being passed in. 30 | 31 | if let windowWrapper = wrapper as? WindowClientProtocol { 32 | return WindowClient(wrapping: windowWrapper) 33 | } else { 34 | return Client(client: wrapper) 35 | } 36 | }() 37 | 38 | self.existingClients.add(newClient) 39 | return newClient 40 | }() 41 | } 42 | 43 | let clientInstance: ClientProtocol 44 | internal init(client: ClientProtocol) { 45 | self.clientInstance = client 46 | } 47 | 48 | func postMessage(_ toSend: JSValue, _: [JSValue]) { 49 | 50 | self.clientInstance.postMessage(message: toSend.toObject(), transferable: nil) 51 | } 52 | 53 | var id: String { 54 | return self.clientInstance.id 55 | } 56 | 57 | var type: String { 58 | return self.clientInstance.type.stringValue 59 | } 60 | 61 | var url: String { 62 | return self.clientInstance.url.absoluteString 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalScope/Clients/WindowClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | import PromiseKit 4 | 5 | @objc protocol WindowClientExports: JSExport { 6 | func focus() -> JSValue? 7 | func navigate(_ url: String) -> JSValue? 8 | var focused: Bool { get } 9 | var visibilityState: String { get } 10 | } 11 | 12 | /// A more specific version of Client, WindowClient: https://developer.mozilla.org/en-US/docs/Web/API/WindowClient 13 | /// also provides information on visibility and focus state (that don't apply to workers etc) 14 | @objc class WindowClient: Client, WindowClientExports { 15 | 16 | let wrapAroundWindow: WindowClientProtocol 17 | 18 | init(wrapping: WindowClientProtocol) { 19 | self.wrapAroundWindow = wrapping 20 | super.init(client: wrapping) 21 | } 22 | 23 | func focus() -> JSValue? { 24 | 25 | return Promise { fulfill, reject in 26 | 27 | wrapAroundWindow.focus { err, windowClient in 28 | if let error = err { 29 | reject(error) 30 | } else if let client = windowClient { 31 | fulfill(Client.getOrCreate(from: client)) 32 | } 33 | } 34 | } 35 | .toJSPromiseInCurrentContext() 36 | } 37 | 38 | func navigate(_ url: String) -> JSValue? { 39 | 40 | return Promise { fulfill, reject in 41 | guard let parsedURL = URL(string: url, relativeTo: nil) else { 42 | return reject(ErrorMessage("Could not parse URL returned by native implementation")) 43 | } 44 | 45 | self.wrapAroundWindow.navigate(to: parsedURL) { err, windowClient in 46 | if let error = err { 47 | reject(error) 48 | } else if let window = windowClient { 49 | fulfill(window) 50 | } 51 | } 52 | }.toJSPromiseInCurrentContext() 53 | } 54 | 55 | var focused: Bool { 56 | return self.wrapAroundWindow.focused 57 | } 58 | 59 | var visibilityState: String { 60 | return self.wrapAroundWindow.visibilityState.stringValue 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalScope/Location/JSURL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc public protocol JSURLExports: JSExport { 5 | var href: String { get set } 6 | var `protocol`: String { get set } 7 | var host: String { get set } 8 | var hostname: String { get set } 9 | var origin: String { get set } 10 | var port: String { get set } 11 | var pathname: String { get set } 12 | var search: String { get set } 13 | var searchParams: URLSearchParams { get } 14 | 15 | init?(url: JSValue, relativeTo: JSValue) 16 | } 17 | 18 | /// An implementation of the JS URL object: https://developer.mozilla.org/en-US/docs/Web/API/URL 19 | @objc public class JSURL: LocationBase, JSURLExports { 20 | 21 | public required init?(url: JSValue, relativeTo: JSValue) { 22 | 23 | do { 24 | 25 | var parsedRelative: URL? 26 | 27 | if relativeTo.isUndefined == false { 28 | guard let relative = URL(string: relativeTo.toString()), relative.host != nil, relative.scheme != nil else { 29 | throw ErrorMessage("Invalid base URL") 30 | } 31 | parsedRelative = relative 32 | } 33 | 34 | guard let parsedURL = URL(string: url.toString(), relativeTo: parsedRelative), parsedURL.host != nil, parsedURL.scheme != nil else { 35 | throw ErrorMessage("Invalid URL") 36 | } 37 | 38 | super.init(withURL: parsedURL) 39 | 40 | } catch { 41 | let err = JSValue(newErrorFromMessage: "\(error)", in: url.context) 42 | url.context.exception = err 43 | return nil 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalScope/Location/WorkerLocation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc public protocol WorkerLocationExports: JSExport { 5 | var href: String { get } 6 | var `protocol`: String { get } 7 | 8 | var host: String { get } 9 | var hostname: String { get } 10 | var origin: String { get } 11 | var port: String { get } 12 | var pathname: String { get } 13 | var search: String { get } 14 | var searchParams: URLSearchParams { get } 15 | } 16 | 17 | /// Basically the same as URL as far as I can, except for the fact that it is 18 | /// read-only. https://developer.mozilla.org/en-US/docs/Web/API/WorkerLocation 19 | @objc(WorkerLocation) public class WorkerLocation: LocationBase, WorkerLocationExports { 20 | } 21 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/GlobalScope/ServiceWorkerGlobalScopeDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// This is primarily used to bridge between ServiceWorkerGlobalScope and ServiceWorkerExecutionEnvironment 5 | /// without having to make a reference loop between then. Leaves open possibility for other delegate 6 | /// implementations, though. 7 | @objc protocol ServiceWorkerGlobalScopeDelegate { 8 | func importScripts(urls: [URL]) throws 9 | func openWebSQLDatabase(name: String) throws -> WebSQLDatabase 10 | func fetch(_: JSValue) -> JSValue? 11 | func skipWaiting() 12 | } 13 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Cache/Cache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// All functions in this protocol should return a Promise resolving to FetchResponse 5 | /// a bool, or string array, depending on function. In retrospect, these should probably 6 | /// be synchronous or use callbacks, and be wrapped internally. Another time. 7 | @objc public protocol Cache: JSExport { 8 | func match(_ request: JSValue, _ options: [String: Any]?) -> JSValue? 9 | func matchAll(_ request: JSValue, _ options: [String: Any]?) -> JSValue? 10 | func add(_ request: JSValue) -> JSValue? 11 | func addAll(_ requests: JSValue) -> JSValue? 12 | func put(_ request: FetchRequest, _ response: CacheableFetchResponse) -> JSValue? 13 | func delete(_ request: JSValue, _ options: [String: Any]?) -> JSValue? 14 | func keys(_ request: JSValue, _ options: [String: Any]?) -> JSValue? 15 | } 16 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Cache/CacheMatchOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// CacheMatchOptions are outlined here: https://developer.mozilla.org/en-US/docs/Web/API/Cache/match 4 | public struct CacheMatchOptions { 5 | let ignoreSearch: Bool 6 | let ignoreMethod: Bool 7 | let ignoreVary: Bool 8 | let cacheName: String? 9 | } 10 | 11 | public extension CacheMatchOptions { 12 | 13 | /// Just a quick shortcut method to let us construct matching options from a JS object 14 | static func fromDictionary(opts: [String: Any]) -> CacheMatchOptions { 15 | 16 | let ignoreSearch = opts["ignoreSearch"] as? Bool ?? false 17 | let ignoreMethod = opts["ignoreMethod"] as? Bool ?? false 18 | let ignoreVary = opts["ignoreVary"] as? Bool ?? false 19 | let cacheName: String? = opts["cacheName"] as? String 20 | 21 | return CacheMatchOptions(ignoreSearch: ignoreSearch, ignoreMethod: ignoreMethod, ignoreVary: ignoreVary, cacheName: cacheName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Cache/CacheStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// Should resolve to JS promises. Like Cache, should probably actually be all native, 5 | /// and be wrapped internally. Otherwise we might end up leaking JSValues everywhere. 6 | @objc public protocol CacheStorageJSExports: JSExport { 7 | func match(_ request: JSValue, _ options: [String: Any]?) -> JSValue? 8 | func has(_ cacheName: String) -> JSValue? 9 | func open(_ cacheName: String) -> JSValue? 10 | func delete(_ cacheName: String) -> JSValue? 11 | func keys() -> JSValue? 12 | } 13 | 14 | @objc public protocol CacheStorage: CacheStorageJSExports, JSExport { 15 | 16 | /// This is used to define the Cache object in a worker's global scope - probaby 17 | /// not strictly necessary, but it matches what browsers do. 18 | static var CacheClass: Cache.Type { get } 19 | } 20 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Cache/CacheStorageProviderDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc public protocol CacheStorageProviderDelegate { 4 | 5 | /// CacheStorage instances are specific to origin, but we send in the worker 6 | /// in case a custom implementation wants to do more. 7 | @objc func createCacheStorage(_: ServiceWorker) throws -> CacheStorage 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/ClientMatchAllOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Outlined here: https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll 4 | @objc public class ClientMatchAllOptions: NSObject { 5 | let includeUncontrolled: Bool 6 | let type: String 7 | 8 | init(includeUncontrolled: Bool, type: String) { 9 | self.includeUncontrolled = includeUncontrolled 10 | self.type = type 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/ClientProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// The representation of a webview that a service worker sees. 5 | @objc public protocol ClientProtocol { 6 | func postMessage(message: Any?, transferable: [Any]?) 7 | var id: String { get } 8 | var type: ClientType { get } 9 | var url: URL { get } 10 | } 11 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/ClientType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Outlined here: https://developer.mozilla.org/en-US/docs/Web/API/Clients/matchAll, though 4 | /// not sure we'll ever implement Worker or SharedWorker (not sure how we'd know about them) 5 | @objc public enum ClientType: Int { 6 | case Window 7 | case Worker 8 | case SharedWorker 9 | } 10 | 11 | // Can't use string enums because Objective C doesn't like them 12 | extension ClientType { 13 | 14 | var stringValue: String { 15 | switch self { 16 | case .SharedWorker: 17 | return "sharedworker" 18 | case .Window: 19 | return "window" 20 | case .Worker: 21 | return "worker" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/ServiceWorkerClientsDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The bridge between the service worker and the webviews in the app. Allows a worker to open a window 4 | /// claim clients, etc. Based on: https://developer.mozilla.org/en-US/docs/Web/API/Clients 5 | @objc public protocol ServiceWorkerClientsDelegate { 6 | 7 | // These are all optional, but in retrospect, I'm not totally sure why. 8 | 9 | @objc optional func clients(_: ServiceWorker, getById: String, _ callback: (Error?, ClientProtocol?) -> Void) 10 | @objc optional func clients(_: ServiceWorker, matchAll: ClientMatchAllOptions, _ cb: (Error?, [ClientProtocol]?) -> Void) 11 | @objc optional func clients(_: ServiceWorker, openWindow: URL, _ cb: (Error?, ClientProtocol?) -> Void) 12 | @objc optional func clientsClaim(_: ServiceWorker, _ cb: (Error?) -> Void) 13 | } 14 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/WindowClientProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Extension of ClientProtocol that specifically handles webviews 4 | @objc public protocol WindowClientProtocol: ClientProtocol { 5 | func focus(_ cb: (Error?, WindowClientProtocol?) -> Void) 6 | func navigate(to: URL, _ cb: (Error?, WindowClientProtocol?) -> Void) 7 | 8 | var focused: Bool { get } 9 | var visibilityState: WindowClientVisibilityState { get } 10 | } 11 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Clients/WindowClientVisibilityState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// As outlined here: https://developer.mozilla.org/en-US/docs/Web/API/WindowClient/visibilityState 4 | @objc public enum WindowClientVisibilityState: Int { 5 | case Hidden 6 | case Visible 7 | case Prerender 8 | case Unloaded 9 | } 10 | 11 | // Objective C doesn't like string enums, so instead we're using an extension. 12 | public extension WindowClientVisibilityState { 13 | var stringValue: String { 14 | switch self { 15 | case .Hidden: 16 | return "hidden" 17 | case .Prerender: 18 | return "prerender" 19 | case .Unloaded: 20 | return "unloaded" 21 | case .Visible: 22 | return "visible" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/Registration/ServiceWorkerRegistrationProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// We don't provide an implementation of ServiceWorkerRegistration in this project, but this 5 | /// protocol is a hook to add one to a worker. Based on: 6 | /// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 7 | @objc public protocol ServiceWorkerRegistrationProtocol { 8 | func showNotification(_: JSValue) -> JSValue 9 | var id: String { get } 10 | var scope: URL { get } 11 | var active: ServiceWorker? { get } 12 | var waiting: ServiceWorker? { get } 13 | var installing: ServiceWorker? { get } 14 | var redundant: ServiceWorker? { get } 15 | } 16 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Interop/ServiceWorkerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Storage specific hooks for a worker. Should rename to ServiceWorkerStorageDelegate at 4 | /// some point. 5 | @objc public protocol ServiceWorkerDelegate { 6 | 7 | @objc func serviceWorker(_: ServiceWorker, importScript: URL, _ callback: @escaping (Error?, String?) -> Void) 8 | @objc func serviceWorkerGetDomainStoragePath(_: ServiceWorker) throws -> URL 9 | @objc func serviceWorkerGetScriptContent(_: ServiceWorker) throws -> String 10 | @objc func getCoreDatabaseURL() -> URL 11 | } 12 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/JS/GlobalVariableProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | // I'm seeing weird issues with memory holds when we add objects directly to a 5 | // JSContext's global object. So instead we use this and defineProperty to map 6 | // properties without directly attaching them to the object. 7 | class GlobalVariableProvider { 8 | 9 | static let variableMaps = NSMapTable(keyOptions: NSPointerFunctions.Options.weakMemory, valueOptions: NSPointerFunctions.Options.strongMemory) 10 | 11 | fileprivate static func getDictionary(forContext context: JSContext) -> NSDictionary { 12 | if let existing = variableMaps.object(forKey: context) { 13 | return existing 14 | } 15 | 16 | let newDictionary = NSMutableDictionary() 17 | variableMaps.setObject(newDictionary, forKey: context) 18 | return newDictionary 19 | } 20 | 21 | fileprivate static func createPropertyAccessor(for name: String) -> @convention(block) () -> Any? { 22 | return { 23 | 24 | guard let ctx = JSContext.current() else { 25 | Log.error?("Tried to use a JS property accessor with no JSContext. Should never happen") 26 | return nil 27 | } 28 | 29 | let dict = GlobalVariableProvider.getDictionary(forContext: ctx) 30 | return dict[name] 31 | } 32 | } 33 | 34 | static func destroy(forContext context: JSContext) { 35 | 36 | if let dict = variableMaps.object(forKey: context) { 37 | // Not really sure if this makes a difference, but we might as well 38 | // delete the property callbacks we created. 39 | dict.allKeys.forEach { key in 40 | if let keyAsString = key as? String { 41 | context.globalObject.deleteProperty(keyAsString) 42 | } 43 | } 44 | } 45 | 46 | if context.globalObject.hasProperty("self") { 47 | context.globalObject.deleteProperty("self") 48 | } 49 | 50 | self.variableMaps.removeObject(forKey: context) 51 | } 52 | 53 | /// A special case so we don't need to hold a reference to the global object 54 | static func addSelf(to context: JSContext) { 55 | context.globalObject.defineProperty("self", descriptor: [ 56 | "get": { 57 | JSContext.current().globalObject 58 | } as @convention(block) () -> Any? 59 | ]) 60 | } 61 | 62 | static func add(variable: Any, to context: JSContext, withName name: String) { 63 | 64 | let dictionary = GlobalVariableProvider.getDictionary(forContext: context) 65 | dictionary.setValue(variable, forKey: name) 66 | 67 | context.globalObject.defineProperty(name, descriptor: [ 68 | "get": createPropertyAccessor(for: name) 69 | ]) 70 | } 71 | 72 | static func add(missingPropertyWithError error: String, to context: JSContext, withName name: String) { 73 | 74 | context.globalObject.defineProperty(name, descriptor: [ 75 | "get": { 76 | if let ctx = JSContext.current() { 77 | let err = JSValue(newErrorFromMessage: error, in: ctx) 78 | ctx.exception = err 79 | } 80 | } as @convention(block) () -> Void 81 | ]) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/JS/JSArrayBuffer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | /// We need a little help with ArrayBuffers because they require us to maintain 5 | /// a reference to the Data contained within them, otherwise the reference is 6 | /// lost and the data overwritten. 7 | class JSArrayBuffer: NSObject { 8 | 9 | /// We keep track of all the JSArrayBuffer instances that have been made 10 | /// and not yet deallocated. This means we never lose references to the data 11 | /// an array buffer is using. 12 | fileprivate static var currentInstances = Set() 13 | 14 | // The actual mutable the array buffer stores data in 15 | var data: Data 16 | 17 | /// This is called by the ArrayBuffer deallocator - set in make() 18 | static func unassign(bytes _: UnsafeMutableRawPointer?, reference: UnsafeMutableRawPointer?) { 19 | 20 | guard let existingReference = reference else { 21 | Log.error?("Received deallocate message from a JSArrayBuffer with no native reference") 22 | return 23 | } 24 | 25 | let jsb = Unmanaged.fromOpaque(existingReference).takeUnretainedValue() 26 | JSArrayBuffer.currentInstances.remove(jsb) 27 | Log.info?("Unassigning JSArrayBuffer memory: \(jsb.data.count) bytes") 28 | } 29 | 30 | // fileprivate becuase we don't ever want to make one of these without wrapping it 31 | // in the JSContext ArrayBuffer, as done in make() 32 | fileprivate init(from data: Data) { 33 | self.data = data 34 | super.init() 35 | } 36 | 37 | static func make(from data: Data, in context: JSContext) -> JSValue { 38 | 39 | let instance = JSArrayBuffer(from: data) 40 | 41 | // create a strong reference to this data 42 | JSArrayBuffer.currentInstances.insert(instance) 43 | 44 | // the deallocator can't store a reference to the instance directly, instead 45 | // we pass a pointer into the Array Buffer constructor which is then passed back 46 | // when the deallocator is run. 47 | let instancePointer = Unmanaged.passUnretained(instance).toOpaque() 48 | 49 | // Now we make our actual array buffer JSValue using the data and deallocation callback 50 | let jsInstance = instance.data.withUnsafeMutableBytes { pointer -> JSObjectRef in 51 | 52 | return JSObjectMakeArrayBufferWithBytesNoCopy(context.jsGlobalContextRef, pointer, data.count, { bytes, reference in 53 | JSArrayBuffer.unassign(bytes: bytes, reference: reference) 54 | }, instancePointer, nil) 55 | } 56 | 57 | return JSValue(jsValueRef: jsInstance, in: context) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/JS/JSArrayBufferStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | // An additional initialiser for InputStream that handles ArrayBuffers created inside 5 | // a JS Context. 6 | extension InputStream { 7 | 8 | /// To ensure the memory does not get overwritten, we keep a reference to any byte pointer currently in use. 9 | /// In the data deallocator we remove this reference and free up the memory. 10 | fileprivate static var inUseArrayBufferPointers = Set() 11 | 12 | convenience init?(arrayBuffer val: JSValue) { 13 | 14 | // The JS methods used here store any errors they encounter in this variable... 15 | var maybeError: JSValueRef? 16 | 17 | // ...so we have this function to throw an error back into the JS context, if it exists. 18 | let makeExceptionIfNeeded = { 19 | guard let error = maybeError else { 20 | // There is no error, so we're OK 21 | return 22 | } 23 | 24 | let jsError = JSValue(jsValueRef: maybeError, in: val.context) 25 | val.context.exception = jsError 26 | throw ErrorMessage("Creation of ArrayBufferStream failed \(error)") 27 | } 28 | 29 | do { 30 | let arrType = JSValueGetTypedArrayType(val.context.jsGlobalContextRef, val.jsValueRef, &maybeError) 31 | 32 | if arrType != kJSTypedArrayTypeArrayBuffer { 33 | // This isn't an ArrayBuffer, so we stop before we go any further. 34 | return nil 35 | } 36 | 37 | try makeExceptionIfNeeded() 38 | 39 | let length = JSObjectGetArrayBufferByteLength(val.context.jsGlobalContextRef, val.jsValueRef, &maybeError) 40 | 41 | try makeExceptionIfNeeded() 42 | 43 | guard let bytes = JSObjectGetArrayBufferBytesPtr(val.context.jsGlobalContextRef, val.jsValueRef, &maybeError) else { 44 | throw ErrorMessage("Could not get bytes from ArrayBuffer") 45 | } 46 | 47 | try makeExceptionIfNeeded() 48 | 49 | // At this point we store the pointer to our data... 50 | InputStream.inUseArrayBufferPointers.insert(bytes) 51 | 52 | let data = Data(bytesNoCopy: bytes, count: length, deallocator: Data.Deallocator.custom({ releasedBytes, _ in 53 | 54 | // ...and then release it again when the Data object is deallocated. 55 | 56 | InputStream.inUseArrayBufferPointers.remove(releasedBytes) 57 | 58 | })) 59 | 60 | // Now we can just use the standard InputStream constructor. 61 | 62 | self.init(data: data) 63 | 64 | } catch { 65 | Log.error?("\(error)") 66 | return nil 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/JS/PromiseToJSPromise.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PromiseKit 3 | import JavaScriptCore 4 | 5 | public extension Promise { 6 | 7 | /// A convenience function added to all promises, to turn them into 8 | /// JS promises quickly and easily. 9 | public func toJSPromiseInCurrentContext() -> JSValue? { 10 | 11 | guard let ctx = JSContext.current() else { 12 | fatalError("Cannot call toJSPromiseInCurrentContext() outside of a JSContext") 13 | } 14 | 15 | do { 16 | let jsp = try JSContextPromise(newPromiseInContext: ctx) 17 | self.then { response -> Void in 18 | jsp.fulfill(response) 19 | } 20 | .catch { error in 21 | jsp.reject(error) 22 | } 23 | return jsp.jsValue 24 | } catch { 25 | let err = JSValue(newErrorFromMessage: "\(error)", in: ctx) 26 | ctx.exception = err 27 | return nil 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Messaging/ExtendableMessageEvent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc public protocol MessageEventExports: Event, JSExport { 5 | var data: Any { get } 6 | var ports: [SWMessagePort] { get } 7 | } 8 | 9 | /// ExtendableMessageEvent is like an ExtendableEvent except it also lets you transfer 10 | /// data and an array of transferrables (right now just MessagePort): 11 | /// https://developer.mozilla.org/en-US/docs/Web/API/ExtendableMessageEvent 12 | @objc public class ExtendableMessageEvent: ExtendableEvent, MessageEventExports { 13 | 14 | public let data: Any 15 | public let ports: [SWMessagePort] 16 | 17 | public init(data: Any, ports: [SWMessagePort] = []) { 18 | self.data = data 19 | self.ports = ports 20 | super.init(type: "message") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Messaging/MessageChannel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc public protocol MessageChannelExports: JSExport { 5 | var port1: SWMessagePort { get } 6 | var port2: SWMessagePort { get } 7 | init() 8 | } 9 | 10 | /// An implementation of the JavaScript MessageChannel object: 11 | /// https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel 12 | /// This is basically just a pair of MessagePorts, connected to each other. 13 | @objc public class MessageChannel: NSObject, MessageChannelExports { 14 | public let port1: SWMessagePort 15 | public let port2: SWMessagePort 16 | 17 | public required override init() { 18 | self.port1 = SWMessagePort() 19 | self.port2 = SWMessagePort() 20 | super.init() 21 | self.port1.targetPort = port2 22 | self.port2.targetPort = self.port1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Messaging/MessagePortTarget.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Normally a MessagePort communicates with another MessagePort (as facilitated 4 | /// by Message Channel) but at times we need to do something different, like set up 5 | /// a proxy to send messages into a SWWebView. This protocol allows us to do that. 6 | public protocol MessagePortTarget: class { 7 | var started: Bool { get } 8 | func start() 9 | func receiveMessage(_: ExtendableMessageEvent) 10 | func close() 11 | } 12 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Messaging/Transferable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// On the web this is for MessagePorts, ImageBitmaps and ArrayBuffers 4 | /// but for now we're just focusing on MessagePorts. Getting things like 5 | /// ArrayBuffers into SWWebView will be a pain, but not impossible. SharedArrayBuffers 6 | /// probably are impossible, though. 7 | @objc public protocol Transferable { 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/SQLite/SQLiteBlobReadStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLite3 3 | 4 | /// A bridge between the a Foundation InputStream and the SQLite C API's blob functions. 5 | public class SQLiteBlobReadStream: InputStreamImplementation { 6 | 7 | let dbPointer: SQLiteBlobStreamPointer 8 | 9 | init(_ db: SQLiteConnection, table: String, column: String, row: Int64) { 10 | self.dbPointer = SQLiteBlobStreamPointer(db, table: table, column: column, row: row, isWrite: true) 11 | 12 | // Don't understand why, but it forces us to call a specified initializer. So we'll do it with empty data. 13 | let dummyData = Data(count: 0) 14 | super.init(data: dummyData) 15 | self.streamStatus = .notOpen 16 | } 17 | 18 | public override func open() { 19 | 20 | do { 21 | self.streamStatus = Stream.Status.opening 22 | try self.dbPointer.open() 23 | self.streamStatus = Stream.Status.open 24 | self.emitEvent(event: .openCompleted) 25 | self.emitEvent(event: .hasBytesAvailable) 26 | } catch { 27 | self.throwError(error) 28 | } 29 | } 30 | 31 | public override var hasBytesAvailable: Bool { 32 | guard let state = self.dbPointer.openState else { 33 | // As specified in docs: https://developer.apple.com/documentation/foundation/inputstream/1409410-hasbytesavailable 34 | // both hasSpaceAvailable and hasBytesAvailable should return true when the actual state is unknown. 35 | return true 36 | } 37 | return state.currentPosition < state.blobLength 38 | } 39 | 40 | public override func close() { 41 | self.dbPointer.close() 42 | self.streamStatus = .closed 43 | } 44 | 45 | public override func read(_ buffer: UnsafeMutablePointer, maxLength len: Int) -> Int { 46 | do { 47 | self.streamStatus = .reading 48 | guard let state = self.dbPointer.openState else { 49 | throw ErrorMessage("Trying to read a closed stream") 50 | } 51 | 52 | let bytesLeft = state.blobLength - state.currentPosition 53 | 54 | // We can't read more data than exists in the blob, so we make sure we're 55 | // not going to go over: 56 | 57 | let lengthToRead = min(Int32(len), bytesLeft) 58 | 59 | if sqlite3_blob_read(state.pointer, buffer, lengthToRead, state.currentPosition) != SQLITE_OK { 60 | guard let errMsg = sqlite3_errmsg(self.dbPointer.db.db) else { 61 | throw ErrorMessage("SQLite failed, but can't get error") 62 | } 63 | let str = String(cString: errMsg) 64 | throw ErrorMessage(str) 65 | } 66 | 67 | // Now that we've read X bytes, ensure our pointer is updated to the next place we want 68 | // to read from. 69 | 70 | state.currentPosition += lengthToRead 71 | 72 | if state.currentPosition == state.blobLength { 73 | self.streamStatus = .atEnd 74 | self.emitEvent(event: .endEncountered) 75 | } else { 76 | self.streamStatus = .open 77 | self.emitEvent(event: .hasBytesAvailable) 78 | } 79 | 80 | return Int(lengthToRead) 81 | } catch { 82 | 83 | self.throwError(error) 84 | return -1 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/SQLite/SQLiteBlobStreamPointer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLite3 3 | 4 | class SQLiteBlobStreamPointer { 5 | 6 | class State { 7 | let pointer: OpaquePointer 8 | let blobLength: Int32 9 | var currentPosition: Int32 10 | 11 | init(pointer: OpaquePointer, blobLength: Int32) { 12 | self.pointer = pointer 13 | self.blobLength = blobLength 14 | self.currentPosition = 0 15 | } 16 | } 17 | 18 | let db: SQLiteConnection 19 | let table: String 20 | let column: String 21 | let row: Int64 22 | let isWriteStream: Bool 23 | var openState: State? 24 | 25 | init(_ db: SQLiteConnection, table: String, column: String, row: Int64, isWrite: Bool) { 26 | self.db = db 27 | self.table = table 28 | self.column = column 29 | self.row = row 30 | self.isWriteStream = isWrite 31 | } 32 | 33 | func open() throws { 34 | if self.openState != nil { 35 | Log.warn?("Tried to open a SQLiteBlobPointer that was already open") 36 | return 37 | } 38 | 39 | var pointer: OpaquePointer? 40 | 41 | let openResult = sqlite3_blob_open(self.db.db, "main", table, column, row, isWriteStream ? 1 : 0, &pointer) 42 | 43 | if openResult != SQLITE_OK { 44 | guard let errMsg = sqlite3_errmsg(self.db.db) else { 45 | throw ErrorMessage("SQLite failed, but can't get error") 46 | } 47 | let str = String(cString: errMsg) 48 | throw ErrorMessage("SQLite Error: \(str)") 49 | } 50 | 51 | guard let setPointer = pointer else { 52 | throw ErrorMessage("SQLite Blob pointer was not created successfully") 53 | } 54 | 55 | let blobLength = sqlite3_blob_bytes(setPointer) 56 | self.openState = State(pointer: setPointer, blobLength: blobLength) 57 | } 58 | 59 | func close() { 60 | guard let openState = self.openState else { 61 | Log.warn?("Tried to close a SQLiteBlobPointer that was already closed") 62 | return 63 | } 64 | sqlite3_blob_close(openState.pointer) 65 | self.openState = nil 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/SQLite/SQLiteBlobWriteStream.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLite3 3 | 4 | /// A bridge between the a Foundation OutputStream and the SQLite C API's blob functions. Important 5 | /// to note that the SQLite streaming functions cannot change the size of a BLOB field. It must be 6 | /// created in an INSERT or UPDATE query beforehand. 7 | public class SQLiteBlobWriteStream: OutputStreamImplementation { 8 | 9 | let dbPointer: SQLiteBlobStreamPointer 10 | 11 | init(_ db: SQLiteConnection, table: String, column: String, row: Int64) { 12 | self.dbPointer = SQLiteBlobStreamPointer(db, table: table, column: column, row: row, isWrite: true) 13 | 14 | // Not sure why we have to call this initializer, but we'll do it with empty data 15 | var empty = [UInt8]() 16 | super.init(toBuffer: &empty, capacity: 0) 17 | self.streamStatus = .notOpen 18 | } 19 | 20 | public override func open() { 21 | do { 22 | try self.dbPointer.open() 23 | self.emitEvent(event: .openCompleted) 24 | self.emitEvent(event: .hasSpaceAvailable) 25 | } catch { 26 | self.streamStatus = .error 27 | self.streamError = error 28 | } 29 | } 30 | 31 | public override var hasSpaceAvailable: Bool { 32 | guard let state = self.dbPointer.openState else { 33 | // As specified in docs: https://developer.apple.com/documentation/foundation/inputstream/1409410-hasbytesavailable 34 | // both hasSpaceAvailable and hasBytesAvailable should return true when the actual state is unknown. 35 | return true 36 | } 37 | return state.currentPosition < state.blobLength 38 | } 39 | 40 | public override func close() { 41 | self.dbPointer.close() 42 | self.streamStatus = .closed 43 | } 44 | 45 | public override func write(_ buffer: UnsafePointer, maxLength len: Int) -> Int { 46 | do { 47 | 48 | guard let state = self.dbPointer.openState else { 49 | throw ErrorMessage("Cannot write to a stream that is not open") 50 | } 51 | self.streamStatus = .writing 52 | 53 | let bytesLeft = state.blobLength - state.currentPosition 54 | 55 | // Same as when reading, we don't want to write more data than the blob can hold 56 | // so we cut it off if necessary - the streaming functions cannot change the size 57 | // of a blob, only UPDATEs/INSERTs can. 58 | 59 | let lengthToWrite = min(Int32(len), bytesLeft) 60 | 61 | if sqlite3_blob_write(state.pointer, buffer, lengthToWrite, state.currentPosition) != SQLITE_OK { 62 | guard let errMsg = sqlite3_errmsg(self.dbPointer.db.db) else { 63 | throw ErrorMessage("SQLite failed, but can't get error") 64 | } 65 | let str = String(cString: errMsg) 66 | throw ErrorMessage(str) 67 | } 68 | 69 | // Update the position we next want to write to 70 | state.currentPosition += lengthToWrite 71 | 72 | if state.currentPosition == state.blobLength { 73 | self.streamStatus = .atEnd 74 | self.emitEvent(event: .endEncountered) 75 | } else { 76 | self.streamStatus = .open 77 | self.emitEvent(event: .hasSpaceAvailable) 78 | } 79 | 80 | return Int(lengthToWrite) 81 | 82 | } catch { 83 | self.throwError(error) 84 | return -1 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/SQLite/SQLiteDataType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SQLite3 3 | 4 | public enum SQLiteDataType { 5 | case Text 6 | case Int 7 | case Blob 8 | case Float 9 | case Null 10 | } 11 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/ServiceWorker.h: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceWorker.h 3 | // ServiceWorker 4 | // 5 | // Created by alastair.coote on 01/08/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ServiceWorker. 12 | FOUNDATION_EXPORT double ServiceWorkerVersionNumber; 13 | 14 | //! Project version string for ServiceWorker. 15 | FOUNDATION_EXPORT const unsigned char ServiceWorkerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/ServiceWorkerInstallState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The various states a Service Worker can exist in. As outlined in: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker/state 4 | /// 5 | /// - Downloading: Isn't in the spec, but we use it when we are streaming the download of the JS 6 | /// - Installing: The worker is currently in the process of installing 7 | /// - Installed: The worker has successfully installed and is awaiting activation 8 | /// - Activating: The worker is currently in the process of activating 9 | /// - Activated: The worker is activated and ready to receive events and messages 10 | /// - Redundant: The worker has either failed to install or has been superseded by a new version of the worker. 11 | public enum ServiceWorkerInstallState: String { 12 | // case downloading = "downloading" 13 | case installing 14 | case installed 15 | case activating 16 | case activated 17 | case redundant 18 | } 19 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Stream/InputStreamImplementation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// If you want to subclass InputStream you have to implement a load of boilerplate code to 4 | /// go along with it. This is that boilerplate code, so we can just inherit from this instead. 5 | open class InputStreamImplementation: InputStream { 6 | 7 | // Get an error about abstract classes if we do not implement this. No idea why. 8 | open override var delegate: StreamDelegate? { 9 | get { 10 | return self._delegate 11 | } 12 | set(val) { 13 | self._delegate = val 14 | } 15 | } 16 | 17 | fileprivate var _streamStatus: Stream.Status = .notOpen 18 | 19 | open internal(set) override var streamStatus: Stream.Status { 20 | get { 21 | return self._streamStatus 22 | } 23 | set(val) { 24 | self._streamStatus = val 25 | } 26 | } 27 | 28 | fileprivate var _streamError: Error? 29 | 30 | open internal(set) override var streamError: Error? { 31 | get { 32 | return self._streamError 33 | } 34 | set(val) { 35 | self._streamError = val 36 | } 37 | } 38 | 39 | var runLoops: [RunLoop: Set] = [:] 40 | 41 | var pendingEvents: [Stream.Event] = [] 42 | 43 | fileprivate weak var _delegate: StreamDelegate? 44 | 45 | internal func throwError(_ error: Error) { 46 | self._streamStatus = .error 47 | self._streamError = error 48 | self.emitEvent(event: .errorOccurred) 49 | } 50 | 51 | internal func emitEvent(event: Stream.Event) { 52 | if self.runLoops.count > 0 { 53 | 54 | // If we're already scheduled in a run loop, send immediately 55 | 56 | self.runLoops.forEach({ loopPair in 57 | loopPair.key.perform(inModes: Array(loopPair.value), block: { 58 | self.delegate?.stream?(self, handle: event) 59 | }) 60 | }) 61 | 62 | } else { 63 | 64 | // Otherwise store these events to be sent when we are scheduled 65 | 66 | self.pendingEvents.append(event) 67 | } 68 | } 69 | 70 | open override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoopMode) { 71 | 72 | var modeArray = self.runLoops[aRunLoop] ?? Set() 73 | modeArray.insert(mode) 74 | self.runLoops[aRunLoop] = modeArray 75 | 76 | // send any pending events that were fired when there was no runloop 77 | self.pendingEvents.forEach { self.emitEvent(event: $0) } 78 | self.pendingEvents.removeAll() 79 | } 80 | 81 | open override func remove(from aRunLoop: RunLoop, forMode mode: RunLoopMode) { 82 | 83 | guard var existing = self.runLoops[aRunLoop] else { 84 | return 85 | } 86 | existing.remove(mode) 87 | 88 | if existing.count == 0 { 89 | self.runLoops.removeValue(forKey: aRunLoop) 90 | } else { 91 | self.runLoops[aRunLoop] = existing 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Stream/OutputStreamImplementation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Same as InputStreamImplementation, this just adds the boilerplate code any 4 | /// subclass of OutputStream requires. 5 | open class OutputStreamImplementation: OutputStream { 6 | 7 | // Get an error about abstract classes if we do not implement this. No idea why. 8 | open override var delegate: StreamDelegate? { 9 | get { 10 | return self._delegate 11 | } 12 | set(val) { 13 | self._delegate = val 14 | } 15 | } 16 | 17 | fileprivate var _streamStatus: Stream.Status = .notOpen 18 | 19 | open internal(set) override var streamStatus: Stream.Status { 20 | get { 21 | return self._streamStatus 22 | } 23 | set(val) { 24 | self._streamStatus = val 25 | } 26 | } 27 | 28 | fileprivate var _streamError: Error? 29 | 30 | open internal(set) override var streamError: Error? { 31 | get { 32 | return self._streamError 33 | } 34 | set(val) { 35 | self._streamError = val 36 | } 37 | } 38 | 39 | var runLoops: [RunLoop: Set] = [:] 40 | 41 | var pendingEvents: [Stream.Event] = [] 42 | 43 | fileprivate weak var _delegate: StreamDelegate? 44 | 45 | public func throwError(_ error: Error) { 46 | self._streamStatus = .error 47 | self._streamError = error 48 | self.emitEvent(event: .errorOccurred) 49 | } 50 | 51 | public func emitEvent(event: Stream.Event) { 52 | if self.runLoops.count > 0 { 53 | 54 | // If we're already scheduled in a run loop, send immediately 55 | 56 | self.runLoops.forEach({ loopPair in 57 | loopPair.key.perform(inModes: Array(loopPair.value), block: { 58 | self.delegate?.stream?(self, handle: event) 59 | }) 60 | }) 61 | 62 | } else { 63 | 64 | // Otherwise store these events to be sent when we are scheduled 65 | 66 | self.pendingEvents.append(event) 67 | } 68 | } 69 | 70 | open override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoopMode) { 71 | 72 | var modeArray = self.runLoops[aRunLoop] ?? Set() 73 | modeArray.insert(mode) 74 | self.runLoops[aRunLoop] = modeArray 75 | 76 | // send any pending events that were fired when there was no runloop 77 | self.pendingEvents.forEach { self.emitEvent(event: $0) } 78 | self.pendingEvents.removeAll() 79 | } 80 | 81 | open override func remove(from aRunLoop: RunLoop, forMode mode: RunLoopMode) { 82 | 83 | guard var existing = self.runLoops[aRunLoop] else { 84 | return 85 | } 86 | existing.remove(mode) 87 | 88 | if existing.count == 0 { 89 | self.runLoops.removeValue(forKey: aRunLoop) 90 | } else { 91 | self.runLoops[aRunLoop] = existing 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Util/ErrorMessage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// An attempt to bridge between JS-like errors and Swift-like errors - Swift 4 | /// would (I think) like us to create different classes/enums for each error. But 5 | /// this lets us throw errors with custom strings attached. 6 | public class ErrorMessage: Error, CustomStringConvertible { 7 | 8 | public let message: String 9 | 10 | public init(_ message: String) { 11 | self.message = message 12 | } 13 | 14 | public var description: String { 15 | return self.message 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Util/PromiseFulfillExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import PromiseKit 3 | 4 | /// This is messy, but our Objective-C functions that we want to call on separate threads 5 | /// often need to resolve promises. But we can't return anything from these functions 6 | /// (NSObject.perform() is always a void) so instead we need to pass through fulfill and 7 | /// reject as function parameters. So we use this as a container for those functions. 8 | @objc class PromisePassthrough: NSObject { 9 | 10 | let fulfill: (Any?) -> Void 11 | let reject: (Error) -> Void 12 | 13 | init(fulfill: @escaping (Any?) -> Void, reject: @escaping (Error) -> Void) { 14 | self.fulfill = fulfill 15 | self.reject = reject 16 | } 17 | } 18 | 19 | extension Promise { 20 | 21 | /// And an extension method on Promise to create a passthrough promise 22 | static func makePassthrough() -> (promise: Promise, passthrough: PromisePassthrough) { 23 | 24 | let (promise, fulfill, reject) = Promise.pending() 25 | 26 | let fulfillCast = { (result: Any?) in 27 | 28 | if T.self == Void.self, let voidResult = () as? T { 29 | fulfill(voidResult) 30 | return 31 | } 32 | 33 | guard let cast = result as? T else { 34 | reject(ErrorMessage("Could not cast \(result ?? "nil") to desired type \(T.self)")) 35 | return 36 | } 37 | fulfill(cast) 38 | } 39 | 40 | let passthrough = PromisePassthrough(fulfill: fulfillCast, reject: reject) 41 | 42 | return (promise, passthrough) 43 | } 44 | 45 | /// And to turn any already-created promise into a passthrough. 46 | func passthrough(_ target: PromisePassthrough) { 47 | self.then { result in 48 | target.fulfill(result) 49 | } 50 | .catch { error in 51 | target.reject(error) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/Util/SharedLogInterface.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SharedLogInterface { 4 | public var debug: ((String) -> Void)? 5 | public var info: ((String) -> Void)? 6 | public var warn: ((String) -> Void)? 7 | public var error: ((String) -> Void)? 8 | } 9 | 10 | // We want to be able to plug in a custom logging interface depending on environment. 11 | // This var is here for quick access inside the SW code (Log?.info()), but can be set 12 | // via ServiceWorker.logInterface in external code. 13 | public var Log = SharedLogInterface(debug: nil, info: nil, warn: nil, error: nil) 14 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/WebSQL/WebSQLResultRows.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc protocol WebSQLResultRowsExports: JSExport { 5 | func item(_: Int) -> Any? 6 | var length: Int { get } 7 | } 8 | 9 | @objc class WebSQLResultRows: NSObject, WebSQLResultRowsExports { 10 | 11 | let rows: [Any] 12 | 13 | init(rows: [Any]) { 14 | self.rows = rows 15 | } 16 | 17 | func item(_ index: Int) -> Any? { 18 | return self.rows[index] 19 | } 20 | 21 | var length: Int { 22 | return self.rows.count 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/WebSQL/WebSQLResultSet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JavaScriptCore 3 | 4 | @objc protocol WebSQLResultSetProtocol: JSExport { 5 | var insertId: Int64 { get } 6 | var rowsAffected: Int { get } 7 | var rows: WebSQLResultRows { get } 8 | } 9 | 10 | @objc class WebSQLResultSet: NSObject, WebSQLResultSetProtocol { 11 | 12 | let insertId: Int64 13 | let rowsAffected: Int 14 | var rows: WebSQLResultRows 15 | 16 | init(fromUpdateIn connection: SQLiteConnection) throws { 17 | guard let lastInsertedId = connection.lastInsertRowId, let lastNumberChanges = connection.lastNumberChanges else { 18 | throw ErrorMessage("Could not fetch last inserted ID/last number of changes from database") 19 | } 20 | 21 | self.insertId = lastInsertedId 22 | self.rowsAffected = lastNumberChanges 23 | self.rows = WebSQLResultRows(rows: []) 24 | } 25 | 26 | init(resultSet: SQLiteResultSet, connection _: SQLiteConnection) throws { 27 | 28 | self.insertId = -1 29 | self.rowsAffected = 0 30 | var rows: [Any] = [] 31 | 32 | while try resultSet.next() { 33 | 34 | var row = [String: Any?]() 35 | 36 | try resultSet.columnNames.forEach { name in 37 | 38 | let colType = try resultSet.getColumnType(name) 39 | 40 | if colType == .Text { 41 | row[name] = try resultSet.string(name) 42 | } else if colType == .Int { 43 | row[name] = try resultSet.int(name) 44 | } else if colType == .Float { 45 | row[name] = try resultSet.double(name) 46 | } else if colType == .Null { 47 | row[name] = NSNull() 48 | } else if colType == .Blob { 49 | // Obviously this isn't correct, but WebSQL doesn't support 50 | // binary blobs 51 | row[name] = try resultSet.string(name) 52 | } 53 | } 54 | 55 | rows.append(row) 56 | } 57 | 58 | self.rows = WebSQLResultRows(rows: rows) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/js-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ios-service-worker", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eventtarget": "^0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorker/js-src/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | eventtarget@^0.1.0: 6 | version "0.1.0" 7 | resolved "https://registry.yarnpkg.com/eventtarget/-/eventtarget-0.1.0.tgz#1ebdd72a3dcfbb0218e16d51450940cdffea4e26" 8 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Bootstrap.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | import PromiseKit 4 | 5 | public class TestBootstrap: NSObject { 6 | override init() { 7 | super.init() 8 | // Log.enable() 9 | 10 | Log.debug = { NSLog($0) } 11 | Log.info = { NSLog($0) } 12 | Log.warn = { NSLog($0) } 13 | Log.error = { NSLog($0) } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/DatabaseMigration/Migrate.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class Migrate: XCTestCase { 5 | 6 | let migrateTempPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("migrations", isDirectory: true) 7 | 8 | override func setUp() { 9 | super.setUp() 10 | 11 | do { 12 | try FileManager.default.createDirectory(at: self.migrateTempPath, withIntermediateDirectories: true, attributes: nil) 13 | } catch { 14 | XCTFail(String(describing: error)) 15 | } 16 | 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | do { 23 | try FileManager.default.removeItem(at: self.migrateTempPath) 24 | } catch { 25 | XCTFail(String(describing: error)) 26 | } 27 | super.tearDown() 28 | } 29 | 30 | func testBasicMigrations() { 31 | 32 | let testMigration = """ 33 | CREATE TABLE test ( 34 | "val" text NOT NULL 35 | ) 36 | """ 37 | 38 | let testMigrationTwo = """ 39 | INSERT INTO test VALUES ("success") 40 | """ 41 | 42 | XCTAssertNoThrow(try testMigration.data(using: String.Encoding.utf8)!.write(to: migrateTempPath.appendingPathComponent("1_one.sql"))) 43 | 44 | let dbPath = migrateTempPath.appendingPathComponent("test.db") 45 | 46 | var version = -1 47 | 48 | XCTAssertNoThrow(version = try DatabaseMigration.check(dbPath: dbPath, migrationsPath: migrateTempPath)) 49 | 50 | XCTAssertEqual(version, 1) 51 | 52 | XCTAssertNoThrow(try testMigrationTwo.data(using: String.Encoding.utf8)!.write(to: migrateTempPath.appendingPathComponent("2_two.sql"))) 53 | 54 | XCTAssertNoThrow(version = try DatabaseMigration.check(dbPath: dbPath, migrationsPath: migrateTempPath)) 55 | 56 | XCTAssertEqual(version, 2) 57 | 58 | XCTAssertNoThrow(try SQLiteConnection.inConnection(dbPath) { db in 59 | try db.select(sql: "SELECT val FROM test") { rs in 60 | XCTAssertEqual(try rs.next(), true) 61 | XCTAssertEqual(try rs.string("val"), "success") 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Events/EventTargetTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import JavaScriptCore 4 | 5 | class EventTargetTests: XCTestCase { 6 | 7 | func testShouldFireEvents() { 8 | 9 | let sw = ServiceWorker.createTestWorker(id: name) 10 | 11 | return sw.evaluateScript(""" 12 | var didFire = false; 13 | self.addEventListener('test', function() { 14 | didFire = true; 15 | }); 16 | self.dispatchEvent(new Event('test')); 17 | didFire; 18 | """) 19 | 20 | .then { (didFire:Bool?) -> Void in 21 | XCTAssertEqual(didFire, true) 22 | } 23 | .assertResolves() 24 | } 25 | 26 | func testShouldRemoveEventListeners() { 27 | 28 | let testEvents = EventTarget() 29 | 30 | let sw = ServiceWorker.createTestWorker(id: name) 31 | 32 | let expect = expectation(description: "Code ran") 33 | 34 | sw.withJSContext { context in 35 | context.globalObject.setValue(testEvents, forProperty: "testEvents") 36 | } 37 | .then { 38 | return sw.evaluateScript(""" 39 | var didFire = false; 40 | function trigger() { 41 | didFire = true; 42 | } 43 | testEvents.addEventListener('test', trigger); 44 | testEvents.removeEventListener('test', trigger); 45 | testEvents.dispatchEvent(new Event('test')); 46 | didFire; 47 | """) 48 | } 49 | .then { (didFire:Bool?) -> Void in 50 | XCTAssertEqual(didFire, false) 51 | expect.fulfill() 52 | } 53 | .catch { error -> Void in 54 | XCTFail("\(error)") 55 | } 56 | 57 | wait(for: [expect], timeout: 1) 58 | } 59 | 60 | func testShouldFireSwiftEvents() { 61 | 62 | let testEvents = EventTarget() 63 | var fired = false 64 | 65 | let testEvent = ConstructableEvent(type: "test") 66 | 67 | testEvents.addEventListener("test") { (ev: ConstructableEvent) in 68 | XCTAssertEqual(ev, testEvent) 69 | fired = true 70 | } 71 | 72 | testEvents.dispatchEvent(testEvent) 73 | 74 | XCTAssertTrue(fired) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Events/ExtendableEventTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import JavaScriptCore 3 | @testable import ServiceWorker 4 | import PromiseKit 5 | 6 | class ExtendableEventTests: XCTestCase { 7 | 8 | func testExtendingAnEvent() { 9 | 10 | let sw = ServiceWorker.createTestWorker(id: name) 11 | 12 | sw.withJSContext { context in 13 | let ev = ExtendableEvent(type: "test") 14 | context.globalObject.setValue(ev, forProperty: "testEvent") 15 | } 16 | .then { 17 | sw.evaluateScript(""" 18 | var testResult = false 19 | testEvent.waitUntil(new Promise(function(fulfill,reject) { 20 | testResult = true 21 | fulfill() 22 | })); 23 | testEvent; 24 | """) 25 | } 26 | 27 | .then { (ev: ExtendableEvent) in 28 | ev.resolve(in: sw) 29 | } 30 | .then { 31 | return sw.evaluateScript("testResult") 32 | } 33 | .then { (result: Bool) in 34 | XCTAssertEqual(result, true) 35 | } 36 | 37 | .assertResolves() 38 | } 39 | 40 | func testPromiseRejection() { 41 | 42 | let sw = ServiceWorker.createTestWorker(id: name) 43 | 44 | sw.withJSContext { context in 45 | let ev = ExtendableEvent(type: "test") 46 | context.globalObject.setValue(ev, forProperty: "testEvent") 47 | } 48 | .then { 49 | sw.evaluateScript(""" 50 | testEvent.waitUntil(new Promise(function(fulfill,reject) { 51 | reject(new Error("failure")) 52 | })) 53 | testEvent; 54 | """) 55 | } 56 | 57 | .then { (ev: ExtendableEvent) in 58 | ev.resolve(in: sw) 59 | } 60 | .then { 61 | XCTFail("Promise should not resolve") 62 | } 63 | .recover { error in 64 | XCTAssertEqual(String(describing: error), "failure") 65 | } 66 | 67 | .assertResolves() 68 | } 69 | 70 | func testMultiplePromises() { 71 | 72 | let sw = ServiceWorker.createTestWorker(id: name) 73 | 74 | sw.withJSContext { context in 75 | let ev = ExtendableEvent(type: "test") 76 | context.globalObject.setValue(ev, forProperty: "testEvent") 77 | } 78 | .then { 79 | sw.evaluateScript(""" 80 | var resultArray = []; 81 | testEvent.waitUntil(new Promise(function(fulfill,reject) { 82 | resultArray.push(1); 83 | fulfill(); 84 | })) 85 | testEvent.waitUntil(new Promise(function(fulfill,reject) { 86 | setTimeout(function() { 87 | resultArray.push(2); 88 | fulfill(); 89 | },10); 90 | })); 91 | testEvent; 92 | """) 93 | } 94 | 95 | .then { (ev: ExtendableEvent) in 96 | ev.resolve(in: sw) 97 | } 98 | .then { 99 | return sw.evaluateScript("resultArray") 100 | } 101 | .then { (results: [Int]?) -> Void in 102 | 103 | XCTAssertEqual(results?.count, 2) 104 | XCTAssertEqual(results?[0] as? Int, 1) 105 | XCTAssertEqual(results?[1] as? Int, 2) 106 | } 107 | 108 | .assertResolves() 109 | } 110 | 111 | func testNoPromises() { 112 | let sw = ServiceWorker.createTestWorker(id: name) 113 | 114 | let ev = ExtendableEvent(type: "test") 115 | ev.resolve(in: sw) 116 | .then { () -> Void in 117 | // compiler requires this 118 | } 119 | .assertResolves() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Events/FetchEventTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class FetchEventTests: XCTestCase { 5 | 6 | func testRespondWithString() { 7 | 8 | let sw = ServiceWorker.createTestWorker(id: name, content: """ 9 | self.addEventListener('fetch',(e) => { 10 | e.respondWith(new Response("hello")); 11 | }); 12 | """) 13 | 14 | let request = FetchRequest(url: URL(string: "https://www.example.com")!) 15 | 16 | let fetch = FetchEvent(request: request) 17 | 18 | sw.dispatchEvent(fetch) 19 | .then { 20 | return try fetch.resolve(in: sw) 21 | } 22 | .then { res in 23 | return res!.text() 24 | } 25 | .then { responseText in 26 | XCTAssertEqual(responseText, "hello") 27 | } 28 | .assertResolves() 29 | } 30 | 31 | func testRespondWithPromise() { 32 | 33 | let sw = ServiceWorker.createTestWorker(id: name, content: """ 34 | self.addEventListener('fetch',(e) => { 35 | e.respondWith(new Promise((fulfill) => { 36 | fulfill(new Response("hello")) 37 | })); 38 | }); 39 | """) 40 | 41 | let request = FetchRequest(url: URL(string: "https://www.example.com")!) 42 | 43 | let fetch = FetchEvent(request: request) 44 | 45 | sw.dispatchEvent(fetch) 46 | .then { 47 | return try fetch.resolve(in: sw) 48 | } 49 | .then { res in 50 | return res!.text() 51 | } 52 | .then { responseText in 53 | XCTAssertEqual(responseText, "hello") 54 | } 55 | .assertResolves() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Fetch/ConstructableFetchResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import PromiseKit 4 | import JavaScriptCore 5 | 6 | class ConstructableFetchResponseTests: XCTestCase { 7 | 8 | func testManualTextResponseCreationInWorker() { 9 | 10 | let sw = ServiceWorker.createTestWorker(id: self.name) 11 | 12 | sw.evaluateScript(""" 13 | var response = new Response("hello"); 14 | response.text() 15 | .then((text) => { 16 | return [text, response.status, response.url, response.headers.get('content-type')] 17 | }) 18 | """) 19 | .then { (jsVal: JSContextPromise) in 20 | return jsVal.resolve() 21 | }.then { (array: [Any]) -> Void in 22 | XCTAssertEqual(array[0] as? String, "hello") 23 | XCTAssertEqual(array[1] as? Int, 200) 24 | XCTAssertEqual(array[2] as? String, "") 25 | XCTAssertEqual(array[3] as? String, "text/plain;charset=UTF-8") 26 | } 27 | .assertResolves() 28 | } 29 | 30 | func testResponseConstructionOptions() { 31 | 32 | let sw = ServiceWorker.createTestWorker(id: self.name) 33 | 34 | sw.evaluateScript(""" 35 | new Response("hello", { 36 | status: 201, 37 | statusText: "CUSTOM TEXT", 38 | headers: { 39 | "X-Custom-Header":"blah", 40 | "Content-Type":"text/custom-content" 41 | } 42 | }) 43 | """) 44 | .then { (response: FetchResponseProxy?) in 45 | XCTAssertEqual(response!.status, 201) 46 | XCTAssertEqual(response!.statusText, "CUSTOM TEXT") 47 | XCTAssertEqual(response!.headers.get("X-Custom-Header"), "blah") 48 | XCTAssertEqual(response!.headers.get("Content-Type"), "text/custom-content") 49 | return response!.text() 50 | } 51 | .then { text -> Void in 52 | 53 | XCTAssertEqual(text, "hello") 54 | } 55 | .assertResolves() 56 | } 57 | 58 | func testResponseWithArrayBuffer() { 59 | 60 | let sw = ServiceWorker.createTestWorker(id: self.name) 61 | 62 | sw.evaluateScript(""" 63 | let buffer = new Uint8Array([1,2,3,4]).buffer; 64 | new Response(buffer) 65 | """) 66 | .then { (response: FetchResponseProtocol?) -> Promise in 67 | return response!.data() 68 | } 69 | .then { data -> Void in 70 | let array = [UInt8](data) 71 | XCTAssertEqual(array[0], 1) 72 | XCTAssertEqual(array[1], 2) 73 | XCTAssertEqual(array[2], 3) 74 | XCTAssertEqual(array[3], 4) 75 | } 76 | .assertResolves() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Fetch/FetchHeadersTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class FetchHeadersTests: XCTestCase { 5 | 6 | func testShouldParseJSON() { 7 | 8 | let headersJSON = """ 9 | [ 10 | {"key":"Content-Type", "value": "application/json"}, 11 | {"key":"Cache-Control", "value": "public"}, 12 | {"key":"Cache-Control", "value": "max-age=1"} 13 | ] 14 | """ 15 | 16 | var headers: FetchHeaders? 17 | 18 | XCTAssertNoThrow(headers = try FetchHeaders.fromJSON(headersJSON)) 19 | 20 | XCTAssert(headers!.get("Content-Type")! == "application/json") 21 | 22 | let cacheControl = headers!.getAll("Cache-Control") 23 | 24 | XCTAssertEqual(cacheControl.count, 2) 25 | XCTAssertEqual(cacheControl[1], "max-age=1") 26 | } 27 | 28 | func testShouldAppendGetAndDelete() { 29 | 30 | let fh = FetchHeaders() 31 | fh.append("Test", "Value") 32 | 33 | XCTAssertEqual(fh.get("Test"), "Value") 34 | 35 | fh.append("Test", "Value2") 36 | 37 | XCTAssertEqual(fh.get("Test"), "Value,Value2") 38 | 39 | XCTAssertEqual(fh.getAll("test"), ["Value", "Value2"]) 40 | 41 | fh.set("test", "NEW VALUE") 42 | 43 | XCTAssertEqual(fh.get("test"), "NEW VALUE") 44 | 45 | fh.delete("test") 46 | 47 | XCTAssertNil(fh.get("Test")) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Fetch/FetchPerformance.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import GCDWebServers 4 | import Gzip 5 | import JavaScriptCore 6 | import PromiseKit 7 | 8 | class FetchPerformance: XCTestCase { 9 | 10 | override func setUp() { 11 | super.setUp() 12 | URLCache.shared.removeAllCachedResponses() 13 | TestWeb.createServer() 14 | 15 | let testData = Data(count: 327_680) 16 | 17 | TestWeb.server!.addHandler(forMethod: "GET", path: "/data", request: GCDWebServerRequest.self) { (_) -> GCDWebServerResponse? in 18 | let res = GCDWebServerDataResponse(data: testData, contentType: "anything") 19 | return res 20 | } 21 | } 22 | 23 | override func tearDown() { 24 | TestWeb.destroyServer() 25 | super.tearDown() 26 | } 27 | 28 | func testNative() { 29 | // This is an example of a performance test case. 30 | self.measure { 31 | 32 | let data = try! Data(contentsOf: TestWeb.serverURL.appendingPathComponent("data")) 33 | } 34 | } 35 | 36 | func testFetch() { 37 | self.measure { 38 | FetchSession.default.fetch(TestWeb.serverURL.appendingPathComponent("data")) 39 | .then { res in 40 | res.data() 41 | } 42 | .assertResolves() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Fetch/FetchRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class FetchRequestTests: XCTestCase { 5 | 6 | func testShouldConstructAbsoluteURL() { 7 | 8 | let sw = TestWorker(id: "TEST", state: .activated, url: URL(string: "http://www.example.com/sw.js")!, content: "") 9 | 10 | sw.evaluateScript("new Request('./test')") 11 | .then { (req: FetchRequest?) -> Void in 12 | XCTAssertNotNil(req) 13 | XCTAssertEqual(req?.url.absoluteString, "http://www.example.com/test") 14 | } 15 | .assertResolves() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/GlobalScope/ConsoleMirrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import PromiseKit 4 | import JavaScriptCore 5 | 6 | class ConsoleMirrorTests: XCTestCase { 7 | 8 | override func tearDown() { 9 | Log.debug = { NSLog($0) } 10 | Log.info = { NSLog($0) } 11 | Log.warn = { NSLog($0) } 12 | Log.error = { NSLog($0) } 13 | } 14 | 15 | func testShouldMirrorAllLevels() { 16 | 17 | var functionsRun: Set = [] 18 | 19 | let testWorker = ServiceWorker.createTestWorker(id: name) 20 | 21 | // We need to do this first because the exec environment creation uses 22 | // the logging. 23 | 24 | testWorker.getExecutionEnvironment() 25 | .then { _ -> Promise in 26 | 27 | Log.info = { msg in 28 | XCTAssertEqual(msg, "info-test") 29 | functionsRun.insert("info") 30 | Log.info = nil 31 | } 32 | 33 | Log.debug = { msg in 34 | XCTAssertEqual(msg, "debug-test") 35 | functionsRun.insert("debug") 36 | Log.debug = nil 37 | } 38 | 39 | Log.warn = { msg in 40 | XCTAssertEqual(msg, "warn-test") 41 | functionsRun.insert("warn") 42 | Log.warn = nil 43 | } 44 | 45 | Log.error = { msg in 46 | XCTAssertEqual(msg, "error-test") 47 | functionsRun.insert("error") 48 | Log.error = nil 49 | } 50 | 51 | return testWorker.evaluateScript(""" 52 | console.info('info-test'); 53 | console.debug('debug-test'); 54 | console.warn('warn-test'); 55 | console.error('error-test'); 56 | """) 57 | } 58 | .then { _ -> Void in 59 | 60 | XCTAssert(functionsRun.contains("info"), "Info") 61 | XCTAssert(functionsRun.contains("debug"), "Debug") 62 | XCTAssert(functionsRun.contains("warn"), "Warn") 63 | XCTAssert(functionsRun.contains("error"), "Error") 64 | } 65 | .assertResolves() 66 | } 67 | 68 | func testShouldMirrorObjects() { 69 | 70 | let expect = expectation(description: "Should log") 71 | 72 | Log.debug = { msg in 73 | XCTAssert(msg.contains("test = looks;")) 74 | XCTAssert(msg.contains("like = this;")) 75 | expect.fulfill() 76 | } 77 | 78 | let testWorker = ServiceWorker.createTestWorker(id: name) 79 | 80 | testWorker.evaluateScript("console.debug({test:'looks', like: 'this'})") 81 | .then { 82 | self.wait(for: [expect], timeout: 1) 83 | } 84 | .assertResolves() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/GlobalScope/ExecutionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class ExecutionTests: XCTestCase { 5 | 6 | // func testAsyncDispatch() { 7 | // // Trying to work out why variables sometimes don't exist 8 | // 9 | // let worker = ServiceWorker.createTestWorker(id: name, content: """ 10 | // var test = "hello" 11 | // """) 12 | // 13 | // worker.evaluateScript("test") 14 | // .then { jsVal -> Void in 15 | // XCTAssertEqual(jsVal!.toString(), "hello") 16 | // } 17 | // .assertResolves() 18 | // } 19 | } 20 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/GlobalScope/GlobalScopeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import JavaScriptCore 4 | 5 | class GlobalScopeTests: XCTestCase { 6 | 7 | func testCanAccessGlobalVariables() { 8 | 9 | let sw = ServiceWorker.createTestWorker(id: name) 10 | 11 | sw.evaluateScript("location.host") 12 | .then { (val: String?) in 13 | XCTAssertEqual(val, "www.example.com") 14 | } 15 | .assertResolves() 16 | } 17 | 18 | func testEventListenersWork() { 19 | 20 | let sw = ServiceWorker.createTestWorker(id: name) 21 | sw.withJSContext { context in 22 | 23 | // should be accessible globally and in self. 24 | context.evaluateScript(""" 25 | var fired = 0; 26 | self.addEventListener("test", function() { 27 | fired++ 28 | }) 29 | 30 | addEventListener("test", function() { 31 | fired++ 32 | }) 33 | """) 34 | } 35 | .then { 36 | let ev = ExtendableEvent(type: "test") 37 | return sw.dispatchEvent(ev) 38 | } 39 | .then { 40 | return sw.withJSContext { context in 41 | XCTAssertEqual(context.objectForKeyedSubscript("fired").toInt32(), 2) 42 | } 43 | } 44 | .assertResolves() 45 | } 46 | 47 | func testEventListenersHandleErrors() { 48 | 49 | let sw = ServiceWorker.createTestWorker(id: name) 50 | sw.withJSContext { context in 51 | 52 | // should be accessible globally and in self. 53 | context.evaluateScript(""" 54 | self.addEventListener("activate", function () { 55 | throw new Error("oh no") 56 | }); 57 | """) 58 | } 59 | .then { 60 | let ev = ExtendableEvent(type: "activate") 61 | return sw.dispatchEvent(ev) 62 | } 63 | .then { () -> Int in 64 | return 1 65 | } 66 | .recover { error -> Int in 67 | XCTAssertEqual((error as! ErrorMessage).message, "Error: oh no") 68 | return 0 69 | } 70 | .then { val in 71 | XCTAssertEqual(val, 0) 72 | } 73 | 74 | .assertResolves() 75 | } 76 | 77 | func testAllEventFunctionsAreAdded() { 78 | let sw = ServiceWorker.createTestWorker(id: name) 79 | 80 | let keys = [ 81 | "addEventListener", "removeEventListener", "dispatchEvent", 82 | "self.addEventListener", "self.removeEventListener", "self.dispatchEvent" 83 | ] 84 | 85 | sw.evaluateScript("[\(keys.joined(separator: ","))]") 86 | .then { (val: [Any]?) -> Void in 87 | if let valArray = val { 88 | valArray.enumerated().forEach { arg in 89 | let asJsVal = arg.element as? JSValue 90 | XCTAssert(asJsVal == nil || asJsVal!.isUndefined == true, "Not found: " + keys[arg.offset]) 91 | } 92 | 93 | } else { 94 | XCTFail("Could not get array, val: \(val)") 95 | } 96 | } 97 | .assertResolves() 98 | } 99 | 100 | func testHasLocation() { 101 | 102 | let sw = TestWorker(id: name, state: .activated, url: URL(string: "http://www.example.com/sw.js")!, content: "") 103 | 104 | sw.evaluateScript("[self.location, location]") 105 | .then { (arr: [WorkerLocation]?) -> Void in 106 | XCTAssertNotNil(arr?[0]) 107 | XCTAssertNotNil(arr?[1]) 108 | XCTAssertEqual(arr?[0], arr?[1]) 109 | XCTAssertEqual(arr?[0].href, "http://www.example.com/sw.js") 110 | } 111 | .assertResolves() 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/GlobalScope/TimeoutTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import PromiseKit 4 | 5 | class TimeoutTests: XCTestCase { 6 | 7 | func promiseDelay(delay: Double) -> Promise { 8 | return Promise { fulfill, _ in 9 | DispatchQueue.main.asyncAfter(deadline: .now() + (delay / 1000), execute: { 10 | fulfill(()) 11 | }) 12 | } 13 | } 14 | 15 | func testSetTimeout() { 16 | let sw = ServiceWorker.createTestWorker(id: name) 17 | 18 | sw.evaluateScript(""" 19 | 20 | var ticks = 0; 21 | 22 | setTimeout(function() { 23 | ticks++; 24 | }, 10); 25 | 26 | setTimeout(function() { 27 | ticks++; 28 | }, 30); 29 | 30 | """) 31 | .then { 32 | return self.promiseDelay(delay: 20) 33 | } 34 | .then { 35 | return sw.evaluateScript("ticks") 36 | } 37 | .then { (response: Int) -> Void in 38 | XCTAssertEqual(response, 1) 39 | } 40 | .assertResolves() 41 | } 42 | 43 | // func testSetTimeoutWithArguments() { 44 | // let sw = ServiceWorker.createTestWorker(id: name) 45 | // 46 | // sw.evaluateScript(""" 47 | // new Promise((fulfill,reject) => { 48 | // setTimeout(function(one,two) { 49 | // fulfill([one,two]) 50 | // },10,"one","two") 51 | // }); 52 | // 53 | // """) 54 | // .then { jsVal in 55 | // return JSPromise.fromJSValue(jsVal!) 56 | // } 57 | // .then { (response: [String]?) -> Void in 58 | // XCTAssertEqual(response?[0], "one") 59 | // XCTAssertEqual(response?[1], "two") 60 | // } 61 | // 62 | // .assertResolves() 63 | // } 64 | 65 | func testSetInterval() { 66 | let sw = ServiceWorker.createTestWorker(id: name) 67 | 68 | sw.evaluateScript(""" 69 | 70 | var ticks = 0; 71 | 72 | var interval = setInterval(function() { 73 | ticks++; 74 | }, 10); 75 | 76 | """) 77 | .then { 78 | return self.promiseDelay(delay: 25) 79 | } 80 | .then { 81 | return sw.evaluateScript("clearInterval(interval); ticks") 82 | } 83 | .then { (response: Int?) -> Promise in 84 | XCTAssertEqual(response, 2) 85 | // check clearInterval works 86 | return self.promiseDelay(delay: 10) 87 | } 88 | .then { () -> Promise in 89 | return sw.evaluateScript("ticks") 90 | } 91 | .then { (response: Int) -> Void in 92 | XCTAssertEqual(response, 2) 93 | } 94 | .assertResolves() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/GlobalScope/URLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class URLTests: XCTestCase { 5 | 6 | func testURLExists() { 7 | 8 | let sw = ServiceWorker.createTestWorker(id: name) 9 | 10 | sw.evaluateScript("typeof(URL) != 'undefined' && self.URL == URL") 11 | .then { (val: Bool?) in 12 | 13 | XCTAssertEqual(val, true) 14 | } 15 | .assertResolves() 16 | } 17 | 18 | func testURLHashExists() { 19 | 20 | let sw = ServiceWorker.createTestWorker(id: name) 21 | 22 | sw.evaluateScript("new URL('http://www.example.com/#test').hash") 23 | .then { (val: String?) in 24 | 25 | XCTAssertEqual(val, "#test") 26 | } 27 | .assertResolves() 28 | } 29 | 30 | func testURLHashCanBeSet() { 31 | 32 | let sw = ServiceWorker.createTestWorker(id: name) 33 | 34 | sw.evaluateScript("let url = new URL('http://www.example.com/#test'); url.hash = 'test2'; url.hash") 35 | .then { (val: String?) in 36 | 37 | XCTAssertEqual(val, "#test2") 38 | } 39 | .assertResolves() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrincipalClass 6 | ServiceWorkerTests.TestBootstrap 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Messaging/MessageChannelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import JavaScriptCore 4 | 5 | class MessageChannelTests: XCTestCase { 6 | 7 | func testMessageChannelInJS() { 8 | let channel = MessageChannel() 9 | let jsc = JSContext()! 10 | jsc.setObject(channel, forKeyedSubscript: "testChannel" as (NSCopying & NSObjectProtocol)!) 11 | 12 | jsc.evaluateScript(""" 13 | var didFire = false; 14 | testChannel.port2.onmessage = function() { 15 | didFire = true 16 | } 17 | testChannel.port1.postMessage("hi"); 18 | """) 19 | 20 | XCTAssertTrue(jsc.objectForKeyedSubscript("didFire").toBool()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Messaging/MessagePortTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import JavaScriptCore 4 | 5 | class MessagePortTests: XCTestCase { 6 | 7 | func testSendingAMessage() { 8 | let portOne = SWMessagePort() 9 | let portTwo = SWMessagePort() 10 | 11 | portOne.targetPort = portTwo 12 | 13 | var fired = false 14 | let listener = portTwo.addEventListener("message") { (ev: ExtendableMessageEvent) in 15 | 16 | let dict = ev.data as! [String: Any] 17 | 18 | XCTAssertEqual(dict["hello"] as? String, "there") 19 | fired = true 20 | } 21 | 22 | portOne.postMessage([ 23 | "hello": "there" 24 | ]) 25 | 26 | portTwo.start() 27 | 28 | XCTAssertTrue(fired) 29 | } 30 | 31 | func testAutoStartOnMessageSetter() { 32 | let portOne = SWMessagePort() 33 | let portTwo = SWMessagePort() 34 | 35 | portOne.targetPort = portTwo 36 | portTwo.targetPort = portOne 37 | 38 | let jsc = JSContext()! 39 | jsc.setObject(portTwo, forKeyedSubscript: "testPort" as NSCopying & NSObjectProtocol) 40 | portOne.postMessage(["hello": "there"]) 41 | 42 | jsc.evaluateScript(""" 43 | var fireResponse = null 44 | testPort.onmessage = function(e) { 45 | fireResponse = e.data.hello; 46 | } 47 | """) 48 | 49 | XCTAssertEqual(jsc.objectForKeyedSubscript("fireResponse")!.toString(), "there") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/PerformanceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | 4 | class PerformanceTests: XCTestCase { 5 | 6 | // func testPerformanceExample() { 7 | // // This is an example of a performance test case. 8 | // self.measure { 9 | // let testSw = ServiceWorker.createTestWorker(id: "PERFORMANCE") 10 | // testSw.evaluateScript("console.log('hi')") 11 | // .assertResolves() 12 | // } 13 | // } 14 | } 15 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/ServiceWorkerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import JavaScriptCore 4 | import PromiseKit 5 | 6 | class ServiceWorkerTests: XCTestCase { 7 | 8 | func testLoadContentFunction() { 9 | 10 | let sw = ServiceWorker.createTestWorker(id: name, content: "var testValue = 'hello';") 11 | 12 | return sw.evaluateScript("testValue") 13 | .then { (val: String?) -> Void in 14 | XCTAssertEqual(val, "hello") 15 | } 16 | .assertResolves() 17 | } 18 | 19 | func testThreadFreezing() { 20 | 21 | let sw = ServiceWorker.createTestWorker(id: name, content: "var testValue = 'hello';") 22 | 23 | sw.withJSContext { _ in 24 | 25 | let semaphore = DispatchSemaphore(value: 0) 26 | 27 | DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: { 28 | 29 | Log.info?("signalling") 30 | semaphore.signal() 31 | }) 32 | 33 | DispatchQueue.global().async { 34 | Promise(value: ()) 35 | .then { 36 | Log.info?("doing this now") 37 | } 38 | } 39 | 40 | Log.info?("waiting") 41 | semaphore.wait() 42 | } 43 | .assertResolves() 44 | } 45 | 46 | func testThreadFreezingInJS() { 47 | 48 | let sw = ServiceWorker.createTestWorker(id: name, content: "var testValue = 'hello';") 49 | 50 | let run: @convention(block) () -> Void = { 51 | let semaphore = DispatchSemaphore(value: 0) 52 | DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: { 53 | 54 | Promise(value: ()) 55 | .then { () -> Void in 56 | Log.info?("signalling") 57 | semaphore.signal() 58 | } 59 | 60 | }) 61 | Log.info?("wait") 62 | semaphore.wait() 63 | } 64 | 65 | sw.withJSContext { context in 66 | 67 | context.globalObject.setValue(run, forProperty: "testFunc") 68 | } 69 | .then { 70 | return sw.evaluateScript("testFunc()") 71 | } 72 | .then { () -> Void in 73 | // compiler needs this to be here 74 | } 75 | .assertResolves() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Stream/PassthroughStream.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ServiceWorker 3 | 4 | class PassthroughStreamTests: XCTestCase { 5 | 6 | func testPassthroughStream() { 7 | 8 | let originalData = "TEST DATA".data(using: String.Encoding.utf8)! 9 | 10 | let originalInput = InputStream(data: originalData) 11 | 12 | let (passthroughInput, passthroughOutput) = PassthroughStream.create() 13 | 14 | let finalOutput = OutputStream.toMemory() 15 | 16 | StreamPipe.pipe(from: originalInput, to: passthroughOutput, bufferSize: 1) 17 | .then { _ in 18 | 19 | StreamPipe.pipe(from: passthroughInput, to: finalOutput, bufferSize: 1) 20 | } 21 | .then { () -> Void in 22 | 23 | let data = finalOutput.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as! Data 24 | let str = String(data: data, encoding: String.Encoding.utf8) 25 | 26 | XCTAssertEqual(str, "TEST DATA") 27 | } 28 | .assertResolves() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/Stream/StreamPipeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorker 3 | import PromiseKit 4 | 5 | class StreamPipeTests: XCTestCase { 6 | 7 | func testPipingAStream() { 8 | 9 | let testData = "THIS IS TEST DATA".data(using: String.Encoding.utf8)! 10 | 11 | let inputStream = InputStream(data: testData) 12 | let outputStream = OutputStream.toMemory() 13 | 14 | StreamPipe.pipe(from: inputStream, to: outputStream, bufferSize: 1) 15 | .then { _ -> Void in 16 | 17 | let transferredData = outputStream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as! Data 18 | 19 | let str = String(data: transferredData, encoding: String.Encoding.utf8) 20 | 21 | XCTAssertEqual(str, "THIS IS TEST DATA") 22 | } 23 | .assertResolves() 24 | } 25 | 26 | func testPipingAStreamOffMainThread() { 27 | 28 | let testData = "THIS IS TEST DATA".data(using: String.Encoding.utf8)! 29 | 30 | let inputStream = InputStream(data: testData) 31 | let outputStream = OutputStream.toMemory() 32 | 33 | let (promise, fulfill, reject) = Promise.pending() 34 | 35 | let queue = DispatchQueue.global() 36 | 37 | DispatchQueue.global().async { 38 | StreamPipe.pipe(from: inputStream, to: outputStream, bufferSize: 1) 39 | .then { _ -> Void in 40 | 41 | let transferredData = outputStream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as! Data 42 | 43 | let str = String(data: transferredData, encoding: String.Encoding.utf8) 44 | 45 | XCTAssertEqual(str, "THIS IS TEST DATA") 46 | fulfill(()) 47 | } 48 | .catch { error in 49 | reject(error) 50 | } 51 | } 52 | 53 | promise.assertResolves() 54 | } 55 | 56 | func testPipingToMultipleStreams() { 57 | 58 | let testData = "THIS IS TEST DATA".data(using: String.Encoding.utf8)! 59 | 60 | let inputStream = InputStream(data: testData) 61 | let outputStream = OutputStream.toMemory() 62 | let outputStream2 = OutputStream.toMemory() 63 | 64 | let streamPipe = StreamPipe(from: inputStream, bufferSize: 1) 65 | XCTAssertNoThrow(try streamPipe.add(stream: outputStream)) 66 | XCTAssertNoThrow(try streamPipe.add(stream: outputStream2)) 67 | 68 | streamPipe.pipe() 69 | .then { () -> Void in 70 | [outputStream, outputStream2].forEach { stream in 71 | let transferredData = stream.property(forKey: Stream.PropertyKey.dataWrittenToMemoryStreamKey) as! Data 72 | 73 | let str = String(data: transferredData, encoding: String.Encoding.utf8) 74 | 75 | XCTAssertEqual(str, "THIS IS TEST DATA") 76 | } 77 | } 78 | .assertResolves() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ServiceWorker/ServiceWorkerTests/ZZZZ_TestEndChecks.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PromiseKit 3 | @testable import ServiceWorker 4 | import JavaScriptCore 5 | 6 | class ZZZZ_TestEndChecks: XCTestCase { 7 | 8 | /// A wrap-up test we always want to run last, that double-checks all of our JSContexts 9 | /// have been garbage collected. If they haven't, it means we have a memory leak somewhere. 10 | func testShouldDeinitSuccessfully() { 11 | 12 | let queues = ServiceWorkerExecutionEnvironment.contexts 13 | 14 | Promise(value: ()) 15 | .then { () -> Promise in 16 | 17 | let allContexts = ServiceWorkerExecutionEnvironment.contexts.keyEnumerator().allObjects as! [JSContext] 18 | 19 | allContexts.forEach { context in 20 | NSLog("Still active context: \(context.name)") 21 | } 22 | if allContexts.count > 0 { 23 | throw ErrorMessage("Contexts still exist") 24 | } 25 | 26 | let worker = ServiceWorker.createTestWorker(id: self.name) 27 | return worker.getExecutionEnvironment() 28 | .then { _ -> Void in 29 | XCTAssertEqual(ServiceWorkerExecutionEnvironment.contexts.keyEnumerator().allObjects.count, 1) 30 | } 31 | 32 | }.then { _ -> Promise in 33 | 34 | Promise { fulfill, _ in 35 | 36 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { 37 | NSLog("Performing check") 38 | 39 | queues.objectEnumerator()!.forEach { _ in 40 | NSLog("valll") 41 | } 42 | 43 | queues.keyEnumerator().forEach { key in 44 | let val = queues.object(forKey: key as! JSContext) 45 | NSLog("WHAAT") 46 | } 47 | 48 | XCTAssertEqual(ServiceWorkerExecutionEnvironment.contexts.keyEnumerator().allObjects.count, 0) 49 | fulfill(()) 50 | }) 51 | } 52 | } 53 | .assertResolves() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ServiceWorker/js-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service-worker-js-src", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "indexeddbshim": "^3.0.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - identifier_name 3 | - line_length 4 | opt_in_rules: 5 | - force_unwrapping 6 | included: 7 | - ServiceWorkerContainer 8 | #force_unwrapping: error 9 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/DatabaseMigrations/cache/1_initial_cache_table.sql: -------------------------------------------------------------------------------- 1 | 2 | PRAGMA foreign_keys = false; 3 | 4 | -- ---------------------------- 5 | -- Table structure for caches 6 | -- ---------------------------- 7 | DROP TABLE IF EXISTS "caches"; 8 | CREATE TABLE "caches" ( 9 | "cache_name" TEXT NOT NULL UNIQUE, 10 | PRIMARY KEY("cache_name") 11 | ); 12 | 13 | 14 | PRAGMA foreign_keys = true; 15 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/DatabaseMigrations/cache/2_cache_entries_table.sql: -------------------------------------------------------------------------------- 1 | 2 | PRAGMA foreign_keys = false; 3 | 4 | -- ---------------------------- 5 | -- Table structure for cache_entries 6 | -- ---------------------------- 7 | DROP TABLE IF EXISTS "cache_entries"; 8 | CREATE TABLE "cache_entries" ( 9 | "cache_name" TEXT NOT NULL, 10 | "method" TEXT NOT NULL, 11 | "request_url_no_query" TEXT NOT NULL, 12 | "request_query" TEXT, 13 | "vary_by_headers" TEXT, 14 | "request_headers" TEXT NOT NULL, 15 | "response_headers" TEXT NOT NULL, 16 | "response_url" TEXT, 17 | "response_type" TEXT NOT NULL, 18 | "response_status" INTEGER NOT NULL, 19 | "response_status_text" TEXT NOT NULL, 20 | "response_redirected" INT NOT NULL, 21 | "response_body" BLOB NOT NULL, 22 | PRIMARY KEY("cache_name", "method", "request_url_no_query", "request_query", "vary_by_headers") 23 | ); 24 | 25 | 26 | PRAGMA foreign_keys = true; 27 | 28 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/DatabaseMigrations/core/1_worker_tables.sql: -------------------------------------------------------------------------------- 1 | PRAGMA foreign_keys = false; 2 | 3 | -- ---------------------------- 4 | -- Table structure for registrations 5 | -- ---------------------------- 6 | DROP TABLE IF EXISTS "registrations"; 7 | CREATE TABLE "registrations" ( 8 | "registration_id" TEXT NOT NULL, 9 | "scope" TEXT NOT NULL UNIQUE, 10 | "active" TEXT, 11 | "installing" TEXT, 12 | "waiting" TEXT, 13 | "redundant" TEXT, 14 | PRIMARY KEY("registration_id"), 15 | CONSTRAINT "active_worker" FOREIGN KEY ("active") REFERENCES "workers" ("worker_id") ON DELETE SET NULL, 16 | CONSTRAINT "installing_worker" FOREIGN KEY ("installing") REFERENCES "workers" ("worker_id") ON DELETE SET NULL, 17 | CONSTRAINT "waiting_worker" FOREIGN KEY ("waiting") REFERENCES "workers" ("worker_id") ON DELETE SET NULL, 18 | CONSTRAINT "redundant_worker" FOREIGN KEY ("redundant") REFERENCES "workers" ("worker_id") ON DELETE SET NULL 19 | ); 20 | 21 | -- ---------------------------- 22 | -- Table structure for workers 23 | -- ---------------------------- 24 | DROP TABLE IF EXISTS "workers"; 25 | CREATE TABLE "workers" ( 26 | "worker_id" text(36,0) NOT NULL, 27 | "url" text NOT NULL, 28 | "registration_id" text NOT NULL, 29 | "headers" text NULL, 30 | "content" blob NULL, 31 | "install_state" integer NOT NULL, 32 | PRIMARY KEY("worker_id") 33 | ); 34 | 35 | PRAGMA foreign_keys = true; 36 | 37 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/DatabaseMigrations/core/2_imported_scripts.sql: -------------------------------------------------------------------------------- 1 | 2 | PRAGMA foreign_keys = false; 3 | 4 | DROP TABLE IF EXISTS "worker_imported_scripts"; 5 | CREATE TABLE "worker_imported_scripts" ( 6 | "worker_id" text NOT NULL, 7 | "url" TEXT NOT NULL, 8 | "headers" text NOT NULL, 9 | "content" blob NOT NULL, 10 | PRIMARY KEY("worker_id","url"), 11 | CONSTRAINT "worker" FOREIGN KEY ("worker_id") REFERENCES "workers" ("worker_id") ON DELETE CASCADE 12 | ); 13 | 14 | PRAGMA foreign_keys = true; 15 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/DatabaseMigrations/core/3_add_content_hashes.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER TABLE "workers" ADD COLUMN content_hash BLOB; 3 | ALTER TABLE "worker_imported_scripts" ADD COLUMN content_hash BLOB; 4 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Events/WorkerInstallationError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | 4 | public struct WorkerInstallationError { 5 | public let worker: ServiceWorker 6 | public let container: ServiceWorkerContainer 7 | public let error: Error 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Registration/RegistrationWorkerSlots.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum RegistrationWorkerSlot: String { 4 | case active 5 | case waiting 6 | case installing 7 | case redundant 8 | } 9 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Registration/ServiceWorkerRegistrationOptions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct ServiceWorkerRegistrationOptions { 4 | public let scope: URL? 5 | 6 | public init(scope: URL?) { 7 | self.scope = scope 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/ServiceWorkerContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceWorkerContainer.h 3 | // ServiceWorkerContainer 4 | // 5 | // Created by alastair.coote on 05/08/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ServiceWorkerContainer. 12 | FOUNDATION_EXPORT double ServiceWorkerContainerVersionNumber; 13 | 14 | //! Project version string for ServiceWorkerContainer. 15 | FOUNDATION_EXPORT const unsigned char ServiceWorkerContainerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Storage/CoreDatabase.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// CoreDatabase.swift 3 | //// ServiceWorkerContainer 4 | //// 5 | //// Created by alastair.coote on 24/07/2017. 6 | //// Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | //// 8 | // 9 | // import Foundation 10 | // import PromiseKit 11 | // import ServiceWorker 12 | // 13 | // public class CoreDatabasea { 14 | // 15 | // // public static let dbPath = SharedResources.appGroupStorage.appendingPathComponent("core.db") 16 | // public static var dbDirectory: URL? 17 | // 18 | // static var dbPath: URL? { 19 | // return self.dbDirectory?.appendingPathComponent("core.db") 20 | // } 21 | // 22 | // /// The migrations only change with a new version of the app, so as long as we've 23 | // /// checked for migrations once per app launch, we're OK to not check again 24 | // static var dbMigrationCheckDone = false 25 | // 26 | // fileprivate static func doMigrationCheck() throws { 27 | // 28 | // guard let dbPath = self.dbPath else { 29 | // throw ErrorMessage("CoreDatabase.dbPath must be set on app startup") 30 | // } 31 | // 32 | // if self.dbMigrationCheckDone == false { 33 | // 34 | // Log.info?("Migration check for core DB not done yet, doing it now...") 35 | // 36 | // let migrations = URL(fileURLWithPath: Bundle(for: CoreDatabase.self).bundlePath, isDirectory: true) 37 | // .appendingPathComponent("DatabaseMigrations", isDirectory: true) 38 | // .appendingPathComponent("core", isDirectory: true) 39 | // 40 | // // This might be the first time it's being run, in which case, we need to ensure we have the 41 | // // directory structure ready. 42 | // try FileManager.default.createDirectory(at: dbPath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) 43 | // 44 | // _ = try DatabaseMigration.check(dbPath: dbPath, migrationsPath: migrations) 45 | // dbMigrationCheckDone = true 46 | // } 47 | // } 48 | // 49 | // // OK, running into lock issues so now we're sticking with one connection. 50 | // fileprivate static var conn: SQLiteConnection? 51 | // 52 | // public static func inConnection(_ cb: (SQLiteConnection) throws -> T) throws -> T { 53 | // 54 | // guard let dbPath = self.dbPath else { 55 | // throw ErrorMessage("CoreDatabase.dbPath must be set on app startup") 56 | // } 57 | // 58 | // try self.doMigrationCheck() 59 | // 60 | // return try cb(self.getOrCreateConnection(dbPath)) 61 | // } 62 | // 63 | // fileprivate static func getOrCreateConnection(_ dbPath: URL) throws -> SQLiteConnection { 64 | // if let conn = self.conn { 65 | // return conn 66 | // } else { 67 | // let conn = try SQLiteConnection(dbPath) 68 | // self.conn = conn 69 | // return conn 70 | // } 71 | // } 72 | // 73 | // public static func inConnection(_ cb: @escaping (SQLiteConnection) throws -> Promise) -> Promise { 74 | // 75 | // return firstly { 76 | // 77 | // guard let dbPath = self.dbPath else { 78 | // throw ErrorMessage("CoreDatabase.dbPath must be set on app startup") 79 | // } 80 | // try self.doMigrationCheck() 81 | // 82 | // return try cb(self.getOrCreateConnection(dbPath)) 83 | // } 84 | // } 85 | // 86 | // static func createConnection() throws -> SQLiteConnection { 87 | // guard let dbPath = self.dbPath else { 88 | // throw ErrorMessage("CoreDatabase.dbPath must be set on app startup") 89 | // } 90 | // return try SQLiteConnection(dbPath) 91 | // } 92 | // } 93 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Storage/DBConnectionPool.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorker 3 | import PromiseKit 4 | 5 | /// Still trying to figure out the best trade-off between memory and performance for keeping 6 | /// DB connections hanging around. But this pool ensures that we only ever have one connection 7 | /// open for a URL at a time, and automatically closes them when we're done. In the future, we 8 | /// can customise behaviour here depending on whether we're in the Notification Extension or 9 | /// the full app, maybe. 10 | class DBConnectionPool { 11 | 12 | fileprivate static var currentOpenConnections = NSHashTable.weakObjects() 13 | 14 | // Rather than do a migration check for every opened connection (expensive!) we keep track of which 15 | // ones we've already checked in this session. If we were opening a lot of connections this would 16 | // be a problem, but we don't (yet, anyway) so this will do for now. 17 | fileprivate static var checkedMigrations = Set() 18 | 19 | static func inConnection(at url: URL, type: DatabaseType, _ callback: (SQLiteConnection) throws -> Promise) -> Promise { 20 | 21 | return firstly { 22 | let connection = try DBConnectionPool.getConnection(for: url, type: type) 23 | return try callback(connection) 24 | } 25 | } 26 | 27 | static func inConnection(at url: URL, type: DatabaseType, _ callback: (SQLiteConnection) throws -> T) throws -> T { 28 | 29 | let connection = try DBConnectionPool.getConnection(for: url, type: type) 30 | return try callback(connection) 31 | } 32 | 33 | fileprivate static func getConnection(for url: URL, type: DatabaseType) throws -> SQLiteConnection { 34 | 35 | let existing = currentOpenConnections.allObjects.first(where: { $0.url.absoluteString == url.absoluteString }) 36 | 37 | if let doesExist = existing { 38 | return doesExist 39 | } 40 | 41 | if self.checkedMigrations.contains(url.absoluteString) == false { 42 | // Need to perform migration checks, but exactly what we run depends on what DB type 43 | // we are creating. Potential problem here with running different DB types at the same 44 | // URL. 45 | try self.checkMigrationsFor(dbPath: url, type: type) 46 | self.checkedMigrations.insert(url.absoluteString) 47 | } 48 | 49 | let newConnection = try SQLiteConnection(url) 50 | self.currentOpenConnections.add(newConnection) 51 | return newConnection 52 | } 53 | 54 | fileprivate static func checkMigrationsFor(dbPath: URL, type: DatabaseType) throws { 55 | Log.info?("Migration check for \(dbPath.path) not done yet, doing it now...") 56 | 57 | let migrations = URL(fileURLWithPath: Bundle(for: DBConnectionPool.self).bundlePath, isDirectory: true) 58 | .appendingPathComponent("DatabaseMigrations", isDirectory: true) 59 | .appendingPathComponent(type.rawValue, isDirectory: true) 60 | 61 | // This might be the first time it's being run, in which case, we need to ensure we have the 62 | // directory structure ready. 63 | try FileManager.default.createDirectory(at: dbPath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) 64 | 65 | _ = try DatabaseMigration.check(dbPath: dbPath, migrationsPath: migrations) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Storage/DatabaseType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum DatabaseType: String { 4 | case core 5 | case cache 6 | } 7 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainer/Storage/SharedResources.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// In order to share data between our notification extension and our main app, we have to use an app group. 4 | /// This is a quick utility class to avoid having to type the app group name everywhere. 5 | public class SharedResources { 6 | 7 | public static var appGroupName: String? { 8 | return SharedResources.appBundle?.object(forInfoDictionaryKey: "HYBRID_APP_GROUP") as? String 9 | } 10 | 11 | public static var appGroupStorage: URL? { 12 | guard let groupName = appGroupName else { 13 | return nil 14 | } 15 | return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupName)?.appendingPathComponent("hybrid/") 16 | } 17 | 18 | /// Because we have content extensions, mainBundle() can sometimes return an extension 19 | /// rather than the app itself. This function detects that, and resets it, so we know for 20 | /// sure that we are always receiving the app bundle. 21 | /// 22 | /// - Returns: An NSBundle for the main hybrid app 23 | public static var appBundle: Bundle? { 24 | var bundle: Bundle? = Bundle.main 25 | if Bundle.main.bundleURL.pathExtension == "appex" { 26 | // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex 27 | 28 | let backTwoURL = Bundle.main.bundleURL.deletingLastPathComponent().deletingLastPathComponent() 29 | 30 | bundle = Bundle(url: backTwoURL) 31 | } 32 | return bundle 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainerTests/Bootstrap.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ServiceWorkerContainer 3 | import XCTest 4 | import ServiceWorker 5 | 6 | class TestBootstrap: NSObject { 7 | 8 | override init() { 9 | super.init() 10 | 11 | Log.error = { NSLog($0) } 12 | Log.warn = { NSLog($0) } 13 | Log.info = { NSLog($0) } 14 | Log.debug = { NSLog($0) } 15 | 16 | do { 17 | CoreDatabase.dbDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("testDB", isDirectory: true) 18 | if FileManager.default.fileExists(atPath: CoreDatabase.dbDirectory!.path) == false { 19 | try FileManager.default.createDirectory(at: CoreDatabase.dbDirectory!, withIntermediateDirectories: true, attributes: nil) 20 | } 21 | } catch { 22 | XCTFail("\(error)") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrincipalClass 6 | ServiceWorkerContainerTests.TestBootstrap 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ServiceWorkerContainer/ServiceWorkerContainerTests/ServiceWorkerContainerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ServiceWorkerContainer 3 | import PromiseKit 4 | 5 | class ServiceWorkerContainerTests: XCTestCase { 6 | 7 | override func setUp() { 8 | super.setUp() 9 | CoreDatabase.clearForTests() 10 | 11 | // Put setup code here. This method is called before the invocation of each test method in the class. 12 | } 13 | 14 | override func tearDown() { 15 | // Put teardown code here. This method is called after the invocation of each test method in the class. 16 | super.tearDown() 17 | } 18 | 19 | let factory = WorkerRegistrationFactory(withWorkerFactory: WorkerFactory()) 20 | 21 | func testContainerCreation() { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | 25 | XCTAssertNoThrow(try { 26 | let testContainer = try ServiceWorkerContainer(forURL: URL(string: "https://www.example.com")!, withFactory: factory) 27 | XCTAssert(testContainer.url.absoluteString == "https://www.example.com") 28 | }()) 29 | } 30 | 31 | func testGetRegistrations() { 32 | 33 | firstly { () -> Promise in 34 | let reg1 = try factory.create(scope: URL(string: "https://www.example.com/scope1")!) 35 | let reg2 = try factory.create(scope: URL(string: "https://www.example.com/scope2")!) 36 | let container = try ServiceWorkerContainer(forURL: URL(string: "https://www.example.com/scope3")!, withFactory: factory) 37 | return container.getRegistrations() 38 | .then { registrations -> Void in 39 | XCTAssertEqual(registrations.count, 2) 40 | XCTAssertEqual(registrations[0], reg1) 41 | XCTAssertEqual(registrations[1], reg2) 42 | } 43 | } 44 | .assertResolves() 45 | } 46 | 47 | func testGetRegistration() { 48 | firstly { () -> Promise in 49 | _ = try factory.create(scope: URL(string: "https://www.example.com/scope1/")!) 50 | let reg1 = try factory.create(scope: URL(string: "https://www.example.com/scope1/scope2/")!) 51 | _ = try factory.create(scope: URL(string: "https://www.example.com/scope1/scope2/file2.html")!) 52 | let container = try ServiceWorkerContainer(forURL: URL(string: "https://www.example.com/scope1/scope2/file.html")!, withFactory: factory) 53 | return container.getRegistration() 54 | .then { registration -> Void in 55 | XCTAssertEqual(registration, reg1) 56 | } 57 | } 58 | .assertResolves() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TestUtilities/GlobalContextMessingAround.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalContextMessingAround.swift 3 | // ServiceWorkerTests 4 | // 5 | // Created by alastair.coote on 13/09/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import JavaScriptCore 11 | 12 | class GlobalContextMessingAround: XCTestCase { 13 | 14 | let blah = "BLAH" 15 | 16 | // func testCreateGlobalContext() { 17 | // 18 | // let ctx = JSContext()! 19 | // 20 | // let toApply: [String: Any] = [ 21 | // "hurr": "durr", 22 | // "blah": self.blah 23 | // ] 24 | // 25 | // let generator = { (obj:Any) -> @convention(block) () -> Any in 26 | // return { 27 | // return obj 28 | // } 29 | // } 30 | // 31 | // toApply.forEach { (key, val) in 32 | // ctx.globalObject.defineProperty(key, descriptor: [ 33 | // "get": generator(val) 34 | // ]) 35 | // } 36 | // 37 | // 38 | // 39 | // ctx.evaluateScript("debugger;") 40 | // 41 | // } 42 | 43 | // func testConvoluted() { 44 | // 45 | //// let blah: @convention(block) () -> String = { 46 | //// return "durr" 47 | //// } 48 | //// 49 | //// 50 | // var definition = kJSClassDefinitionEmpty; 51 | // definition.getProperty = { (ctx, obj, nameRef, exception) -> JSValueRef? in 52 | // 53 | // return JSStringCreateWithCFString("durr" as CFString) 54 | // 55 | // 56 | // } 57 | // 58 | // definition.getPropertyNames = { (ctx, obj, accumulator) in 59 | // JSPropertyNameAccumulatorAddName(accumulator, JSStringCreateWithCFString("hurr" as CFString)) 60 | // } 61 | // 62 | // 63 | // var def = JSClassDefinition(version: 1, attributes: JSClassAttributes(kJSPropertyAttributeNone), className: "WorkerGlobalContext", parentClass: nil, staticValues: nil, staticFunctions: nil, initialize: nil, finalize: nil, hasProperty: nil, getProperty: { (ctx, obj, nameRef, exception) -> JSValueRef? in 64 | // 65 | // return JSStringCreateWithCFString("durr" as CFString) 66 | // 67 | // 68 | // }, setProperty: nil, deleteProperty: nil, getPropertyNames: nil, callAsFunction: nil, callAsConstructor: nil, hasInstance: nil, convertToType: nil) 69 | // 70 | // let cl = JSClassCreate(&definition) 71 | // 72 | // let global = JSGlobalContextCreate 73 | // JSGlobalContextRetain(global) 74 | // let ctx = JSContext(jsGlobalContextRef: global!)! 75 | // 76 | // ctx.evaluateScript("debugger") 77 | // 78 | // } 79 | } 80 | -------------------------------------------------------------------------------- /TestUtilities/PromiseAssert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PromiseAssert.swift 3 | // hybrid 4 | // 5 | // Created by alastair.coote on 28/07/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PromiseKit 11 | import XCTest 12 | 13 | extension Promise { 14 | func assertResolves() { 15 | 16 | let expect = XCTestExpectation(description: "Promise resolves") 17 | let waiter = XCTWaiter() 18 | then { (_) in 19 | expect.fulfill() 20 | }.catch { error in 21 | XCTFail("\(error)") 22 | expect.fulfill() 23 | } 24 | 25 | waiter.wait(for: [expect], timeout: 30) 26 | } 27 | 28 | func assertRejects() { 29 | 30 | let expect = XCTestExpectation(description: "Promise resolves") 31 | 32 | then { _ in 33 | XCTFail("Promise should reject") 34 | }.catch { _ in 35 | expect.fulfill() 36 | } 37 | 38 | let waiter = XCTWaiter() 39 | waiter.wait(for: [expect], timeout: 1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TestUtilities/ServiceWorkerContainerExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceWorkerContainerExtensions.swift 3 | // ServiceWorkerContainerTests 4 | // 5 | // Created by alastair.coote on 08/08/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import ServiceWorkerContainer 11 | import XCTest 12 | import ServiceWorker 13 | 14 | extension CoreDatabase { 15 | static func clearForTests() { 16 | 17 | do { 18 | try SQLiteConnection.inConnection(self.dbPath!) { db in 19 | try db.exec(sql: """ 20 | PRAGMA writable_schema = 1; 21 | delete from sqlite_master where type in ('table', 'index', 'trigger'); 22 | PRAGMA writable_schema = 0; 23 | VACUUM; 24 | """) 25 | } 26 | 27 | self.dbMigrationCheckDone = false 28 | } catch { 29 | XCTFail("\(error)") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TestUtilities/ServiceWorkerExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceWorkerExtensions.swift 3 | // hybrid 4 | // 5 | // Created by alastair.coote on 01/08/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import ServiceWorker 11 | 12 | class StaticContentDelegate : NSObject, ServiceWorkerDelegate { 13 | 14 | func serviceWorkerGetDomainStoragePath(_ worker: ServiceWorker) throws -> URL { 15 | return StaticContentDelegate.storageURL 16 | .appendingPathComponent("domains", isDirectory: true) 17 | .appendingPathComponent(worker.url.host!, isDirectory: true) 18 | } 19 | 20 | static let storageURL = URL(fileURLWithPath: NSTemporaryDirectory()) 21 | 22 | func serviceWorker(_: ServiceWorker, importScript: URL, _ callback: @escaping (Error?, String?) -> Void) { 23 | callback(ErrorMessage("not implemented"), nil) 24 | } 25 | 26 | func serviceWorkerGetScriptContent(_: ServiceWorker) throws -> String { 27 | return self.script 28 | } 29 | 30 | func getCoreDatabaseURL() -> URL { 31 | return StaticContentDelegate.storageURL.appendingPathComponent("core.db") 32 | } 33 | 34 | 35 | let script:String 36 | 37 | init(script:String) { 38 | self.script = script 39 | } 40 | } 41 | 42 | class TestWorker : ServiceWorker { 43 | 44 | fileprivate let staticDelegate: ServiceWorkerDelegate 45 | 46 | init(id: String, state: ServiceWorkerInstallState = .activated, url: URL? = nil, content: String = "") { 47 | self.staticDelegate = StaticContentDelegate(script: content) 48 | 49 | let urlToUse = url ?? URL(string: "http://www.example.com/\(ServiceWorker.escapeID(id)).js")! 50 | 51 | super.init(id: id, url: urlToUse, state: state) 52 | self.delegate = self.staticDelegate 53 | } 54 | 55 | } 56 | 57 | extension ServiceWorker { 58 | 59 | fileprivate static func escapeID(_ id: String) -> String { 60 | return id.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 61 | .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 62 | .replacingOccurrences(of: " ", with: "_") 63 | } 64 | 65 | static func createTestWorker(id: String, state: ServiceWorkerInstallState = .activated, content: String = "") -> ServiceWorker { 66 | return TestWorker(id: id, state: state, content: content) 67 | } 68 | 69 | static func createTestWorker(id: String) -> ServiceWorker { 70 | return TestWorker(id: id, state: .activated) 71 | } 72 | 73 | static func createTestWorker(id: String, content: String) -> ServiceWorker { 74 | return TestWorker(id: id, state: .activated, content: content) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /TestUtilities/TestWeb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestWeb.swift 3 | // ServiceWorkerContainerTests 4 | // 5 | // Created by alastair.coote on 22/06/2017. 6 | // Copyright © 2017 Guardian Mobile Innovation Lab. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GCDWebServers 11 | import PromiseKit 12 | 13 | class TestWeb { 14 | 15 | static var server: GCDWebServer? 16 | 17 | static var serverURL: URL { 18 | var url = URLComponents(string: "http://localhost")! 19 | url.port = Int(server!.port) 20 | return url.url! 21 | } 22 | 23 | static func createServer() { 24 | URLCache.shared.removeAllCachedResponses() 25 | self.server = GCDWebServer() 26 | self.server!.start() 27 | } 28 | 29 | static func destroyServer() { 30 | self.server!.stop() 31 | self.server = nil 32 | } 33 | } 34 | --------------------------------------------------------------------------------