├── .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 |
--------------------------------------------------------------------------------