├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── docs └── SWPluginHowTo.md ├── package.json ├── plugin.xml ├── src └── ios │ ├── CDVServiceWorker.h │ ├── CDVServiceWorker.m │ ├── FetchConnectionDelegate.h │ ├── FetchConnectionDelegate.m │ ├── FetchInterceptorProtocol.h │ ├── FetchInterceptorProtocol.m │ ├── ServiceWorkerCache.h │ ├── ServiceWorkerCache.m │ ├── ServiceWorkerCacheApi.h │ ├── ServiceWorkerCacheApi.m │ ├── ServiceWorkerCacheEntry.h │ ├── ServiceWorkerCacheEntry.m │ ├── ServiceWorkerRequest.h │ ├── ServiceWorkerRequest.m │ ├── ServiceWorkerResponse.h │ └── ServiceWorkerResponse.m └── www ├── kamino.js ├── service_worker.js ├── service_worker_container.js ├── service_worker_registration.js └── sw_assets ├── cache.js ├── client.js ├── event.js ├── fetch.js ├── kamino.js └── message.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style of different editors and IDEs. 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .DS_Store 4 | /node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Worker Plugin for iOS 2 | 3 | This plugin adds [Service Worker](https://github.com/slightlyoff/ServiceWorker) support to Cordova apps on iOS. To use it: 4 | 5 | 1. Install this plugin. 6 | 2. Create `sw.js` in your `www/` directory. 7 | 3. Add the following preference to your config.xml file: 8 | 9 | ``` 10 | 11 | ``` 12 | 13 | That's it! Your calls to the ServiceWorker API should now work. 14 | 15 | ## Cordova Asset Cache 16 | 17 | This plugin automatically creates a cache (called `Cordova Assets`) containing all of the assets in your app's `www/` directory. 18 | 19 | To prevent this automatic caching, add the following preference to your config.xml file: 20 | 21 | ``` 22 | 23 | ``` 24 | 25 | ## Examples 26 | 27 | One use case is to check your caches for any fetch request, only attempting to retrieve it from the network if it's not there. 28 | 29 | ``` 30 | self.addEventListener('fetch', function(event) { 31 | event.respondWith( 32 | // Check the caches. 33 | caches.match(event.request).then(function(response) { 34 | // If the response exists, return it; otherwise, fetch it from the network. 35 | return response || fetch(event.request); 36 | }) 37 | ); 38 | }); 39 | ``` 40 | 41 | Another option is to go to the network first, only checking the cache if that fails (e.g. if the device is offline). 42 | 43 | ``` 44 | self.addEventListener('fetch', function(event) { 45 | // If the caches provide a response, return it. Otherwise, return the original network response. 46 | event.respondWith( 47 | // Fetch from the network. 48 | fetch(event.request).then(function(networkResponse) { 49 | // If the response exists and has a 200 status, return it. 50 | if (networkResponse && networkResponse.status === 200) { 51 | return networkResponse; 52 | } 53 | 54 | // The network didn't yield a useful response, so check the caches. 55 | return caches.match(event.request).then(function(cacheResponse) { 56 | // If the cache yielded a response, return it; otherwise, return the original network response. 57 | return cacheResponse || networkResponse; 58 | }); 59 | }) 60 | ); 61 | }); 62 | ``` 63 | 64 | ## Caveats 65 | 66 | * Having multiple Service Workers in your app is unsupported. 67 | * Service Worker uninstallation is unsupported. 68 | * IndexedDB is unsupported. 69 | 70 | ## Release Notes 71 | 72 | ### 1.0.1 73 | 74 | * Significantly enhanced version numbering. 75 | 76 | ### 1.0.0 77 | 78 | * Initial release. 79 | -------------------------------------------------------------------------------- /docs/SWPluginHowTo.md: -------------------------------------------------------------------------------- 1 | #How To Create Plugins for Service Worker on iOS 2 | 3 | Below are some useful pieces of information that can help you create new cordova service worker plugins on iOS. Example code in this document originates from the [background sync](https://github.com/imintz/cordova-plugin-background-sync) plugin. 4 | 5 | ##Setting Up an Event in Service Worker Context: 6 | Create a new folder in the `www` directory of your plugin named `sw_assets`. This folder will contain all of your plugin components for the service worker context. In `sw_assets` create a JavaScript file for your event. 7 | 8 | First, you need to define your event as a global property in the service worker context. 9 | ```javascript 10 | Object.defineProperty(this, ‘onsync’, { 11 | configurable: false, 12 | enumerable: true, 13 | get: eventGetter(‘sync’), 14 | set: eventSetter(‘sync’) 15 | }); 16 | ``` 17 | The above code defines an event property for when the system initiates a background sync. Note that the event property has the prefix “on”, while the getter and setter do not. 18 | 19 | Next, you need to define the event type that will be passed to the service worker when your new event is fired. 20 | ```javascript 21 | function SyncEvent() { 22 | ExtendableEvent.call(this, ‘sync’); 23 | this.registration = new Registration(); 24 | } 25 | SyncEvent.prototype = Object.create(ExtendableEvent.prototype); 26 | SyncEvent.constructor = SyncEvent; 27 | ``` 28 | Here you can initialize any properties that may be needed for every event of this type. In this example, the `SyncEvent` has a `registration` object as a property. 29 | 30 | The above sync event inherits from the `ExtendableEvent` class. An `ExtendableEvent` enables the service worker to invoke the `waitUntil` function from within its event handler. `waitUntil` should take a promise and prevent the device from terminating the service worker until that promise has been settled. Be aware that this does not happen automatically, your plugin is responsible for preserving the service worker until `waitUntil` has been settled. 31 | 32 | As a result, it is necessary to create a custom event firing function when dealing with `ExtendableEvent`s. This allows the plugin to "clean up" after the `waitUntil` promise has been settled. 33 | ```javascript 34 | function FireSyncEvent (data) { 35 | var ev = new SyncEvent(); 36 | ev.registration.tag = data.tag; 37 | dispatchEvent(ev); 38 | if (Array.isArray(ev._promises)) { 39 | return Promise.all(ev._promises).then(function(){ 40 | sendSyncResponse(0, data.tag); 41 | },function(){ 42 | sendSyncResponse(2, data.tag); 43 | }); 44 | } else { 45 | sendSyncResponse(1, data.tag); 46 | return Promise.resolve(); 47 | } 48 | } 49 | ``` 50 | The first thing that happens in this function is the creation of a new event object of the class we just defined. Then the event object is populated with any additional data that may be needed in the service worker script. The event object is dispatched in the service worker context using the `dispatchEvent` function. 51 | 52 | Since our event inherits from the `ExtendableEvent` class, if the service worker calls `waitUntil` on the event, an array of promises called `_promises` will be added to the event object. `_promises` contains whatever promise was used as a parameter when `waitUntil` was called. If `waitUntil` is not called, then `_promises` will be null. 53 | 54 | If `waitUntil` has been called, this function returns a new promise that does not resolve until every promise in `_promises` has been resolved. The rest of this function is executed asynchronously once `Promise.all(ev._promises)` has been settled. If a single promise in `_promises` rejects, then this promise will reject. This promise is dependent on every promise passed to `waitUntil` and you can use `.then` to define what happens after it is settled. In the case of background sync, once all of the promises have been resolved, the function `sendSyncResponse` is called to tell iOS that the background fetch is over successfully and it can put the app back into a suspended state. If one of the promises rejects, then `sendSyncResponse` is called to indicate that the background execution failed and iOS should put the app back into a suspended state. 55 | 56 | If `waitUntil` is never called in the first place, then `Array.isArray(ev._promises)` will fail, and `sendSyncResponse` will be called immediately to tell iOS that no data was found and that the app can be put back into a suspended state without waiting for anything. 57 | 58 | The last thing that needs to be done for your new service worker event is to add it to the plugin.xml. 59 | ```xml 60 | 61 | ``` 62 | This will put your new service worker event in the correct place for when a project is created. 63 | 64 | ##Communicating between Objective C and Service Worker 65 | In your plugin class, you can define a `CDVServiceWorker` property called `serviceWorker`. This will be your access variable for the active service worker instance. Inside your `pluginInitialize` function, include the following line of code: 66 | ```objective-c 67 | self.serviceWorker = [self.commandDelegate getCommandInstance:@"ServiceWorker"]; 68 | ``` 69 | This line returns a pointer to the current active service worker. 70 | 71 | Note: Unless you specify `` in your plugin.xml file, `pluginInitialize` will not be executed until the first cordova exec call. As an alternative to `pluginInitialize`, you can create an initialization function that you explicitly call after your service worker is ready. 72 | 73 | There are two ways to execute JavaScript code in the service worker context from Objective C. If the closure of the code that you are calling is not important, or you want to define some javascript code in an Objective C string, use the `evaluateScript` function. 74 | ```objective-c 75 | NSData *json = [NSJSONSerialization dataWithJSONObject:message options:0 error:&error]; 76 | NSString *dispatchCode = [NSString stringWithFormat:@"FireSyncEvent(%@);", [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]]; 77 | [serviceWorker.context evaluateScript:dispatchCode]; 78 | ``` 79 | In this block of code, a JSON object is created with data that needs to be sent to the service worker. A string, `dispatchCode`, is created containing the JavaScript that we want to call. In this case, that JavaScript is the `FireSyncEvent` function that was defined in the previous section. `evaluateScript` will execute the given script in the global scope of your service worker context. 80 | 81 | Note: Trying to use `evaluateScript` in an asynchronous function can cause threading issues. To get around this problem, you can use the `performSelectorOnMainThread` function as shown here. 82 | ```objective-c 83 | [serviceWorker.context performSelectorOnMainThread:@selector(evaluateScript:) withObject:dispatchCode waitUntilDone:NO]; 84 | ``` 85 | 86 | If you want to execute some JavaScript code within a specific closure, you cannot define your script in Objective C, you must use a function callback passed in from JavaScript as a JSValue. But first you need a way to pass a reference to your native code from your service worker context. 87 | 88 | To have your service worker context call functions in the native code of your plugin you can use Objective C's JSCore JavaScript function block definitions. 89 | ```objective-c 90 | __weak CDVBackgroundSync* weakSelf = self; 91 | serviceWorker.context[@"unregisterSync"] = ^(JSValue *registrationId) { 92 | [weakSelf unregisterSyncById:[registrationId toString]]; 93 | }; 94 | ``` 95 | You can define an Objective C blocks to be tied to JavaScript variables in your service worker context. 96 | In this example, a block is defined for the service worker context variable of `unregisterSync`. After these lines of code have been executed, whenever `unregisterSync` is called from the service worker context, this block will be executed. All parameters for this type of code block should be of type `JSValue`. 97 | 98 | If one of your JSValue parameters is a function, you can use `callWithArguments` to invoke that function with its original closure. For example, if we wanted `unregisterSync` to have a callback we would add: 99 | ```objective-c 100 | serviceWorker.context[@"unregisterSync"] = ^(JSValue *registrationId, JSValue *callback) { 101 | [weakSelf unregisterSyncById:[registrationId toString]]; 102 | NSArray *arguments = @[registrationId]; 103 | [callback callWithArguments:arguments]; 104 | }; 105 | ``` 106 | 107 | ##Attaching Property to Service Worker Registration 108 | If you want to add an object as a property of a service worker registration, simply create a class or object as you normally would in javascript. In the same file as your class definition listen for the `serviceWorker.ready` promise and then add a new object from your class as a property of the service worker registration that is returned by ready. 109 | ```javascript 110 | navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) { 111 | serviceWorkerRegistration.sync = new SyncManager(); 112 | ... 113 | }); 114 | ``` 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phonegap-plugin-service-worker", 3 | "version": "1.0.1", 4 | "description": "Service Worker Plugin", 5 | "cordova": { 6 | "id": "phonegap-plugin-service-worker", 7 | "platforms": [ 8 | "ios" 9 | ] 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/phonegap/phonegap-plugin-service-worker.git" 14 | }, 15 | "keywords": [ 16 | "cordova", 17 | "serviceworker", 18 | "service", 19 | "worker", 20 | "ecosystem:cordova", 21 | "cordova-ios" 22 | ], 23 | "author": "The Chrome Team", 24 | "license": "Apache 2.0", 25 | "bugs": { 26 | "url": "https://github.com/phonegap/phonegap-plugin-service-worker/issues" 27 | }, 28 | "homepage": "https://github.com/phonegap/phonegap-plugin-service-worker" 29 | } 30 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | Service Worker 6 | Service Worker Plugin 7 | Apache 2.0 8 | cordova,serviceworker,service,worker 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/ios/CDVServiceWorker.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | #import "ServiceWorkerCacheApi.h" 23 | 24 | extern NSString * const SERVICE_WORKER; 25 | extern NSString * const SERVICE_WORKER_CACHE_CORDOVA_ASSETS; 26 | extern NSString * const SERVICE_WORKER_ACTIVATED; 27 | extern NSString * const SERVICE_WORKER_INSTALLED; 28 | extern NSString * const SERVICE_WORKER_SCRIPT_CHECKSUM; 29 | 30 | extern NSString * const REGISTER_OPTIONS_KEY_SCOPE; 31 | 32 | extern NSString * const REGISTRATION_KEY_ACTIVE; 33 | extern NSString * const REGISTRATION_KEY_INSTALLING; 34 | extern NSString * const REGISTRATION_KEY_REGISTERING_SCRIPT_URL; 35 | extern NSString * const REGISTRATION_KEY_SCOPE; 36 | extern NSString * const REGISTRATION_KEY_WAITING; 37 | 38 | extern NSString * const SERVICE_WORKER_KEY_SCRIPT_URL; 39 | 40 | @interface CDVServiceWorker : CDVPlugin {} 41 | 42 | + (CDVServiceWorker *)instanceForRequest:(NSURLRequest *)request; 43 | - (void)addRequestToQueue:(NSURLRequest *)request withId:(NSNumber *)requestId delegateTo:(NSURLProtocol *)protocol; 44 | 45 | @property (nonatomic, retain) JSContext *context; 46 | @property (nonatomic, retain) UIWebView *workerWebView; 47 | @property (nonatomic, retain) NSMutableDictionary *requestDelegates; 48 | @property (nonatomic, retain) NSMutableArray *requestQueue; 49 | @property (nonatomic, retain) NSDictionary *registration; 50 | @property (nonatomic, retain) NSString *serviceWorkerScriptFilename; 51 | @property (nonatomic, retain) ServiceWorkerCacheApi *cacheApi; 52 | 53 | @end 54 | 55 | -------------------------------------------------------------------------------- /src/ios/CDVServiceWorker.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | #import 23 | #import "CDVServiceWorker.h" 24 | #import "FetchConnectionDelegate.h" 25 | #import "FetchInterceptorProtocol.h" 26 | #import "ServiceWorkerCacheApi.h" 27 | #import "ServiceWorkerRequest.h" 28 | 29 | static bool isServiceWorkerActive = NO; 30 | 31 | NSString * const SERVICE_WORKER = @"serviceworker"; 32 | NSString * const SERVICE_WORKER_SCOPE = @"serviceworkerscope"; 33 | NSString * const SERVICE_WORKER_CACHE_CORDOVA_ASSETS = @"cachecordovaassets"; 34 | NSString * const SERVICE_WORKER_ACTIVATED = @"ServiceWorkerActivated"; 35 | NSString * const SERVICE_WORKER_INSTALLED = @"ServiceWorkerInstalled"; 36 | NSString * const SERVICE_WORKER_SCRIPT_CHECKSUM = @"ServiceWorkerScriptChecksum"; 37 | 38 | NSString * const REGISTER_OPTIONS_KEY_SCOPE = @"scope"; 39 | 40 | NSString * const REGISTRATION_KEY_ACTIVE = @"active"; 41 | NSString * const REGISTRATION_KEY_INSTALLING = @"installing"; 42 | NSString * const REGISTRATION_KEY_REGISTERING_SCRIPT_URL = @"registeringScriptURL"; 43 | NSString * const REGISTRATION_KEY_SCOPE = @"scope"; 44 | NSString * const REGISTRATION_KEY_WAITING = @"waiting"; 45 | 46 | NSString * const SERVICE_WORKER_KEY_SCRIPT_URL = @"scriptURL"; 47 | 48 | @implementation CDVServiceWorker 49 | 50 | @synthesize context = _context; 51 | @synthesize workerWebView = _workerWebView; 52 | @synthesize registration = _registration; 53 | @synthesize requestDelegates = _requestDelegates; 54 | @synthesize requestQueue = _requestQueue; 55 | @synthesize serviceWorkerScriptFilename = _serviceWorkerScriptFilename; 56 | @synthesize cacheApi = _cacheApi; 57 | 58 | - (NSString *)hashForString:(NSString *)string 59 | { 60 | const char *cstring = [string UTF8String]; 61 | size_t length = strlen(cstring); 62 | 63 | // We're assuming below that CC_LONG is an unsigned int; fail here if that's not true. 64 | assert(sizeof(CC_LONG) == sizeof(unsigned int)); 65 | 66 | unsigned char hash[33]; 67 | 68 | CC_MD5_CTX hashContext; 69 | 70 | // We'll almost certainly never see >4GB files, but loop with UINT32_MAX sized-chunks just to be correct 71 | CC_MD5_Init(&hashContext); 72 | CC_LONG dataToHash; 73 | while (length != 0) { 74 | if (length > UINT32_MAX) { 75 | dataToHash = UINT32_MAX; 76 | length -= UINT32_MAX; 77 | } else { 78 | dataToHash = (CC_LONG)length; 79 | length = 0; 80 | } 81 | CC_MD5_Update(&hashContext, cstring, dataToHash); 82 | cstring += dataToHash; 83 | } 84 | CC_MD5_Final(hash, &hashContext); 85 | 86 | // Construct a simple base-16 representation of the hash for comparison 87 | for (int i=15; i >= 0; --i) { 88 | hash[i*2+1] = 'a' + (hash[i] & 0x0f); 89 | hash[i*2] = 'a' + ((hash[i] >> 4) & 0x0f); 90 | } 91 | // Null-terminate 92 | hash[32] = 0; 93 | 94 | return [NSString stringWithCString:(char *)hash 95 | encoding:NSUTF8StringEncoding]; 96 | } 97 | 98 | CDVServiceWorker *singletonInstance = nil; // TODO: Something better 99 | + (CDVServiceWorker *)instanceForRequest:(NSURLRequest *)request 100 | { 101 | return singletonInstance; 102 | } 103 | 104 | - (void)pluginInitialize 105 | { 106 | // TODO: Make this better; probably a registry 107 | singletonInstance = self; 108 | 109 | self.requestDelegates = [[NSMutableDictionary alloc] initWithCapacity:10]; 110 | self.requestQueue = [NSMutableArray new]; 111 | 112 | [NSURLProtocol registerClass:[FetchInterceptorProtocol class]]; 113 | 114 | // Get the app settings. 115 | BOOL cacheCordovaAssets = YES; 116 | NSString *serviceWorkerScope; 117 | if ([[self viewController] isKindOfClass:[CDVViewController class]]) { 118 | CDVViewController *vc = (CDVViewController *)[self viewController]; 119 | NSMutableDictionary *settings = [vc settings]; 120 | self.serviceWorkerScriptFilename = [settings objectForKey:SERVICE_WORKER]; 121 | NSObject *cacheCordovaAssetsObject = [settings objectForKey:SERVICE_WORKER_CACHE_CORDOVA_ASSETS]; 122 | serviceWorkerScope = [settings objectForKey:SERVICE_WORKER_SCOPE]; 123 | cacheCordovaAssets = (cacheCordovaAssetsObject == nil) ? YES : [(NSString *)cacheCordovaAssetsObject boolValue]; 124 | } 125 | 126 | // Initialize CoreData for the Cache API. 127 | self.cacheApi = [[ServiceWorkerCacheApi alloc] initWithScope:serviceWorkerScope cacheCordovaAssets:cacheCordovaAssets]; 128 | [self.cacheApi initializeStorage]; 129 | 130 | self.workerWebView = [[UIWebView alloc] init]; // Headless 131 | [self.viewController.view addSubview:self.workerWebView]; 132 | [self.workerWebView setDelegate:self]; 133 | [self.workerWebView loadHTMLString:@"Service Worker Page" baseURL:[NSURL fileURLWithPath:[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"GeneratedWorker.html"]]]; 134 | } 135 | 136 | # pragma mark ServiceWorker Functions 137 | 138 | - (void)register:(CDVInvokedUrlCommand*)command 139 | { 140 | NSString *scriptUrl = [command argumentAtIndex:0]; 141 | NSDictionary *options = [command argumentAtIndex:1]; 142 | 143 | // The script url must be at the root. 144 | // TODO: Look into supporting non-root ServiceWorker scripts. 145 | if ([scriptUrl containsString:@"/"]) { 146 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 147 | messageAsString:@"The script URL must be at the root."]; 148 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 149 | } 150 | 151 | // The provided scope is ignored; we always set it to the root. 152 | // TODO: Support provided scopes. 153 | NSString *scopeUrl = @"/"; 154 | 155 | // If we have a registration on record, make sure it matches the attempted registration. 156 | // If it matches, return it. If it doesn't, we have a problem! 157 | // If we don't have a registration on record, create one, store it, and return it. 158 | if (self.registration != nil) { 159 | if (![[self.registration valueForKey:REGISTRATION_KEY_REGISTERING_SCRIPT_URL] isEqualToString:scriptUrl]) { 160 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 161 | messageAsString:@"The script URL doesn't match the existing registration."]; 162 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 163 | } else if (![[self.registration valueForKey:REGISTRATION_KEY_SCOPE] isEqualToString:scopeUrl]) { 164 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 165 | messageAsString:@"The scope URL doesn't match the existing registration."]; 166 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 167 | } 168 | } else { 169 | [self createServiceWorkerRegistrationWithScriptUrl:scriptUrl scopeUrl:scopeUrl]; 170 | } 171 | 172 | // Return the registration. 173 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:self.registration]; 174 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 175 | } 176 | 177 | - (void)createServiceWorkerRegistrationWithScriptUrl:(NSString*)scriptUrl scopeUrl:(NSString*)scopeUrl 178 | { 179 | NSDictionary *serviceWorker = [NSDictionary dictionaryWithObject:scriptUrl forKey:SERVICE_WORKER_KEY_SCRIPT_URL]; 180 | // TODO: Add a state to the ServiceWorker object. 181 | 182 | NSArray *registrationKeys = @[REGISTRATION_KEY_INSTALLING, 183 | REGISTRATION_KEY_WAITING, 184 | REGISTRATION_KEY_ACTIVE, 185 | REGISTRATION_KEY_REGISTERING_SCRIPT_URL, 186 | REGISTRATION_KEY_SCOPE]; 187 | NSArray *registrationObjects = @[[NSNull null], [NSNull null], serviceWorker, scriptUrl, scopeUrl]; 188 | self.registration = [NSDictionary dictionaryWithObjects:registrationObjects forKeys:registrationKeys]; 189 | } 190 | 191 | - (void)serviceWorkerReady:(CDVInvokedUrlCommand*)command 192 | { 193 | // The provided scope is ignored; we always set it to the root. 194 | // TODO: Support provided scopes. 195 | NSString *scopeUrl = @"/"; 196 | NSString *scriptUrl = self.serviceWorkerScriptFilename; 197 | 198 | if (isServiceWorkerActive) { 199 | if (self.registration == nil) { 200 | [self createServiceWorkerRegistrationWithScriptUrl:scriptUrl scopeUrl:scopeUrl]; 201 | } 202 | // Return the registration. 203 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:self.registration]; 204 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 205 | } else { 206 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR 207 | messageAsString:@"No Service Worker is currently active."]; 208 | [[self commandDelegate] sendPluginResult:pluginResult callbackId:[command callbackId]]; 209 | } 210 | } 211 | 212 | - (void)postMessage:(CDVInvokedUrlCommand*)command 213 | { 214 | NSString *message = [command argumentAtIndex:0]; 215 | 216 | // Fire a message event in the JSContext. 217 | NSString *dispatchCode = [NSString stringWithFormat:@"dispatchEvent(new MessageEvent({data:Kamino.parse('%@')}));", message]; 218 | [self evaluateScript:dispatchCode]; 219 | } 220 | 221 | - (void)installServiceWorker 222 | { 223 | [self evaluateScript:@"FireInstallEvent().then(installServiceWorkerCallback);"]; 224 | } 225 | 226 | - (void)activateServiceWorker 227 | { 228 | [self evaluateScript:@"FireActivateEvent().then(activateServiceWorkerCallback);"]; 229 | } 230 | 231 | - (void)initiateServiceWorker 232 | { 233 | isServiceWorkerActive = YES; 234 | NSLog(@"SW active! Processing request queue."); 235 | [self processRequestQueue]; 236 | } 237 | 238 | # pragma mark Helper Functions 239 | 240 | - (void)evaluateScript:(NSString *)script 241 | { 242 | if ([NSThread isMainThread]) { 243 | [self.workerWebView stringByEvaluatingJavaScriptFromString:script]; 244 | } else { 245 | [self.workerWebView performSelectorOnMainThread:@selector(stringByEvaluatingJavaScriptFromString:) withObject:script waitUntilDone:NO]; 246 | } 247 | } 248 | 249 | - (void)createServiceWorkerFromScript:(NSString *)script 250 | { 251 | // Get the JSContext from the webview 252 | self.context = [self.workerWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; 253 | 254 | [self.context setExceptionHandler:^(JSContext *context, JSValue *value) { 255 | NSLog(@"%@", value); 256 | }]; 257 | 258 | // Pipe JS logging in this context to NSLog. 259 | // NOTE: Not the nicest of hacks, but useful! 260 | [self evaluateScript:@"var console = {}"]; 261 | self.context[@"console"][@"log"] = ^(NSString *message) { 262 | NSLog(@"JS log: %@", message); 263 | }; 264 | 265 | CDVServiceWorker * __weak weakSelf = self; 266 | 267 | self.context[@"installServiceWorkerCallback"] = ^() { 268 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:SERVICE_WORKER_INSTALLED]; 269 | [weakSelf activateServiceWorker]; 270 | }; 271 | 272 | self.context[@"activateServiceWorkerCallback"] = ^() { 273 | [[NSUserDefaults standardUserDefaults] setBool:YES forKey:SERVICE_WORKER_ACTIVATED]; 274 | [weakSelf initiateServiceWorker]; 275 | }; 276 | 277 | self.context[@"handleFetchResponse"] = ^(JSValue *jsRequestId, JSValue *response) { 278 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; 279 | [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; 280 | NSNumber *requestId = [formatter numberFromString:[jsRequestId toString]]; 281 | FetchInterceptorProtocol *interceptor = (FetchInterceptorProtocol *)[weakSelf.requestDelegates objectForKey:requestId]; 282 | [weakSelf.requestDelegates removeObjectForKey:requestId]; 283 | 284 | // Convert the response body to base64. 285 | //NSData *data = [NSData dataFromBase64String:[response[@"body"] toString]]; 286 | NSData *data = [[NSData alloc] initWithBase64EncodedString:[response[@"body"] toString] options:0]; 287 | 288 | JSValue *headers = response[@"headers"]; 289 | NSString *mimeType = [headers[@"mimeType"] toString]; 290 | NSString *encoding = @"utf-8"; 291 | NSString *url = [response[@"url"] toString]; // TODO: Can this ever be different than the request url? if not, don't allow it to be overridden 292 | 293 | NSURLResponse *urlResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:url] 294 | MIMEType:mimeType 295 | expectedContentLength:data.length 296 | textEncodingName:encoding]; 297 | 298 | [interceptor handleAResponse:urlResponse withSomeData:data]; 299 | }; 300 | 301 | self.context[@"handleFetchDefault"] = ^(JSValue *jsRequestId, JSValue *response) { 302 | NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; 303 | [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; 304 | NSNumber *requestId = [formatter numberFromString:[jsRequestId toString]]; 305 | FetchInterceptorProtocol *interceptor = (FetchInterceptorProtocol *)[weakSelf.requestDelegates objectForKey:requestId]; 306 | [weakSelf.requestDelegates removeObjectForKey:requestId]; 307 | [interceptor passThrough]; 308 | }; 309 | 310 | self.context[@"handleTrueFetch"] = ^(JSValue *method, JSValue *resourceUrl, JSValue *headers, JSValue *resolve, JSValue *reject) { 311 | NSString *resourceUrlString = [resourceUrl toString]; 312 | if (![[resourceUrl toString] containsString:@"://"]) { 313 | resourceUrlString = [NSString stringWithFormat:@"file://%@/www/%@", [[NSBundle mainBundle] resourcePath], resourceUrlString]; 314 | } 315 | 316 | // Create the request. 317 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:resourceUrlString]]; 318 | [request setHTTPMethod:[method toString]]; 319 | NSDictionary *headerDictionary = [headers toDictionary]; 320 | [headerDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL* stop) { 321 | [request addValue:value forHTTPHeaderField:key]; 322 | }]; 323 | [NSURLProtocol setProperty:@YES forKey:@"PureFetch" inRequest:request]; 324 | 325 | // Create a connection and send the request. 326 | FetchConnectionDelegate *delegate = [FetchConnectionDelegate new]; 327 | delegate.resolve = ^(ServiceWorkerResponse *response) { 328 | [resolve callWithArguments:@[[response toDictionary]]]; 329 | }; 330 | delegate.reject = ^(NSString *error) { 331 | [reject callWithArguments:@[error]]; 332 | }; 333 | [NSURLConnection connectionWithRequest:request delegate:delegate]; 334 | }; 335 | 336 | // This function is called by `postMessage`, defined in message.js. 337 | // `postMessage` serializes the message using kamino.js and passes it here. 338 | self.context[@"postMessageInternal"] = ^(JSValue *serializedMessage) { 339 | NSString *postMessageCode = [NSString stringWithFormat:@"window.postMessage(Kamino.parse('%@'), '*')", [serializedMessage toString]]; 340 | [weakSelf.webView performSelectorOnMainThread:@selector(stringByEvaluatingJavaScriptFromString:) withObject:postMessageCode waitUntilDone:NO]; 341 | }; 342 | 343 | // Install cache API JS methods 344 | [self.cacheApi defineFunctionsInContext:self.context]; 345 | 346 | // Load the required assets. 347 | [self loadServiceWorkerAssetsIntoContext]; 348 | 349 | // Load the ServiceWorker script. 350 | [self loadScript:script]; 351 | } 352 | 353 | - (void)createServiceWorkerClientWithUrl:(NSString *)url 354 | { 355 | // Create a ServiceWorker client. 356 | NSString *createClientCode = [NSString stringWithFormat:@"var client = new Client('%@');", url]; 357 | [self evaluateScript:createClientCode]; 358 | } 359 | 360 | - (NSString *)readScriptAtRelativePath:(NSString *)relativePath 361 | { 362 | // NOTE: Relative path means relative to the app bundle. 363 | 364 | // Compose the absolute path. 365 | NSString *absolutePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:[NSString stringWithFormat:@"/%@", relativePath]]; 366 | 367 | // Read the script from the file. 368 | NSError *error; 369 | NSString *script = [NSString stringWithContentsOfFile:absolutePath encoding:NSUTF8StringEncoding error:&error]; 370 | 371 | // If there was an error, log it and return. 372 | if (error) { 373 | NSLog(@"Could not read script: %@", [error description]); 374 | return nil; 375 | } 376 | 377 | // Return our script! 378 | return script; 379 | } 380 | 381 | - (void)loadServiceWorkerAssetsIntoContext 382 | { 383 | // Specify the assets directory. 384 | // TODO: Move assets up one directory, so they're not in www. 385 | NSString *assetDirectoryPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/www/sw_assets"]; 386 | 387 | // Get the list of assets. 388 | NSArray *assetFilenames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:assetDirectoryPath error:NULL]; 389 | 390 | // Read and load each asset. 391 | for (NSString *assetFilename in assetFilenames) { 392 | NSString *relativePath = [NSString stringWithFormat:@"www/sw_assets/%@", assetFilename]; 393 | [self readAndLoadScriptAtRelativePath:relativePath]; 394 | } 395 | } 396 | 397 | - (void)loadScript:(NSString *)script 398 | { 399 | // Evaluate the script. 400 | [self evaluateScript:script]; 401 | } 402 | 403 | - (void)readAndLoadScriptAtRelativePath:(NSString *)relativePath 404 | { 405 | // Log! 406 | NSLog(@"Loading script: %@", relativePath); 407 | 408 | // Read the script. 409 | NSString *script = [self readScriptAtRelativePath:relativePath]; 410 | 411 | if (script == nil) { 412 | return; 413 | } 414 | 415 | // Load the script into the context. 416 | [self loadScript:script]; 417 | } 418 | 419 | - (void)webViewDidFinishLoad:(UIWebView *)wv 420 | { 421 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 422 | bool serviceWorkerInstalled = [defaults boolForKey:SERVICE_WORKER_INSTALLED]; 423 | bool serviceWorkerActivated = [defaults boolForKey:SERVICE_WORKER_ACTIVATED]; 424 | NSString *serviceWorkerScriptChecksum = [defaults stringForKey:SERVICE_WORKER_SCRIPT_CHECKSUM]; 425 | if (self.serviceWorkerScriptFilename != nil) { 426 | NSString *serviceWorkerScriptRelativePath = [NSString stringWithFormat:@"www/%@", self.serviceWorkerScriptFilename]; 427 | NSLog(@"ServiceWorker relative path: %@", serviceWorkerScriptRelativePath); 428 | NSString *serviceWorkerScript = [self readScriptAtRelativePath:serviceWorkerScriptRelativePath]; 429 | if (serviceWorkerScript != nil) { 430 | if (![[self hashForString:serviceWorkerScript] isEqualToString:serviceWorkerScriptChecksum]) { 431 | serviceWorkerInstalled = NO; 432 | serviceWorkerActivated = NO; 433 | [defaults setBool:NO forKey:SERVICE_WORKER_INSTALLED]; 434 | [defaults setBool:NO forKey:SERVICE_WORKER_ACTIVATED]; 435 | [defaults setObject:[self hashForString:serviceWorkerScript] forKey:SERVICE_WORKER_SCRIPT_CHECKSUM]; 436 | } 437 | [self createServiceWorkerFromScript:serviceWorkerScript]; 438 | [self createServiceWorkerClientWithUrl:self.serviceWorkerScriptFilename]; 439 | if (!serviceWorkerInstalled) { 440 | [self installServiceWorker]; 441 | } else if (!serviceWorkerActivated) { 442 | [self activateServiceWorker]; 443 | } else { 444 | [self initiateServiceWorker]; 445 | } 446 | } 447 | } else { 448 | NSLog(@"No service worker script defined. Please add the following line to config.xml: "); 449 | } 450 | } 451 | 452 | - (void)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request {} 453 | - (void)webViewDidStartLoad:(UIWebView *)wv {} 454 | - (void)webView:(UIWebView *)wv didFailLoadWithError:(NSError *)error {} 455 | 456 | 457 | - (void)addRequestToQueue:(NSURLRequest *)request withId:(NSNumber *)requestId delegateTo:(NSURLProtocol *)protocol 458 | { 459 | // Log! 460 | NSLog(@"Adding to queue: %@", [[request URL] absoluteString]); 461 | 462 | // Create a request object. 463 | ServiceWorkerRequest *swRequest = [ServiceWorkerRequest new]; 464 | swRequest.request = request; 465 | swRequest.requestId = requestId; 466 | swRequest.protocol = protocol; 467 | 468 | // Add the request object to the queue. 469 | [self.requestQueue addObject:swRequest]; 470 | 471 | // Process the request queue. 472 | [self processRequestQueue]; 473 | } 474 | 475 | - (void)processRequestQueue { 476 | // If the ServiceWorker isn't active, there's nothing we can do yet. 477 | if (!isServiceWorkerActive) { 478 | return; 479 | } 480 | 481 | for (ServiceWorkerRequest *swRequest in self.requestQueue) { 482 | // Log! 483 | NSLog(@"Processing from queue: %@", [[swRequest.request URL] absoluteString]); 484 | 485 | // Register the request and delegate. 486 | [self.requestDelegates setObject:swRequest.protocol forKey:swRequest.requestId]; 487 | 488 | // Fire a fetch event in the JSContext. 489 | NSURLRequest *request = swRequest.request; 490 | NSString *method = [request HTTPMethod]; 491 | NSString *url = [[request URL] absoluteString]; 492 | NSData *headerData = [NSJSONSerialization dataWithJSONObject:[request allHTTPHeaderFields] 493 | options:NSJSONWritingPrettyPrinted 494 | error:nil]; 495 | NSString *headers = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@"\n" withString:@" "]; 496 | 497 | NSString *requestCode = [NSString stringWithFormat:@"new Request('%@', '%@', %@)", method, url, headers]; 498 | NSString *dispatchCode = [NSString stringWithFormat:@"dispatchEvent(new FetchEvent({request:%@, id:'%lld'}));", requestCode, [swRequest.requestId longLongValue]]; 499 | [self evaluateScript:dispatchCode]; 500 | } 501 | 502 | // Clear the queue. 503 | // TODO: Deal with the possibility that requests could be added during the loop that we might not necessarily want to remove. 504 | [self.requestQueue removeAllObjects]; 505 | } 506 | 507 | @end 508 | 509 | -------------------------------------------------------------------------------- /src/ios/FetchConnectionDelegate.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import "ServiceWorkerResponse.h" 22 | 23 | @interface FetchConnectionDelegate : NSObject 24 | 25 | @property (nonatomic, retain) NSMutableData *responseData; 26 | @property (nonatomic, copy) void (^resolve)(ServiceWorkerResponse *); 27 | @property (nonatomic, copy) void (^reject)(NSString *); 28 | 29 | @end 30 | 31 | -------------------------------------------------------------------------------- /src/ios/FetchConnectionDelegate.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "FetchConnectionDelegate.h" 21 | #import "ServiceWorkerResponse.h" 22 | 23 | @implementation FetchConnectionDelegate 24 | 25 | @synthesize responseData = _responseData; 26 | @synthesize resolve = _resolve; 27 | @synthesize reject = _reject; 28 | 29 | #pragma mark NSURLConnection Delegate Methods 30 | 31 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 32 | self.responseData = [[NSMutableData alloc] init]; 33 | } 34 | 35 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 36 | [self.responseData appendData:data]; 37 | } 38 | 39 | - (NSCachedURLResponse *)connection:(NSURLConnection *)connection 40 | willCacheResponse:(NSCachedURLResponse*)cachedResponse { 41 | return nil; 42 | } 43 | 44 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 45 | // Create the response object. 46 | ServiceWorkerResponse *response = [ServiceWorkerResponse new]; 47 | response.url = [[[connection currentRequest] URL] absoluteString]; 48 | response.body = self.responseData; 49 | response.status = @200; 50 | response.headers = [[connection currentRequest] allHTTPHeaderFields]; 51 | 52 | // Convert the response to a dictionary and send it to the promise resolver. 53 | self.resolve(response); 54 | } 55 | 56 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 57 | NSLog(@"%@", [error description]); 58 | } 59 | 60 | @end 61 | 62 | -------------------------------------------------------------------------------- /src/ios/FetchInterceptorProtocol.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | @interface FetchInterceptorProtocol : NSURLProtocol {} 21 | 22 | + (BOOL)canInitWithRequest:(NSURLRequest *)request; 23 | - (void)handleAResponse:(NSURLResponse *)response withSomeData:(NSData *)data; 24 | - (void)passThrough; 25 | 26 | @property (nonatomic, retain) NSURLConnection *connection; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /src/ios/FetchInterceptorProtocol.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "FetchInterceptorProtocol.h" 21 | #import "CDVServiceWorker.h" 22 | 23 | #include 24 | 25 | @implementation FetchInterceptorProtocol 26 | @synthesize connection=_connection; 27 | 28 | static int64_t requestCount = 0; 29 | 30 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { 31 | // We don't want to intercept any requests for the worker page. 32 | if ([[[request URL] absoluteString] hasSuffix:@"GeneratedWorker.html"]) { 33 | return NO; 34 | } 35 | 36 | // Check - is there a service worker for this request? 37 | // For now, assume YES -- all requests go through service worker. This may be incorrect if there are iframes present. 38 | if ([NSURLProtocol propertyForKey:@"PassThrough" inRequest:request]) { 39 | // Already seen; not handling 40 | return NO; 41 | } else if ([NSURLProtocol propertyForKey:@"PureFetch" inRequest:request]) { 42 | // Fetching directly; bypass ServiceWorker. 43 | return NO; 44 | } else { 45 | if ([CDVServiceWorker instanceForRequest:request]) { 46 | // Handling 47 | return YES; 48 | } else { 49 | // No Service Worker installed; not handling 50 | return NO; 51 | } 52 | } 53 | } 54 | 55 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { 56 | return request; 57 | } 58 | 59 | + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { 60 | return [super requestIsCacheEquivalent:a toRequest:b]; 61 | } 62 | 63 | - (void)startLoading { 64 | // Attach a reference to the Service Worker to a copy of the request 65 | NSMutableURLRequest *workerRequest = [self.request mutableCopy]; 66 | CDVServiceWorker *instanceForRequest = [CDVServiceWorker instanceForRequest:workerRequest]; 67 | [NSURLProtocol setProperty:instanceForRequest forKey:@"ServiceWorkerPlugin" inRequest:workerRequest]; 68 | NSNumber *requestId = [NSNumber numberWithLongLong:OSAtomicIncrement64(&requestCount)]; 69 | [NSURLProtocol setProperty:requestId forKey:@"RequestId" inRequest:workerRequest]; 70 | 71 | [instanceForRequest addRequestToQueue:workerRequest withId:requestId delegateTo:self]; 72 | } 73 | 74 | - (void)stopLoading { 75 | [self.connection cancel]; 76 | self.connection = nil; 77 | } 78 | 79 | - (void)passThrough { 80 | // Flag this request as a pass-through so that the URLProtocol doesn't try to grab it again 81 | NSMutableURLRequest *taggedRequest = [self.request mutableCopy]; 82 | [NSURLProtocol setProperty:@YES forKey:@"PassThrough" inRequest:taggedRequest]; 83 | 84 | // Initiate a new request to actually retrieve the resource 85 | self.connection = [NSURLConnection connectionWithRequest:taggedRequest delegate:self]; 86 | } 87 | 88 | - (void)handleAResponse:(NSURLResponse *)response withSomeData:(NSData *)data { 89 | // TODO: Move cache storage policy into args 90 | [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 91 | [self.client URLProtocol:self didLoadData:data]; 92 | [self.client URLProtocolDidFinishLoading:self]; 93 | } 94 | 95 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 96 | [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 97 | } 98 | 99 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 100 | [self.client URLProtocol:self didLoadData:data]; 101 | } 102 | 103 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 104 | [self.client URLProtocolDidFinishLoading:self]; 105 | } 106 | 107 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 108 | [self.client URLProtocol:self didFailWithError:error]; 109 | } 110 | 111 | @end 112 | 113 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCache.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | 23 | #import "ServiceWorkerResponse.h" 24 | #import "ServiceWorkerCacheEntry.h" 25 | 26 | @interface ServiceWorkerCache : NSManagedObject 27 | 28 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request inContext:(NSManagedObjectContext *)moc; 29 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request withOptions:(/*ServiceWorkerCacheMatchOptions*/NSDictionary *)options inContext:(NSManagedObjectContext *)moc; 30 | -(void) putRequest:(NSURLRequest *)request andResponse:(ServiceWorkerResponse *)response inContext:(NSManagedObjectContext *)moc; 31 | -(bool) deleteRequest:(NSURLRequest *)request fromContext:(NSManagedObjectContext *)moc; 32 | -(NSArray *)requestsFromContext:(NSManagedObjectContext *)moc; 33 | 34 | @property (nonatomic, retain) NSString * name; 35 | @property (nonatomic, retain) NSString * scope; 36 | @property (nonatomic, retain) NSManagedObject *entries; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCache.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "ServiceWorkerCache.h" 21 | 22 | @implementation ServiceWorkerCache 23 | 24 | @dynamic name; 25 | @dynamic scope; 26 | @dynamic entries; 27 | 28 | 29 | -(NSString *)urlWithoutQueryForUrl:(NSURL *)url 30 | { 31 | NSURL *absoluteURL = [url absoluteURL]; 32 | NSURL *urlWithoutQuery; 33 | if ([absoluteURL scheme] == nil) { 34 | NSString *path = [absoluteURL path]; 35 | NSRange queryRange = [path rangeOfString:@"?"]; 36 | if (queryRange.location != NSNotFound) { 37 | path = [path substringToIndex:queryRange.location]; 38 | } 39 | return path; 40 | } 41 | urlWithoutQuery = [[NSURL alloc] initWithScheme:[[absoluteURL scheme] lowercaseString] 42 | host:[[absoluteURL host] lowercaseString] 43 | path:[absoluteURL path]]; 44 | return [urlWithoutQuery absoluteString]; 45 | } 46 | 47 | -(NSArray *)entriesMatchingRequestByURL:(NSURL *)url includesQuery:(BOOL)includesQuery inContext:(NSManagedObjectContext *)moc 48 | { 49 | NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; 50 | 51 | NSEntityDescription *entity = [NSEntityDescription 52 | entityForName:@"CacheEntry" inManagedObjectContext:moc]; 53 | [fetchRequest setEntity:entity]; 54 | 55 | NSPredicate *predicate; 56 | 57 | if (includesQuery) { 58 | predicate = [NSPredicate predicateWithFormat:@"(cache == %@) AND (url == %@) AND (query == %@)", self, [self urlWithoutQueryForUrl:url], url.query]; 59 | } else { 60 | predicate = [NSPredicate predicateWithFormat:@"(cache == %@) AND (url == %@)", self, [self urlWithoutQueryForUrl:url]]; 61 | } 62 | [fetchRequest setPredicate:predicate]; 63 | 64 | NSError *error; 65 | NSArray *entries = [moc executeFetchRequest:fetchRequest error:&error]; 66 | 67 | // TODO: check error on entries == nil 68 | return entries; 69 | } 70 | 71 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request inContext:(NSManagedObjectContext *)moc 72 | { 73 | return [self matchForRequest:request withOptions:@{} inContext:moc]; 74 | } 75 | 76 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request withOptions:(/*ServiceWorkerCacheMatchOptions*/NSDictionary *)options inContext:(NSManagedObjectContext *)moc 77 | { 78 | NSArray *candidateEntries = [self matchAllForRequest:request withOptions:options inContext:moc]; 79 | if (candidateEntries == nil || candidateEntries.count == 0) { 80 | return nil; 81 | } 82 | 83 | ServiceWorkerCacheEntry *bestEntry = (ServiceWorkerCacheEntry *)candidateEntries[0]; 84 | ServiceWorkerResponse *bestResponse = (ServiceWorkerResponse *)[NSKeyedUnarchiver unarchiveObjectWithData:bestEntry.response]; 85 | return bestResponse; 86 | } 87 | 88 | -(NSArray *)matchAllForRequest:(NSURLRequest *)request withOptions:(/*ServiceWorkerCacheMatchOptions*/NSDictionary *)options inContext:(NSManagedObjectContext *)moc 89 | { 90 | BOOL query = [options[@"includeQuery"] boolValue]; 91 | NSArray *entries = [self entriesMatchingRequestByURL:request.URL includesQuery:query inContext:moc]; 92 | 93 | if (entries == nil || entries.count == 0) { 94 | return nil; 95 | } 96 | 97 | NSMutableArray *candidateEntries = [[NSMutableArray alloc] init]; 98 | for (ServiceWorkerCacheEntry *entry in entries) { 99 | ServiceWorkerResponse *cachedResponse = (ServiceWorkerResponse *)[NSKeyedUnarchiver unarchiveObjectWithData:entry.response]; 100 | NSString *varyHeader = cachedResponse.headers[@"Vary"]; 101 | BOOL candidateIsViable = YES; 102 | if (varyHeader != nil) { 103 | NSURLRequest *originalRequest = (NSURLRequest *)[NSKeyedUnarchiver unarchiveObjectWithData:entry.request]; 104 | for (NSString *rawVaryHeaderField in [varyHeader componentsSeparatedByString:@","]) { 105 | NSString *varyHeaderField = [rawVaryHeaderField stringByTrimmingCharactersInSet: 106 | [NSCharacterSet whitespaceCharacterSet]]; 107 | if (![[originalRequest valueForHTTPHeaderField:varyHeaderField] isEqualToString:[request valueForHTTPHeaderField:varyHeaderField]]) 108 | candidateIsViable = NO; 109 | // Break out of the Vary header checks; continue with the next candidate response. 110 | break; 111 | } 112 | } 113 | if (candidateIsViable) { 114 | [candidateEntries insertObject:entry atIndex:[candidateEntries count]]; 115 | } 116 | } 117 | NSLog(@"matchAllForRequest returned %lu entries", (unsigned long)[candidateEntries count]); 118 | return candidateEntries; 119 | } 120 | 121 | -(void)putRequest:(NSURLRequest *)request andResponse:(ServiceWorkerResponse *)response inContext:(NSManagedObjectContext *)moc 122 | { 123 | ServiceWorkerCacheEntry *entry = (ServiceWorkerCacheEntry *)[NSEntityDescription insertNewObjectForEntityForName:@"CacheEntry" 124 | inManagedObjectContext:moc]; 125 | entry.url = [self urlWithoutQueryForUrl:request.URL]; 126 | entry.query = request.URL.query; 127 | entry.request = [NSKeyedArchiver archivedDataWithRootObject:request]; 128 | entry.response = [NSKeyedArchiver archivedDataWithRootObject:response]; 129 | entry.cache = self; 130 | NSError *err; 131 | [moc save:&err]; 132 | } 133 | 134 | -(bool)deleteRequest:(NSURLRequest *)request fromContext:(NSManagedObjectContext *)moc 135 | { 136 | NSArray *entries = [self entriesMatchingRequestByURL:request.URL includesQuery:NO inContext:moc]; 137 | 138 | bool requestExistsInCache = ([entries count] > 0); 139 | if (requestExistsInCache) { 140 | [moc deleteObject:entries[0]]; 141 | } 142 | return requestExistsInCache; 143 | } 144 | 145 | -(NSArray *)requestsFromContext:(NSManagedObjectContext *)moc 146 | { 147 | NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; 148 | NSEntityDescription *entity = [NSEntityDescription 149 | entityForName:@"CacheEntry" inManagedObjectContext:moc]; 150 | [fetchRequest setEntity:entity]; 151 | NSError *error; 152 | NSArray *entries = [moc executeFetchRequest:fetchRequest error:&error]; 153 | 154 | return entries; 155 | } 156 | 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCacheApi.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | #import 20 | #import 21 | #import "ServiceWorkerResponse.h" 22 | #import "ServiceWorkerCache.h" 23 | 24 | extern NSString * const SERVICE_WORKER; 25 | 26 | @interface ServiceWorkerCacheStorage : NSObject { } 27 | 28 | -(ServiceWorkerCache*)cacheWithName:(NSString *)cacheName; 29 | -(BOOL)deleteCacheWithName:(NSString *)cacheName; 30 | -(BOOL)hasCacheWithName:(NSString *)cacheName; 31 | 32 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request; 33 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request withOptions:(/*ServiceWorkerCacheMatchOptions*/NSDictionary *)options; 34 | 35 | @property (nonatomic, retain) NSMutableDictionary *caches; 36 | @end 37 | 38 | @interface ServiceWorkerCacheApi : NSObject { } 39 | 40 | -(id)initWithScope:(NSString *)scope cacheCordovaAssets:(BOOL)cacheCordovaAssets; 41 | -(void)defineFunctionsInContext:(JSContext *)context; 42 | -(ServiceWorkerCacheStorage *)cacheStorageForScope:(NSURL *)scope; 43 | -(BOOL)initializeStorage; 44 | 45 | @property (nonatomic, retain) NSMutableDictionary *cacheStorageMap; 46 | @property (nonatomic) BOOL cacheCordovaAssets; 47 | @property (nonatomic, retain) NSString *absoluteScope; 48 | @end 49 | 50 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCacheApi.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | #import "FetchConnectionDelegate.h" 23 | #import "ServiceWorkerCacheApi.h" 24 | #import "ServiceWorkerResponse.h" 25 | 26 | NSString * const CORDOVA_ASSETS_CACHE_NAME = @"CordovaAssets"; 27 | NSString * const CORDOVA_ASSETS_VERSION_KEY = @"CordovaAssetsVersion"; 28 | 29 | static NSManagedObjectContext *moc; 30 | static NSString *rootPath_; 31 | 32 | @implementation ServiceWorkerCacheStorage 33 | 34 | @synthesize caches=caches_; 35 | 36 | -(id) initWithContext:(NSManagedObjectContext *)moc 37 | { 38 | if ((self = [super init]) != nil) { 39 | NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; 40 | 41 | NSEntityDescription *entity = [NSEntityDescription 42 | entityForName:@"Cache" inManagedObjectContext:moc]; 43 | [fetchRequest setEntity:entity]; 44 | 45 | NSError *error; 46 | NSArray *entries = [moc executeFetchRequest:fetchRequest error:&error]; 47 | 48 | // TODO: check error on entries == nil 49 | if (!entries) { 50 | entries = @[]; 51 | } 52 | 53 | caches_ = [[NSMutableDictionary alloc] initWithCapacity:entries.count+2]; 54 | for (ServiceWorkerCache *cache in entries) { 55 | caches_[cache.name] = cache; 56 | } 57 | } 58 | return self; 59 | } 60 | 61 | -(NSArray *)getCacheNames 62 | { 63 | return [self.caches allKeys]; 64 | } 65 | 66 | -(ServiceWorkerCache *)cacheWithName:(NSString *)cacheName create:(BOOL)create 67 | { 68 | ServiceWorkerCache *cache = [self.caches objectForKey:cacheName]; 69 | if (cache == nil) { 70 | // First try to get it from storage: 71 | NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; 72 | 73 | NSEntityDescription *entity = [NSEntityDescription 74 | entityForName:@"Cache" inManagedObjectContext:moc]; 75 | [fetchRequest setEntity:entity]; 76 | 77 | NSPredicate *predicate; 78 | 79 | predicate = [NSPredicate predicateWithFormat:@"(name == %@)", cacheName]; 80 | [fetchRequest setPredicate:predicate]; 81 | 82 | NSError *error; 83 | NSArray *entries = [moc executeFetchRequest:fetchRequest error:&error]; 84 | if (entries.count > 0) { 85 | // TODO: HAVE NOT SEEN THIS BRANCH EXECUTE YET. 86 | cache = entries[0]; 87 | } else if (create) { 88 | // Not there; add it 89 | cache = (ServiceWorkerCache *)[NSEntityDescription insertNewObjectForEntityForName:@"Cache" 90 | inManagedObjectContext:moc]; 91 | [self.caches setObject:cache forKey:cacheName]; 92 | cache.name = cacheName; 93 | NSError *err; 94 | [moc save:&err]; 95 | } 96 | } 97 | if (cache) { 98 | // Cache the cache 99 | [self.caches setObject:cache forKey:cacheName]; 100 | } 101 | return cache; 102 | } 103 | 104 | -(ServiceWorkerCache *)cacheWithName:(NSString *)cacheName 105 | { 106 | return [self cacheWithName:cacheName create:YES]; 107 | } 108 | 109 | -(BOOL)deleteCacheWithName:(NSString *)cacheName 110 | { 111 | ServiceWorkerCache *cache = [self cacheWithName:cacheName create:NO]; 112 | if (cache != nil) { 113 | [moc deleteObject:cache]; 114 | NSError *err; 115 | [moc save:&err]; 116 | [self.caches removeObjectForKey:cacheName]; 117 | return YES; 118 | } 119 | return NO; 120 | } 121 | 122 | -(BOOL)hasCacheWithName:(NSString *)cacheName 123 | { 124 | return ([self cacheWithName:cacheName create:NO] != nil); 125 | } 126 | 127 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request 128 | { 129 | return [self matchForRequest:request withOptions:@{}]; 130 | } 131 | 132 | -(ServiceWorkerResponse *)matchForRequest:(NSURLRequest *)request withOptions:(/*ServiceWorkerCacheMatchOptions*/NSDictionary *)options 133 | { 134 | ServiceWorkerResponse *response = nil; 135 | for (NSString* cacheName in self.caches) { 136 | ServiceWorkerCache* cache = self.caches[cacheName]; 137 | response = [cache matchForRequest:request withOptions:options inContext:moc]; 138 | if (response != nil) { 139 | break; 140 | } 141 | } 142 | return response; 143 | } 144 | 145 | @end 146 | 147 | @implementation ServiceWorkerCacheApi 148 | 149 | @synthesize cacheStorageMap = _cacheStorageMap; 150 | @synthesize cacheCordovaAssets = _cacheCordovaAssets; 151 | @synthesize absoluteScope = _absoluteScope; 152 | 153 | -(id)initWithScope:(NSString *)scope cacheCordovaAssets:(BOOL)cacheCordovaAssets 154 | { 155 | if (self = [super init]) { 156 | if (scope == nil) { 157 | self.absoluteScope = @"/"; 158 | } else { 159 | self.absoluteScope = scope; 160 | } 161 | self.cacheCordovaAssets = cacheCordovaAssets; 162 | } 163 | return self; 164 | } 165 | 166 | +(NSManagedObjectModel *)createManagedObjectModel 167 | { 168 | NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init]; 169 | 170 | NSMutableArray *entities = [NSMutableArray array]; 171 | 172 | // ServiceWorkerCache 173 | NSEntityDescription *cacheEntity = [[NSEntityDescription alloc] init]; 174 | cacheEntity.name = @"Cache"; 175 | cacheEntity.managedObjectClassName = @"ServiceWorkerCache"; 176 | 177 | //ServiceWorkerCacheEntry 178 | NSEntityDescription *cacheEntryEntity = [[NSEntityDescription alloc] init]; 179 | cacheEntryEntity.name = @"CacheEntry"; 180 | cacheEntryEntity.managedObjectClassName = @"ServiceWorkerCacheEntry"; 181 | 182 | NSMutableArray *cacheProperties = [NSMutableArray array]; 183 | NSMutableArray *cacheEntryProperties = [NSMutableArray array]; 184 | 185 | // ServiceWorkerCache::name 186 | NSAttributeDescription *nameAttribute = [[NSAttributeDescription alloc] init]; 187 | nameAttribute.name = @"name"; 188 | nameAttribute.attributeType = NSStringAttributeType; 189 | nameAttribute.optional = NO; 190 | nameAttribute.indexed = YES; 191 | [cacheProperties addObject:nameAttribute]; 192 | 193 | // ServiceWorkerCache::scope 194 | NSAttributeDescription *scopeAttribute = [[NSAttributeDescription alloc] init]; 195 | scopeAttribute.name = @"scope"; 196 | scopeAttribute.attributeType = NSStringAttributeType; 197 | scopeAttribute.optional = YES; 198 | scopeAttribute.indexed = NO; 199 | [cacheProperties addObject:scopeAttribute]; 200 | 201 | // ServiceWorkerCacheEntry::url 202 | NSAttributeDescription *urlAttribute = [[NSAttributeDescription alloc] init]; 203 | urlAttribute.name = @"url"; 204 | urlAttribute.attributeType = NSStringAttributeType; 205 | urlAttribute.optional = YES; 206 | urlAttribute.indexed = YES; 207 | [cacheEntryProperties addObject:urlAttribute]; 208 | 209 | // ServiceWorkerCacheEntry::query 210 | NSAttributeDescription *queryAttribute = [[NSAttributeDescription alloc] init]; 211 | queryAttribute.name = @"query"; 212 | queryAttribute.attributeType = NSStringAttributeType; 213 | queryAttribute.optional = YES; 214 | queryAttribute.indexed = YES; 215 | [cacheEntryProperties addObject:queryAttribute]; 216 | 217 | // ServiceWorkerCacheEntry::request 218 | NSAttributeDescription *requestAttribute = [[NSAttributeDescription alloc] init]; 219 | requestAttribute.name = @"request"; 220 | requestAttribute.attributeType = NSBinaryDataAttributeType; 221 | requestAttribute.optional = NO; 222 | requestAttribute.indexed = NO; 223 | [cacheEntryProperties addObject:requestAttribute]; 224 | 225 | // ServiceWorkerCacheEntry::response 226 | NSAttributeDescription *responseAttribute = [[NSAttributeDescription alloc] init]; 227 | responseAttribute.name = @"response"; 228 | responseAttribute.attributeType = NSBinaryDataAttributeType; 229 | responseAttribute.optional = NO; 230 | responseAttribute.indexed = NO; 231 | [cacheEntryProperties addObject:responseAttribute]; 232 | 233 | 234 | // ServiceWorkerCache::entries 235 | NSRelationshipDescription *entriesRelationship = [[NSRelationshipDescription alloc] init]; 236 | entriesRelationship.name = @"entries"; 237 | entriesRelationship.destinationEntity = cacheEntryEntity; 238 | entriesRelationship.minCount = 0; 239 | entriesRelationship.maxCount = 0; 240 | entriesRelationship.deleteRule = NSCascadeDeleteRule; 241 | 242 | // ServiceWorkerCacheEntry::cache 243 | NSRelationshipDescription *cacheRelationship = [[NSRelationshipDescription alloc] init]; 244 | cacheRelationship.name = @"cache"; 245 | cacheRelationship.destinationEntity = cacheEntity; 246 | cacheRelationship.minCount = 0; 247 | cacheRelationship.maxCount = 1; 248 | cacheRelationship.deleteRule = NSNullifyDeleteRule; 249 | cacheRelationship.inverseRelationship = entriesRelationship; 250 | [cacheEntryProperties addObject:cacheRelationship]; 251 | 252 | 253 | entriesRelationship.inverseRelationship = cacheRelationship; 254 | [cacheProperties addObject:entriesRelationship]; 255 | 256 | cacheEntity.properties = cacheProperties; 257 | cacheEntryEntity.properties = cacheEntryProperties; 258 | 259 | [entities addObject:cacheEntity]; 260 | [entities addObject:cacheEntryEntity]; 261 | 262 | model.entities = entities; 263 | return model; 264 | } 265 | 266 | -(BOOL)initializeStorage 267 | { 268 | NSBundle* mainBundle = [NSBundle mainBundle]; 269 | rootPath_ = [[NSURL fileURLWithPath:[mainBundle pathForResource:@"www" ofType:@"" inDirectory:@""]] absoluteString]; 270 | 271 | if (moc == nil) { 272 | NSManagedObjectModel *model = [ServiceWorkerCacheApi createManagedObjectModel]; 273 | NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; 274 | 275 | NSError *err; 276 | NSFileManager *fm = [NSFileManager defaultManager]; 277 | NSURL *documentsDirectoryURL = [fm URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&err]; 278 | NSURL *cacheDirectoryURL = [documentsDirectoryURL URLByAppendingPathComponent:@"CacheData"]; 279 | [fm createDirectoryAtURL:cacheDirectoryURL withIntermediateDirectories:YES attributes:nil error:&err]; 280 | NSURL *storeURL = [cacheDirectoryURL URLByAppendingPathComponent:@"swcache.db"]; 281 | 282 | if (![fm fileExistsAtPath:[storeURL path]]) { 283 | NSLog(@"Service Worker Cache doesn't exist."); 284 | NSString *initialDataPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"CacheData"]; 285 | BOOL cacheDataIsDirectory; 286 | if ([fm fileExistsAtPath:initialDataPath isDirectory:&cacheDataIsDirectory]) { 287 | if (cacheDataIsDirectory) { 288 | NSURL *initialDataURL = [NSURL fileURLWithPath:initialDataPath isDirectory:YES]; 289 | NSLog(@"Copying Initial Cache."); 290 | NSArray *fileURLs = [fm contentsOfDirectoryAtURL:initialDataURL includingPropertiesForKeys:nil options:0 error:&err]; 291 | for (NSURL *fileURL in fileURLs) { 292 | [fm copyItemAtURL:fileURL toURL:cacheDirectoryURL error:&err]; 293 | } 294 | } 295 | } 296 | } 297 | 298 | NSLog(@"Using file %@ for service worker cache", [cacheDirectoryURL path]); 299 | err = nil; 300 | [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL URLWithString:@"swcache.db" relativeToURL:storeURL] options:nil error:&err]; 301 | if (err) { 302 | // Try to delete the old store and try again 303 | [fm removeItemAtURL:[NSURL URLWithString:@"swcache.db" relativeToURL:storeURL] error:&err]; 304 | [fm removeItemAtURL:[NSURL URLWithString:@"swcache.db-shm" relativeToURL:storeURL] error:&err]; 305 | [fm removeItemAtURL:[NSURL URLWithString:@"swcache.db-wal" relativeToURL:storeURL] error:&err]; 306 | err = nil; 307 | [psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL URLWithString:@"swcache.db" relativeToURL:storeURL] options:nil error:&err]; 308 | if (err) { 309 | return NO; 310 | } 311 | } 312 | moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; 313 | moc.persistentStoreCoordinator = psc; 314 | 315 | // If this is the first run ever, or the app has been updated, populate the Cordova assets cache with assets from www/. 316 | if (self.cacheCordovaAssets) { 317 | NSString *cordovaAssetsVersion = [[NSUserDefaults standardUserDefaults] stringForKey:CORDOVA_ASSETS_VERSION_KEY]; 318 | NSString *currentAppVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; 319 | if (cordovaAssetsVersion == nil || ![cordovaAssetsVersion isEqualToString:currentAppVersion]) { 320 | // Delete the existing cache (if it exists). 321 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 322 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 323 | [cacheStorage deleteCacheWithName:CORDOVA_ASSETS_CACHE_NAME]; 324 | 325 | // Populate the cache. 326 | [self populateCordovaAssetsCache]; 327 | 328 | // Store the app version. 329 | [[NSUserDefaults standardUserDefaults] setObject:currentAppVersion forKey:CORDOVA_ASSETS_VERSION_KEY]; 330 | } 331 | } 332 | } 333 | 334 | return YES; 335 | } 336 | 337 | -(void)populateCordovaAssetsCache 338 | { 339 | NSFileManager *fileManager = [[NSFileManager alloc] init]; 340 | NSString *wwwDirectoryPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/www"]; 341 | NSURL *wwwDirectoryUrl = [NSURL fileURLWithPath:wwwDirectoryPath isDirectory:YES]; 342 | NSArray *keys = [NSArray arrayWithObject:NSURLIsDirectoryKey]; 343 | 344 | NSDirectoryEnumerator *enumerator = [fileManager 345 | enumeratorAtURL:wwwDirectoryUrl 346 | includingPropertiesForKeys:keys 347 | options:0 348 | errorHandler:^(NSURL *url, NSError *error) { 349 | // Handle the error. 350 | // Return YES if the enumeration should continue after the error. 351 | return YES; 352 | } 353 | ]; 354 | 355 | // TODO: Prevent caching of sw_assets? 356 | for (NSURL *url in enumerator) { 357 | NSError *error; 358 | NSNumber *isDirectory = nil; 359 | if (![url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) { 360 | // Handle error. 361 | } else if (![isDirectory boolValue]) { 362 | [self addToCordovaAssetsCache:url]; 363 | } 364 | } 365 | } 366 | 367 | -(void)addToCordovaAssetsCache:(NSURL *)url 368 | { 369 | // Create an NSURLRequest. 370 | NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url]; 371 | 372 | // Ensure we're fetching purely. 373 | [NSURLProtocol setProperty:@YES forKey:@"PureFetch" inRequest:urlRequest]; 374 | 375 | // Create a connection and send the request. 376 | FetchConnectionDelegate *delegate = [FetchConnectionDelegate new]; 377 | delegate.resolve = ^(ServiceWorkerResponse *response) { 378 | // Get or create the specified cache. 379 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 380 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 381 | ServiceWorkerCache *cache = [cacheStorage cacheWithName:CORDOVA_ASSETS_CACHE_NAME]; 382 | 383 | // Create a URL request using a relative path. 384 | NSMutableURLRequest *shortUrlRequest = [self nativeRequestFromDictionary:@{@"url": [url absoluteString]}]; 385 | NSLog(@"Using short url: %@", shortUrlRequest.URL); 386 | 387 | // Put the request and response in the cache. 388 | [cache putRequest:shortUrlRequest andResponse:response inContext:moc]; 389 | }; 390 | [NSURLConnection connectionWithRequest:urlRequest delegate:delegate]; 391 | } 392 | 393 | -(ServiceWorkerCacheStorage *)cacheStorageForScope:(NSURL *)scope 394 | { 395 | if (self.cacheStorageMap == nil) { 396 | self.cacheStorageMap = [[NSMutableDictionary alloc] initWithCapacity:1]; 397 | } 398 | [self initializeStorage]; 399 | ServiceWorkerCacheStorage *cachesForScope = (ServiceWorkerCacheStorage *)[self.cacheStorageMap objectForKey:scope]; 400 | if (cachesForScope == nil) { 401 | // TODO: Init this properly, using `initWithEntity:insertIntoManagedObjectContext:`. 402 | cachesForScope = [[ServiceWorkerCacheStorage alloc] initWithContext:moc]; 403 | [self.cacheStorageMap setObject:cachesForScope forKey:scope]; 404 | } 405 | return cachesForScope; 406 | } 407 | 408 | -(void)defineFunctionsInContext:(JSContext *)context 409 | { 410 | // Cache functions. 411 | 412 | // Resolve with a response. 413 | context[@"cacheMatch"] = ^(JSValue *cacheName, JSValue *request, JSValue *options, JSValue *resolve, JSValue *reject) { 414 | // Retrieve the caches. 415 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 416 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 417 | 418 | // Convert the given request into an NSURLRequest. 419 | NSURLRequest *urlRequest = [self nativeRequestFromJsRequest:request]; 420 | 421 | // Check for a match in the cache. 422 | // TODO: Deal with multiple matches. 423 | ServiceWorkerResponse *cachedResponse; 424 | if (cacheName == nil || !cacheName.isString || cacheName.toString.length == 0) { 425 | cachedResponse = [cacheStorage matchForRequest:urlRequest]; 426 | } else { 427 | cachedResponse = [[cacheStorage cacheWithName:[cacheName toString]] matchForRequest:urlRequest inContext:moc]; 428 | } 429 | 430 | if (cachedResponse != nil) { 431 | // Convert the response to a dictionary and send it to the promise resolver. 432 | NSDictionary *responseDictionary = [cachedResponse toDictionary]; 433 | [resolve callWithArguments:@[responseDictionary]]; 434 | } else { 435 | [resolve callWithArguments:@[[NSNull null]]]; 436 | } 437 | }; 438 | 439 | // Resolve with a list of responses. 440 | context[@"cacheMatchAll"] = ^(JSValue *cacheName, JSValue *request, JSValue *options, JSValue *resolve, JSValue *reject) { 441 | 442 | }; 443 | 444 | // Resolve with nothing. 445 | context[@"cachePut"] = ^(JSValue *cacheName, JSValue *request, JSValue *response, JSValue *resolve, JSValue *reject) { 446 | // Retrieve the caches. 447 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 448 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 449 | 450 | // Get or create the specified cache. 451 | ServiceWorkerCache *cache = [cacheStorage cacheWithName:[cacheName toString]]; 452 | 453 | // Convert the given request into an NSURLRequest. 454 | NSMutableURLRequest *urlRequest; 455 | if ([request isString]) { 456 | urlRequest = [self nativeRequestFromDictionary:@{@"url" : [request toString]}]; 457 | } else { 458 | urlRequest = [self nativeRequestFromJsRequest:request]; 459 | } 460 | 461 | // Convert the response into a ServiceWorkerResponse. 462 | ServiceWorkerResponse *serviceWorkerResponse = [ServiceWorkerResponse responseFromJSValue:response]; 463 | [cache putRequest:urlRequest andResponse:serviceWorkerResponse inContext:moc]; 464 | 465 | // Resolve! 466 | [resolve callWithArguments:@[[NSNull null]]]; 467 | }; 468 | 469 | // Resolve with a boolean. 470 | context[@"cacheDelete"] = ^(JSValue *cacheName, JSValue *request, JSValue *options, JSValue *resolve, JSValue *reject) { 471 | // Retrieve the caches. 472 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 473 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 474 | 475 | // Get or create the specified cache. 476 | ServiceWorkerCache *cache =[cacheStorage cacheWithName:[cacheName toString]]; 477 | 478 | // Convert the given request into an NSURLRequest. 479 | NSURLRequest *urlRequest = [self nativeRequestFromJsRequest:request]; 480 | 481 | // Delete the request key from the cache. 482 | [cache deleteRequest:urlRequest fromContext:moc]; 483 | 484 | // Resolve! 485 | [resolve callWithArguments:@[[NSNull null]]]; 486 | }; 487 | 488 | // Resolve with a list of requests. 489 | context[@"cacheKeys"] = ^(JSValue *cacheName, JSValue *request, JSValue *options, JSValue *resolve, JSValue *reject) { 490 | // Retrieve the caches. 491 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 492 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 493 | 494 | // Get or create the specified cache. 495 | ServiceWorkerCache *cache =[cacheStorage cacheWithName:[cacheName toString]]; 496 | 497 | // Return the requests from the cache. 498 | // TODO: Use the given (optional) request. 499 | NSArray *cacheEntries = [cache requestsFromContext:moc]; 500 | NSMutableArray *requests = [NSMutableArray new]; 501 | for (ServiceWorkerCacheEntry *entry in cacheEntries) { 502 | NSURLRequest *urlRequest = (NSURLRequest *)[NSKeyedUnarchiver unarchiveObjectWithData:entry.request]; 503 | NSString *method = [urlRequest HTTPMethod]; 504 | NSString *url = [[urlRequest URL] absoluteString]; 505 | NSDictionary *headers = [urlRequest allHTTPHeaderFields]; 506 | if (headers == nil) { 507 | headers = [NSDictionary new]; 508 | } 509 | NSDictionary *requestDictionary = @{ @"method": method, @"url": url, @"headers": headers }; 510 | [requests addObject:requestDictionary]; 511 | } 512 | [resolve callWithArguments:@[requests]]; 513 | }; 514 | 515 | 516 | // CacheStorage functions. 517 | 518 | // Resolve with a boolean. 519 | context[@"cachesHas"] = ^(JSValue *cacheName, JSValue *resolve, JSValue *reject) { 520 | // Retrieve the caches. 521 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 522 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 523 | 524 | // Check whether the specified cache exists. 525 | BOOL hasCache = [cacheStorage hasCacheWithName:[cacheName toString]]; 526 | 527 | // Resolve! 528 | [resolve callWithArguments:@[[NSNumber numberWithBool:hasCache]]]; 529 | }; 530 | 531 | // Resolve with a boolean. 532 | context[@"cachesDelete"] = ^(JSValue *cacheName, JSValue *resolve, JSValue *reject) { 533 | // Retrieve the caches. 534 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 535 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 536 | 537 | // Delete the specified cache. 538 | BOOL cacheDeleted = [cacheStorage deleteCacheWithName:[cacheName toString]]; 539 | 540 | // Resolve! 541 | [resolve callWithArguments:@[[NSNumber numberWithBool:cacheDeleted]]]; 542 | }; 543 | 544 | // Resolve with a list of strings. 545 | context[@"cachesKeys"] = ^(JSValue *resolve, JSValue *reject) { 546 | // Retrieve the caches. 547 | NSURL *scope = [NSURL URLWithString:self.absoluteScope]; 548 | ServiceWorkerCacheStorage *cacheStorage = [self cacheStorageForScope:scope]; 549 | 550 | // Resolve! 551 | [resolve callWithArguments:@[[cacheStorage.caches allKeys]]]; 552 | }; 553 | } 554 | 555 | -(NSMutableURLRequest *)nativeRequestFromJsRequest:(JSValue *)jsRequest 556 | { 557 | NSDictionary *requestDictionary = [jsRequest toDictionary]; 558 | return [self nativeRequestFromDictionary:requestDictionary]; 559 | } 560 | 561 | -(NSMutableURLRequest *)nativeRequestFromDictionary:(NSDictionary *)requestDictionary 562 | { 563 | NSString *urlString = requestDictionary[@"url"]; 564 | if ([urlString hasPrefix:rootPath_]) { 565 | urlString = [NSString stringWithFormat:@"%@%@", self.absoluteScope, [urlString substringFromIndex:[rootPath_ length]]]; 566 | } 567 | return [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]]; 568 | } 569 | 570 | @end 571 | 572 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCacheEntry.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | 23 | @class ServiceWorkerCache; 24 | 25 | @interface ServiceWorkerCacheEntry : NSManagedObject 26 | 27 | @property (nonatomic, retain) NSString * query; 28 | @property (nonatomic, retain) NSData * request; 29 | @property (nonatomic, retain) NSData * response; 30 | @property (nonatomic, retain) NSString * url; 31 | @property (nonatomic, retain) ServiceWorkerCache *cache; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerCacheEntry.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "ServiceWorkerCacheEntry.h" 21 | #import "ServiceWorkerCache.h" 22 | 23 | @implementation ServiceWorkerCacheEntry 24 | 25 | @dynamic query; 26 | @dynamic request; 27 | @dynamic response; 28 | @dynamic url; 29 | @dynamic cache; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerRequest.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | @interface ServiceWorkerRequest : NSObject 21 | 22 | @property (nonatomic, strong) NSURLRequest *request; 23 | @property (nonatomic, strong) NSNumber *requestId; 24 | @property (nonatomic, strong) NSURLProtocol *protocol; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerRequest.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "ServiceWorkerRequest.h" 21 | 22 | @implementation ServiceWorkerRequest 23 | 24 | @synthesize request = _request; 25 | @synthesize requestId = _requestId; 26 | @synthesize protocol = _protocol; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerResponse.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | 22 | @interface ServiceWorkerResponse : NSObject 23 | 24 | @property (nonatomic, strong) NSString *url; 25 | @property (nonatomic, strong) NSData *body; 26 | @property (nonatomic, strong) NSNumber *status; 27 | @property (nonatomic, strong) NSDictionary *headers; 28 | 29 | + (ServiceWorkerResponse *)responseFromJSValue:(JSValue *)value; 30 | + (ServiceWorkerResponse *)responseFromDictionary:(NSDictionary *)dictionary; 31 | - (NSDictionary *)toDictionary; 32 | 33 | @end 34 | 35 | -------------------------------------------------------------------------------- /src/ios/ServiceWorkerResponse.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import "ServiceWorkerResponse.h" 22 | 23 | @implementation ServiceWorkerResponse 24 | 25 | @synthesize url = _url; 26 | @synthesize body = _body; 27 | @synthesize status = _status; 28 | 29 | - (id) initWithUrl:(NSString *)url body:(NSData *)body status:(NSNumber *)status headers:(NSDictionary *)headers { 30 | if (self = [super init]) { 31 | _url = url; 32 | _body = body; 33 | _status = status; 34 | _headers = headers; 35 | } 36 | return self; 37 | } 38 | 39 | + (ServiceWorkerResponse *)responseFromJSValue:(JSValue *)jvalue 40 | { 41 | NSString *url = [jvalue[@"url"] toString]; 42 | NSString *body = [jvalue[@"body"] toString]; 43 | NSData *decodedBody = [[NSData alloc] initWithBase64EncodedString:body options:0]; 44 | NSNumber *status = [jvalue[@"status"] toNumber]; 45 | NSDictionary *headers = [jvalue[@"headers"][@"headerDict"] toDictionary]; 46 | return [[ServiceWorkerResponse alloc] initWithUrl:url body:decodedBody status:status headers:headers]; 47 | } 48 | 49 | + (ServiceWorkerResponse *)responseFromDictionary:(NSDictionary *)dictionary 50 | { 51 | NSString *url = dictionary[@"url"]; 52 | NSData *body = dictionary[@"body"]; 53 | NSNumber *status = dictionary[@"status"]; 54 | NSDictionary *headers = dictionary[@"headers"]; 55 | return [[ServiceWorkerResponse alloc] initWithUrl:url body:body status:status headers:headers]; 56 | } 57 | 58 | - (NSDictionary *)toDictionary { 59 | // Convert the body to base64. 60 | NSString *encodedBody = [self.body base64Encoding]; 61 | return [NSDictionary dictionaryWithObjects:@[self.url, encodedBody, self.status, self.headers ? self.headers : [NSDictionary new]] forKeys:@[@"url", @"body", @"status", @"headers"]]; 62 | } 63 | 64 | 65 | - (void)encodeWithCoder:(NSCoder *)aCoder 66 | { 67 | [aCoder encodeObject:self.url forKey:@"url"]; 68 | [aCoder encodeObject:self.body forKey:@"body"]; 69 | [aCoder encodeInt:[self.status intValue] forKey:@"status"]; 70 | [aCoder encodeObject:self.headers forKey:@"headers"]; 71 | } 72 | 73 | - (id)initWithCoder:(NSCoder *)decoder 74 | { 75 | if (self = [super init]) { 76 | self.url = [decoder decodeObjectForKey:@"url"]; 77 | self.body = [decoder decodeObjectForKey:@"body"]; 78 | self.status = [NSNumber numberWithInt:[decoder decodeIntForKey:@"status"]]; 79 | self.headers = [decoder decodeObjectForKey:@"headers"]; 80 | } 81 | return self; 82 | } 83 | 84 | @end 85 | 86 | -------------------------------------------------------------------------------- /www/kamino.js: -------------------------------------------------------------------------------- 1 | /*! Kamino v0.0.1 | http://github.com/Cyril-sf/kamino.js | Copyright 2012, Kit Cambridge | http://kit.mit-license.org */ 2 | (function(window) { 3 | // Convenience aliases. 4 | var getClass = {}.toString, isProperty, forEach, undef; 5 | 6 | Kamino = {}; 7 | if (typeof exports !== 'undefined') { 8 | if (typeof module !== 'undefined' && module.exports) { 9 | exports = module.exports = Kamino; 10 | } 11 | exports.Kamino = Kamino; 12 | } else { 13 | window['Kamino'] = Kamino; 14 | } 15 | 16 | Kamino.VERSION = '0.1.0'; 17 | 18 | KaminoException = function() { 19 | this.name = "KaminoException"; 20 | this.number = 25; 21 | this.message = "Uncaught Error: DATA_CLONE_ERR: Kamino Exception 25"; 22 | }; 23 | 24 | // Test the `Date#getUTC*` methods. Based on work by @Yaffle. 25 | var isExtended = new Date(-3509827334573292); 26 | try { 27 | // The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical 28 | // results for certain dates in Opera >= 10.53. 29 | isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() == 1 && 30 | // Safari < 2.0.2 stores the internal millisecond time value correctly, 31 | // but clips the values returned by the date methods to the range of 32 | // signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]). 33 | isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708; 34 | } catch (exception) {} 35 | 36 | // IE <= 7 doesn't support accessing string characters using square 37 | // bracket notation. IE 8 only supports this for primitives. 38 | var charIndexBuggy = "A"[0] != "A"; 39 | 40 | // Define additional utility methods if the `Date` methods are buggy. 41 | if (!isExtended) { 42 | var floor = Math.floor; 43 | // A mapping between the months of the year and the number of days between 44 | // January 1st and the first of the respective month. 45 | var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 46 | // Internal: Calculates the number of days between the Unix epoch and the 47 | // first day of the given month. 48 | var getDay = function (year, month) { 49 | return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400); 50 | }; 51 | } 52 | 53 | // Internal: Determines if a property is a direct property of the given 54 | // object. Delegates to the native `Object#hasOwnProperty` method. 55 | if (!(isProperty = {}.hasOwnProperty)) { 56 | isProperty = function (property) { 57 | var members = {}, constructor; 58 | if ((members.__proto__ = null, members.__proto__ = { 59 | // The *proto* property cannot be set multiple times in recent 60 | // versions of Firefox and SeaMonkey. 61 | "toString": 1 62 | }, members).toString != getClass) { 63 | // Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but 64 | // supports the mutable *proto* property. 65 | isProperty = function (property) { 66 | // Capture and break the object's prototype chain (see section 8.6.2 67 | // of the ES 5.1 spec). The parenthesized expression prevents an 68 | // unsafe transformation by the Closure Compiler. 69 | var original = this.__proto__, result = property in (this.__proto__ = null, this); 70 | // Restore the original prototype chain. 71 | this.__proto__ = original; 72 | return result; 73 | }; 74 | } else { 75 | // Capture a reference to the top-level `Object` constructor. 76 | constructor = members.constructor; 77 | // Use the `constructor` property to simulate `Object#hasOwnProperty` in 78 | // other environments. 79 | isProperty = function (property) { 80 | var parent = (this.constructor || constructor).prototype; 81 | return property in this && !(property in parent && this[property] === parent[property]); 82 | }; 83 | } 84 | members = null; 85 | return isProperty.call(this, property); 86 | }; 87 | } 88 | 89 | // Internal: Normalizes the `for...in` iteration algorithm across 90 | // environments. Each enumerated key is yielded to a `callback` function. 91 | forEach = function (object, callback) { 92 | var size = 0, Properties, members, property, forEach; 93 | 94 | // Tests for bugs in the current environment's `for...in` algorithm. The 95 | // `valueOf` property inherits the non-enumerable flag from 96 | // `Object.prototype` in older versions of IE, Netscape, and Mozilla. 97 | (Properties = function () { 98 | this.valueOf = 0; 99 | }).prototype.valueOf = 0; 100 | 101 | // Iterate over a new instance of the `Properties` class. 102 | members = new Properties(); 103 | for (property in members) { 104 | // Ignore all properties inherited from `Object.prototype`. 105 | if (isProperty.call(members, property)) { 106 | size++; 107 | } 108 | } 109 | Properties = members = null; 110 | 111 | // Normalize the iteration algorithm. 112 | if (!size) { 113 | // A list of non-enumerable properties inherited from `Object.prototype`. 114 | members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"]; 115 | // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable 116 | // properties. 117 | forEach = function (object, callback) { 118 | var isFunction = getClass.call(object) == "[object Function]", property, length; 119 | for (property in object) { 120 | // Gecko <= 1.0 enumerates the `prototype` property of functions under 121 | // certain conditions; IE does not. 122 | if (!(isFunction && property == "prototype") && isProperty.call(object, property)) { 123 | callback(property); 124 | } 125 | } 126 | // Manually invoke the callback for each non-enumerable property. 127 | for (length = members.length; property = members[--length]; isProperty.call(object, property) && callback(property)); 128 | }; 129 | } else if (size == 2) { 130 | // Safari <= 2.0.4 enumerates shadowed properties twice. 131 | forEach = function (object, callback) { 132 | // Create a set of iterated properties. 133 | var members = {}, isFunction = getClass.call(object) == "[object Function]", property; 134 | for (property in object) { 135 | // Store each property name to prevent double enumeration. The 136 | // `prototype` property of functions is not enumerated due to cross- 137 | // environment inconsistencies. 138 | if (!(isFunction && property == "prototype") && !isProperty.call(members, property) && (members[property] = 1) && isProperty.call(object, property)) { 139 | callback(property); 140 | } 141 | } 142 | }; 143 | } else { 144 | // No bugs detected; use the standard `for...in` algorithm. 145 | forEach = function (object, callback) { 146 | var isFunction = getClass.call(object) == "[object Function]", property, isConstructor; 147 | for (property in object) { 148 | if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) { 149 | callback(property); 150 | } 151 | } 152 | // Manually invoke the callback for the `constructor` property due to 153 | // cross-environment inconsistencies. 154 | if (isConstructor || isProperty.call(object, (property = "constructor"))) { 155 | callback(property); 156 | } 157 | }; 158 | } 159 | return forEach(object, callback); 160 | }; 161 | 162 | // Public: Serializes a JavaScript `value` as a string. The optional 163 | // `filter` argument may specify either a function that alters how object and 164 | // array members are serialized, or an array of strings and numbers that 165 | // indicates which properties should be serialized. The optional `width` 166 | // argument may be either a string or number that specifies the indentation 167 | // level of the output. 168 | 169 | // Internal: A map of control characters and their escaped equivalents. 170 | var Escapes = { 171 | "\\": "\\\\", 172 | '"': '\\"', 173 | "\b": "\\b", 174 | "\f": "\\f", 175 | "\n": "\\n", 176 | "\r": "\\r", 177 | "\t": "\\t" 178 | }; 179 | 180 | // Internal: Converts `value` into a zero-padded string such that its 181 | // length is at least equal to `width`. The `width` must be <= 6. 182 | var toPaddedString = function (width, value) { 183 | // The `|| 0` expression is necessary to work around a bug in 184 | // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`. 185 | return ("000000" + (value || 0)).slice(-width); 186 | }; 187 | 188 | // Internal: Double-quotes a string `value`, replacing all ASCII control 189 | // characters (characters with code unit values between 0 and 31) with 190 | // their escaped equivalents. This is an implementation of the 191 | // `Quote(value)` operation defined in ES 5.1 section 15.12.3. 192 | var quote = function (value) { 193 | var result = '"', index = 0, symbol; 194 | for (; symbol = value.charAt(index); index++) { 195 | // Escape the reverse solidus, double quote, backspace, form feed, line 196 | // feed, carriage return, and tab characters. 197 | result += '\\"\b\f\n\r\t'.indexOf(symbol) > -1 ? Escapes[symbol] : 198 | // If the character is a control character, append its Unicode escape 199 | // sequence; otherwise, append the character as-is. 200 | (Escapes[symbol] = symbol < " " ? "\\u00" + toPaddedString(2, symbol.charCodeAt(0).toString(16)) : symbol); 201 | } 202 | return result + '"'; 203 | }; 204 | 205 | // Internal: detects if an object is a DOM element. 206 | // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object 207 | var isElement = function(o) { 208 | return ( 209 | typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 210 | o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string" 211 | ); 212 | }; 213 | 214 | // Internal: Recursively serializes an object. Implements the 215 | // `Str(key, holder)`, `JO(value)`, and `JA(value)` operations. 216 | var serialize = function (property, object, callback, properties, whitespace, indentation, stack) { 217 | var value = object[property], originalClassName, className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, any, result, 218 | regExpSource, regExpModifiers = ""; 219 | if( value instanceof Error || value instanceof Function) { 220 | throw new KaminoException(); 221 | } 222 | if( isElement( value ) ) { 223 | throw new KaminoException(); 224 | } 225 | if (typeof value == "object" && value) { 226 | originalClassName = getClass.call(value); 227 | if (originalClassName == "[object Date]" && !isProperty.call(value, "toJSON")) { 228 | if (value > -1 / 0 && value < 1 / 0) { 229 | value = value.toUTCString().replace("GMT", "UTC"); 230 | } else { 231 | value = null; 232 | } 233 | } else if (typeof value.toJSON == "function" && ((originalClassName != "[object Number]" && originalClassName != "[object String]" && originalClassName != "[object Array]") || isProperty.call(value, "toJSON"))) { 234 | // Prototype <= 1.6.1 adds non-standard `toJSON` methods to the 235 | // `Number`, `String`, `Date`, and `Array` prototypes. JSON 3 236 | // ignores all `toJSON` methods on these objects unless they are 237 | // defined directly on an instance. 238 | value = value.toJSON(property); 239 | } 240 | } 241 | if (callback) { 242 | // If a replacement function was provided, call it to obtain the value 243 | // for serialization. 244 | value = callback.call(object, property, value); 245 | } 246 | if (value === null) { 247 | return "null"; 248 | } 249 | if (value === undefined) { 250 | return undefined; 251 | } 252 | className = getClass.call(value); 253 | if (className == "[object Boolean]") { 254 | // Booleans are represented literally. 255 | return "" + value; 256 | } else if (className == "[object Number]") { 257 | // Kamino numbers must be finite. `Infinity` and `NaN` are serialized as 258 | // `"null"`. 259 | if( value === Number.POSITIVE_INFINITY ) { 260 | return "Infinity"; 261 | } else if( value === Number.NEGATIVE_INFINITY ) { 262 | return "NInfinity"; 263 | } else if( isNaN( value ) ) { 264 | return "NaN"; 265 | } 266 | return "" + value; 267 | } else if (className == "[object RegExp]") { 268 | // Strings are double-quoted and escaped. 269 | regExpSource = value.source; 270 | regExpModifiers += value.ignoreCase ? "i" : ""; 271 | regExpModifiers += value.global ? "g" : ""; 272 | regExpModifiers += value.multiline ? "m" : ""; 273 | 274 | regExpSource = quote(charIndexBuggy ? regExpSource.split("") : regExpSource); 275 | regExpModifiers = quote(charIndexBuggy ? regExpModifiers.split("") : regExpModifiers); 276 | 277 | // Adds the RegExp prefix. 278 | value = '^' + regExpSource + regExpModifiers; 279 | 280 | return value; 281 | } else if (className == "[object String]") { 282 | // Strings are double-quoted and escaped. 283 | value = quote(charIndexBuggy ? value.split("") : value); 284 | 285 | if( originalClassName == "[object Date]") { 286 | // Adds the Date prefix. 287 | value = '%' + value; 288 | } 289 | 290 | return value; 291 | } 292 | // Recursively serialize objects and arrays. 293 | if (typeof value == "object") { 294 | // Check for cyclic structures. This is a linear search; performance 295 | // is inversely proportional to the number of unique nested objects. 296 | for (length = stack.length; length--;) { 297 | if (stack[length] === value) { 298 | return "&" + length; 299 | } 300 | } 301 | // Add the object to the stack of traversed objects. 302 | stack.push(value); 303 | results = []; 304 | // Save the current indentation level and indent one additional level. 305 | prefix = indentation; 306 | indentation += whitespace; 307 | if (className == "[object Array]") { 308 | // Recursively serialize array elements. 309 | for (index = 0, length = value.length; index < length; any || (any = true), index++) { 310 | element = serialize(index, value, callback, properties, whitespace, indentation, stack); 311 | results.push(element === undef ? "null" : element); 312 | } 313 | result = any ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]"; 314 | } else { 315 | // Recursively serialize object members. Members are selected from 316 | // either a user-specified list of property names, or the object 317 | // itself. 318 | forEach(properties || value, function (property) { 319 | var element = serialize(property, value, callback, properties, whitespace, indentation, stack); 320 | if (element !== undef) { 321 | // According to ES 5.1 section 15.12.3: "If `gap` {whitespace} 322 | // is not the empty string, let `member` {quote(property) + ":"} 323 | // be the concatenation of `member` and the `space` character." 324 | // The "`space` character" refers to the literal space 325 | // character, not the `space` {width} argument provided to 326 | // `JSON.stringify`. 327 | results.push(quote(charIndexBuggy ? property.split("") : property) + ":" + (whitespace ? " " : "") + element); 328 | } 329 | any || (any = true); 330 | }); 331 | result = any ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}"; 332 | } 333 | return result; 334 | } 335 | }; 336 | 337 | // Public: `Kamino.stringify`. See ES 5.1 section 15.12.3. 338 | Kamino.stringify = function (source, filter, width) { 339 | var whitespace, callback, properties; 340 | if (typeof filter == "function" || typeof filter == "object" && filter) { 341 | if (getClass.call(filter) == "[object Function]") { 342 | callback = filter; 343 | } else if (getClass.call(filter) == "[object Array]") { 344 | // Convert the property names array into a makeshift set. 345 | properties = {}; 346 | for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((getClass.call(value) == "[object String]" || getClass.call(value) == "[object Number]") && (properties[value] = 1))); 347 | } 348 | } 349 | if (width) { 350 | if (getClass.call(width) == "[object Number]") { 351 | // Convert the `width` to an integer and create a string containing 352 | // `width` number of space characters. 353 | if ((width -= width % 1) > 0) { 354 | for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " "); 355 | } 356 | } else if (getClass.call(width) == "[object String]") { 357 | whitespace = width.length <= 10 ? width : width.slice(0, 10); 358 | } 359 | } 360 | // Opera <= 7.54u2 discards the values associated with empty string keys 361 | // (`""`) only if they are used directly within an object member list 362 | // (e.g., `!("" in { "": 1})`). 363 | return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []); 364 | }; 365 | 366 | // Public: Parses a source string. 367 | var fromCharCode = String.fromCharCode; 368 | 369 | // Internal: A map of escaped control characters and their unescaped 370 | // equivalents. 371 | var Unescapes = { 372 | "\\": "\\", 373 | '"': '"', 374 | "/": "/", 375 | "b": "\b", 376 | "t": "\t", 377 | "n": "\n", 378 | "f": "\f", 379 | "r": "\r" 380 | }; 381 | 382 | // Internal: Stores the parser state. 383 | var Index, Source, stack; 384 | 385 | // Internal: Resets the parser state and throws a `SyntaxError`. 386 | var abort = function() { 387 | Index = Source = null; 388 | throw SyntaxError(); 389 | }; 390 | 391 | var parseString = function(prefix) { 392 | prefix = prefix || ""; 393 | var source = Source, length = source.length, value, symbol, begin, position; 394 | // Advance to the next character and parse a Kamino string at the 395 | // current position. String tokens are prefixed with the sentinel 396 | // `@` character to distinguish them from punctuators. 397 | for (value = prefix, Index++; Index < length;) { 398 | symbol = source[Index]; 399 | if (symbol < " ") { 400 | // Unescaped ASCII control characters are not permitted. 401 | abort(); 402 | } else if (symbol == "\\") { 403 | // Parse escaped Kamino control characters, `"`, `\`, `/`, and 404 | // Unicode escape sequences. 405 | symbol = source[++Index]; 406 | if ('\\"/btnfr'.indexOf(symbol) > -1) { 407 | // Revive escaped control characters. 408 | value += Unescapes[symbol]; 409 | Index++; 410 | } else if (symbol == "u") { 411 | // Advance to the first character of the escape sequence. 412 | begin = ++Index; 413 | // Validate the Unicode escape sequence. 414 | for (position = Index + 4; Index < position; Index++) { 415 | symbol = source[Index]; 416 | // A valid sequence comprises four hexdigits that form a 417 | // single hexadecimal value. 418 | if (!(symbol >= "0" && symbol <= "9" || symbol >= "a" && symbol <= "f" || symbol >= "A" && symbol <= "F")) { 419 | // Invalid Unicode escape sequence. 420 | abort(); 421 | } 422 | } 423 | // Revive the escaped character. 424 | value += fromCharCode("0x" + source.slice(begin, Index)); 425 | } else { 426 | // Invalid escape sequence. 427 | abort(); 428 | } 429 | } else { 430 | if (symbol == '"') { 431 | // An unescaped double-quote character marks the end of the 432 | // string. 433 | break; 434 | } 435 | // Append the original character as-is. 436 | value += symbol; 437 | Index++; 438 | } 439 | } 440 | if (source[Index] == '"') { 441 | Index++; 442 | // Return the revived string. 443 | return value; 444 | } 445 | // Unterminated string. 446 | abort(); 447 | }; 448 | 449 | // Internal: Returns the next token, or `"$"` if the parser has reached 450 | // the end of the source string. A token may be a string, number, `null` 451 | // literal, `NaN` literal or Boolean literal. 452 | var lex = function () { 453 | var source = Source, length = source.length, symbol, value, begin, position, sign, 454 | dateString, regExpSource, regExpModifiers; 455 | while (Index < length) { 456 | symbol = source[Index]; 457 | if ("\t\r\n ".indexOf(symbol) > -1) { 458 | // Skip whitespace tokens, including tabs, carriage returns, line 459 | // feeds, and space characters. 460 | Index++; 461 | } else if ("{}[]:,".indexOf(symbol) > -1) { 462 | // Parse a punctuator token at the current position. 463 | Index++; 464 | return symbol; 465 | } else if (symbol == '"') { 466 | // Parse strings. 467 | return parseString("@"); 468 | } else if (symbol == '%') { 469 | // Parse dates. 470 | Index++; 471 | symbol = source[Index]; 472 | if(symbol == '"') { 473 | dateString = parseString(); 474 | return new Date( dateString ); 475 | } 476 | abort(); 477 | } else if (symbol == '^') { 478 | // Parse regular expressions. 479 | Index++; 480 | symbol = source[Index]; 481 | if(symbol == '"') { 482 | regExpSource = parseString(); 483 | 484 | symbol = source[Index]; 485 | if(symbol == '"') { 486 | regExpModifiers = parseString(); 487 | 488 | return new RegExp( regExpSource, regExpModifiers ); 489 | } 490 | } 491 | abort(); 492 | } else if (symbol == '&') { 493 | // Parse object references. 494 | Index++; 495 | symbol = source[Index]; 496 | if (symbol >= "0" && symbol <= "9") { 497 | Index++; 498 | return stack[symbol]; 499 | } 500 | abort(); 501 | } else { 502 | // Parse numbers and literals. 503 | begin = Index; 504 | // Advance the scanner's position past the sign, if one is 505 | // specified. 506 | if (symbol == "-") { 507 | sign = true; 508 | symbol = source[++Index]; 509 | } 510 | // Parse an integer or floating-point value. 511 | if (symbol >= "0" && symbol <= "9") { 512 | // Leading zeroes are interpreted as octal literals. 513 | if (symbol == "0" && (symbol = source[Index + 1], symbol >= "0" && symbol <= "9")) { 514 | // Illegal octal literal. 515 | abort(); 516 | } 517 | sign = false; 518 | // Parse the integer component. 519 | for (; Index < length && (symbol = source[Index], symbol >= "0" && symbol <= "9"); Index++); 520 | // Floats cannot contain a leading decimal point; however, this 521 | // case is already accounted for by the parser. 522 | if (source[Index] == ".") { 523 | position = ++Index; 524 | // Parse the decimal component. 525 | for (; position < length && (symbol = source[position], symbol >= "0" && symbol <= "9"); position++); 526 | if (position == Index) { 527 | // Illegal trailing decimal. 528 | abort(); 529 | } 530 | Index = position; 531 | } 532 | // Parse exponents. 533 | symbol = source[Index]; 534 | if (symbol == "e" || symbol == "E") { 535 | // Skip past the sign following the exponent, if one is 536 | // specified. 537 | symbol = source[++Index]; 538 | if (symbol == "+" || symbol == "-") { 539 | Index++; 540 | } 541 | // Parse the exponential component. 542 | for (position = Index; position < length && (symbol = source[position], symbol >= "0" && symbol <= "9"); position++); 543 | if (position == Index) { 544 | // Illegal empty exponent. 545 | abort(); 546 | } 547 | Index = position; 548 | } 549 | // Coerce the parsed value to a JavaScript number. 550 | return +source.slice(begin, Index); 551 | } 552 | // A negative sign may only precede numbers. 553 | if (sign) { 554 | abort(); 555 | } 556 | // `true`, `false`, `Infinity`, `-Infinity`, `NaN` and `null` literals. 557 | if (source.slice(Index, Index + 4) == "true") { 558 | Index += 4; 559 | return true; 560 | } else if (source.slice(Index, Index + 5) == "false") { 561 | Index += 5; 562 | return false; 563 | } else if (source.slice(Index, Index + 8) == "Infinity") { 564 | Index += 8; 565 | return Infinity; 566 | } else if (source.slice(Index, Index + 9) == "NInfinity") { 567 | Index += 9; 568 | return -Infinity; 569 | } else if (source.slice(Index, Index + 3) == "NaN") { 570 | Index += 3; 571 | return NaN; 572 | } else if (source.slice(Index, Index + 4) == "null") { 573 | Index += 4; 574 | return null; 575 | } 576 | // Unrecognized token. 577 | abort(); 578 | } 579 | } 580 | // Return the sentinel `$` character if the parser has reached the end 581 | // of the source string. 582 | return "$"; 583 | }; 584 | 585 | // Internal: Parses a Kamino `value` token. 586 | var get = function (value) { 587 | var results, any, key; 588 | if (value == "$") { 589 | // Unexpected end of input. 590 | abort(); 591 | } 592 | if (typeof value == "string") { 593 | if (value[0] == "@") { 594 | // Remove the sentinel `@` character. 595 | return value.slice(1); 596 | } 597 | // Parse object and array literals. 598 | if (value == "[") { 599 | // Parses a Kamino array, returning a new JavaScript array. 600 | results = []; 601 | stack[stack.length] = results; 602 | for (;; any || (any = true)) { 603 | value = lex(); 604 | // A closing square bracket marks the end of the array literal. 605 | if (value == "]") { 606 | break; 607 | } 608 | // If the array literal contains elements, the current token 609 | // should be a comma separating the previous element from the 610 | // next. 611 | if (any) { 612 | if (value == ",") { 613 | value = lex(); 614 | if (value == "]") { 615 | // Unexpected trailing `,` in array literal. 616 | abort(); 617 | } 618 | } else { 619 | // A `,` must separate each array element. 620 | abort(); 621 | } 622 | } 623 | // Elisions and leading commas are not permitted. 624 | if (value == ",") { 625 | abort(); 626 | } 627 | results.push(get(typeof value == "string" && charIndexBuggy ? value.split("") : value)); 628 | } 629 | return results; 630 | } else if (value == "{") { 631 | // Parses a Kamino object, returning a new JavaScript object. 632 | results = {}; 633 | stack[stack.length] = results; 634 | for (;; any || (any = true)) { 635 | value = lex(); 636 | // A closing curly brace marks the end of the object literal. 637 | if (value == "}") { 638 | break; 639 | } 640 | // If the object literal contains members, the current token 641 | // should be a comma separator. 642 | if (any) { 643 | if (value == ",") { 644 | value = lex(); 645 | if (value == "}") { 646 | // Unexpected trailing `,` in object literal. 647 | abort(); 648 | } 649 | } else { 650 | // A `,` must separate each object member. 651 | abort(); 652 | } 653 | } 654 | // Leading commas are not permitted, object property names must be 655 | // double-quoted strings, and a `:` must separate each property 656 | // name and value. 657 | if (value == "," || typeof value != "string" || value[0] != "@" || lex() != ":") { 658 | abort(); 659 | } 660 | var result = lex(); 661 | results[value.slice(1)] = get(typeof result == "string" && charIndexBuggy ? result.split("") : result); 662 | } 663 | return results; 664 | } 665 | // Unexpected token encountered. 666 | abort(); 667 | } 668 | return value; 669 | }; 670 | 671 | // Internal: Updates a traversed object member. 672 | var update = function(source, property, callback) { 673 | var element = walk(source, property, callback); 674 | if (element === undef) { 675 | delete source[property]; 676 | } else { 677 | source[property] = element; 678 | } 679 | }; 680 | 681 | // Internal: Recursively traverses a parsed Kamino object, invoking the 682 | // `callback` function for each value. This is an implementation of the 683 | // `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2. 684 | var walk = function (source, property, callback) { 685 | var value = source[property], length; 686 | if (typeof value == "object" && value) { 687 | if (getClass.call(value) == "[object Array]") { 688 | for (length = value.length; length--;) { 689 | update(value, length, callback); 690 | } 691 | } else { 692 | // `forEach` can't be used to traverse an array in Opera <= 8.54, 693 | // as `Object#hasOwnProperty` returns `false` for array indices 694 | // (e.g., `![1, 2, 3].hasOwnProperty("0")`). 695 | forEach(value, function (property) { 696 | update(value, property, callback); 697 | }); 698 | } 699 | } 700 | return callback.call(source, property, value); 701 | }; 702 | 703 | // Public: `Kamino.parse`. See ES 5.1 section 15.12.2. 704 | Kamino.parse = function (source, callback) { 705 | var result, value; 706 | Index = 0; 707 | Source = "" + source; 708 | stack = []; 709 | if (charIndexBuggy) { 710 | Source = source.split(""); 711 | } 712 | result = get(lex()); 713 | // If a Kamino string contains multiple tokens, it is invalid. 714 | if (lex() != "$") { 715 | abort(); 716 | } 717 | // Reset the parser state. 718 | Index = Source = null; 719 | return callback && getClass.call(callback) == "[object Function]" ? walk((value = {}, value[""] = result, value), "", callback) : result; 720 | }; 721 | 722 | Kamino.clone = function(source) { 723 | return Kamino.parse( Kamino.stringify(source) ); 724 | }; 725 | })(this); 726 | -------------------------------------------------------------------------------- /www/service_worker.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'); 2 | 3 | var ServiceWorker = function() { 4 | return this; 5 | }; 6 | 7 | ServiceWorker.prototype.postMessage = function(message, targetOrigin) { 8 | // TODO: Validate the target origin. 9 | 10 | // Serialize the message. 11 | var serializedMessage = Kamino.stringify(message); 12 | 13 | // Send the message to native for delivery to the JSContext. 14 | exec(null, null, "ServiceWorker", "postMessage", [serializedMessage, targetOrigin]); 15 | }; 16 | 17 | module.exports = ServiceWorker; 18 | 19 | -------------------------------------------------------------------------------- /www/service_worker_container.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'); 2 | 3 | var ServiceWorkerContainer = { 4 | //The ready promise is resolved when there is an active Service Worker with registration and the device is ready 5 | ready: new Promise(function(resolve, reject) { 6 | var innerResolve = function(result) { 7 | var onDeviceReady = function() { 8 | resolve(new ServiceWorkerRegistration(result.installing, result.waiting, new ServiceWorker(), result.registeringScriptUrl, result.scope)); 9 | } 10 | document.addEventListener('deviceready', onDeviceReady, false); 11 | } 12 | exec(innerResolve, null, "ServiceWorker", "serviceWorkerReady", []); 13 | }), 14 | register: function(scriptURL, options) { 15 | console.log("Registering " + scriptURL); 16 | return new Promise(function(resolve, reject) { 17 | var innerResolve = function(result) { 18 | resolve(new ServiceWorkerRegistration(result.installing, result.waiting, new ServiceWorker(), result.registeringScriptUrl, result.scope)); 19 | } 20 | exec(innerResolve, reject, "ServiceWorker", "register", [scriptURL, options]); 21 | }); 22 | } 23 | }; 24 | 25 | module.exports = ServiceWorkerContainer; 26 | -------------------------------------------------------------------------------- /www/service_worker_registration.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'); 2 | 3 | var ServiceWorkerRegistration = function(installing, waiting, active, registeringScriptURL, scope) { 4 | this.installing = installing; 5 | this.waiting = waiting; 6 | this.active = active; 7 | this.scope = scope; 8 | this.registeringScriptURL = registeringScriptURL; 9 | this.uninstalling = false; 10 | 11 | // TODO: Update? 12 | }; 13 | 14 | module.exports = ServiceWorkerRegistration; 15 | 16 | -------------------------------------------------------------------------------- /www/sw_assets/cache.js: -------------------------------------------------------------------------------- 1 | Cache = function(cacheName) { 2 | this.name = cacheName; 3 | return this; 4 | }; 5 | 6 | Cache.prototype.match = function(request, options) { 7 | var cacheName = this.name; 8 | return new Promise(function(resolve, reject) { 9 | var encodeResponse = function(response) { 10 | if (response) { 11 | response = new Response(window.atob(response.body), response.url, response.status, response.headers); 12 | } 13 | return resolve(response); 14 | }; 15 | // Call the native match function. 16 | cacheMatch(cacheName, request, options, encodeResponse, reject); 17 | }); 18 | }; 19 | 20 | Cache.prototype.matchAll = function(request, options) { 21 | var cacheName = this.name; 22 | return new Promise(function(resolve, reject) { 23 | var encodeResponses = function(responses) { 24 | if (responses instanceof Array) { 25 | var encodedResponses = []; 26 | for (var i=0; i < responses.length; ++i) { 27 | var response = responses[i]; 28 | encodedReponses.push(new Response(window.atob(response.body), response.url, response.status, response.headers)); 29 | } 30 | return resolve(encodedResponses); 31 | } 32 | return resolve(responses); 33 | }; 34 | // Call the native matchAll function. 35 | cacheMatchAll(cacheName, request, options, encodeResponses, reject); 36 | }); 37 | }; 38 | 39 | Cache.prototype.add = function(request) { 40 | // Fetch a response for the given request, then put the pair into the cache. 41 | var cache = this; 42 | return fetch(request).then(function(response) { 43 | cache.put(request, response); 44 | }); 45 | }; 46 | 47 | Cache.prototype.addAll = function(requests) { 48 | // Create a list of `add` promises, one for each request. 49 | var promiseList = []; 50 | for (var i=0; i= 10.53. 29 | isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() == 1 && 30 | // Safari < 2.0.2 stores the internal millisecond time value correctly, 31 | // but clips the values returned by the date methods to the range of 32 | // signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]). 33 | isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708; 34 | } catch (exception) {} 35 | 36 | // IE <= 7 doesn't support accessing string characters using square 37 | // bracket notation. IE 8 only supports this for primitives. 38 | var charIndexBuggy = "A"[0] != "A"; 39 | 40 | // Define additional utility methods if the `Date` methods are buggy. 41 | if (!isExtended) { 42 | var floor = Math.floor; 43 | // A mapping between the months of the year and the number of days between 44 | // January 1st and the first of the respective month. 45 | var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 46 | // Internal: Calculates the number of days between the Unix epoch and the 47 | // first day of the given month. 48 | var getDay = function (year, month) { 49 | return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400); 50 | }; 51 | } 52 | 53 | // Internal: Determines if a property is a direct property of the given 54 | // object. Delegates to the native `Object#hasOwnProperty` method. 55 | if (!(isProperty = {}.hasOwnProperty)) { 56 | isProperty = function (property) { 57 | var members = {}, constructor; 58 | if ((members.__proto__ = null, members.__proto__ = { 59 | // The *proto* property cannot be set multiple times in recent 60 | // versions of Firefox and SeaMonkey. 61 | "toString": 1 62 | }, members).toString != getClass) { 63 | // Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but 64 | // supports the mutable *proto* property. 65 | isProperty = function (property) { 66 | // Capture and break the object's prototype chain (see section 8.6.2 67 | // of the ES 5.1 spec). The parenthesized expression prevents an 68 | // unsafe transformation by the Closure Compiler. 69 | var original = this.__proto__, result = property in (this.__proto__ = null, this); 70 | // Restore the original prototype chain. 71 | this.__proto__ = original; 72 | return result; 73 | }; 74 | } else { 75 | // Capture a reference to the top-level `Object` constructor. 76 | constructor = members.constructor; 77 | // Use the `constructor` property to simulate `Object#hasOwnProperty` in 78 | // other environments. 79 | isProperty = function (property) { 80 | var parent = (this.constructor || constructor).prototype; 81 | return property in this && !(property in parent && this[property] === parent[property]); 82 | }; 83 | } 84 | members = null; 85 | return isProperty.call(this, property); 86 | }; 87 | } 88 | 89 | // Internal: Normalizes the `for...in` iteration algorithm across 90 | // environments. Each enumerated key is yielded to a `callback` function. 91 | forEach = function (object, callback) { 92 | var size = 0, Properties, members, property, forEach; 93 | 94 | // Tests for bugs in the current environment's `for...in` algorithm. The 95 | // `valueOf` property inherits the non-enumerable flag from 96 | // `Object.prototype` in older versions of IE, Netscape, and Mozilla. 97 | (Properties = function () { 98 | this.valueOf = 0; 99 | }).prototype.valueOf = 0; 100 | 101 | // Iterate over a new instance of the `Properties` class. 102 | members = new Properties(); 103 | for (property in members) { 104 | // Ignore all properties inherited from `Object.prototype`. 105 | if (isProperty.call(members, property)) { 106 | size++; 107 | } 108 | } 109 | Properties = members = null; 110 | 111 | // Normalize the iteration algorithm. 112 | if (!size) { 113 | // A list of non-enumerable properties inherited from `Object.prototype`. 114 | members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"]; 115 | // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable 116 | // properties. 117 | forEach = function (object, callback) { 118 | var isFunction = getClass.call(object) == "[object Function]", property, length; 119 | for (property in object) { 120 | // Gecko <= 1.0 enumerates the `prototype` property of functions under 121 | // certain conditions; IE does not. 122 | if (!(isFunction && property == "prototype") && isProperty.call(object, property)) { 123 | callback(property); 124 | } 125 | } 126 | // Manually invoke the callback for each non-enumerable property. 127 | for (length = members.length; property = members[--length]; isProperty.call(object, property) && callback(property)); 128 | }; 129 | } else if (size == 2) { 130 | // Safari <= 2.0.4 enumerates shadowed properties twice. 131 | forEach = function (object, callback) { 132 | // Create a set of iterated properties. 133 | var members = {}, isFunction = getClass.call(object) == "[object Function]", property; 134 | for (property in object) { 135 | // Store each property name to prevent double enumeration. The 136 | // `prototype` property of functions is not enumerated due to cross- 137 | // environment inconsistencies. 138 | if (!(isFunction && property == "prototype") && !isProperty.call(members, property) && (members[property] = 1) && isProperty.call(object, property)) { 139 | callback(property); 140 | } 141 | } 142 | }; 143 | } else { 144 | // No bugs detected; use the standard `for...in` algorithm. 145 | forEach = function (object, callback) { 146 | var isFunction = getClass.call(object) == "[object Function]", property, isConstructor; 147 | for (property in object) { 148 | if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) { 149 | callback(property); 150 | } 151 | } 152 | // Manually invoke the callback for the `constructor` property due to 153 | // cross-environment inconsistencies. 154 | if (isConstructor || isProperty.call(object, (property = "constructor"))) { 155 | callback(property); 156 | } 157 | }; 158 | } 159 | return forEach(object, callback); 160 | }; 161 | 162 | // Public: Serializes a JavaScript `value` as a string. The optional 163 | // `filter` argument may specify either a function that alters how object and 164 | // array members are serialized, or an array of strings and numbers that 165 | // indicates which properties should be serialized. The optional `width` 166 | // argument may be either a string or number that specifies the indentation 167 | // level of the output. 168 | 169 | // Internal: A map of control characters and their escaped equivalents. 170 | var Escapes = { 171 | "\\": "\\\\", 172 | '"': '\\"', 173 | "\b": "\\b", 174 | "\f": "\\f", 175 | "\n": "\\n", 176 | "\r": "\\r", 177 | "\t": "\\t" 178 | }; 179 | 180 | // Internal: Converts `value` into a zero-padded string such that its 181 | // length is at least equal to `width`. The `width` must be <= 6. 182 | var toPaddedString = function (width, value) { 183 | // The `|| 0` expression is necessary to work around a bug in 184 | // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`. 185 | return ("000000" + (value || 0)).slice(-width); 186 | }; 187 | 188 | // Internal: Double-quotes a string `value`, replacing all ASCII control 189 | // characters (characters with code unit values between 0 and 31) with 190 | // their escaped equivalents. This is an implementation of the 191 | // `Quote(value)` operation defined in ES 5.1 section 15.12.3. 192 | var quote = function (value) { 193 | var result = '"', index = 0, symbol; 194 | for (; symbol = value.charAt(index); index++) { 195 | // Escape the reverse solidus, double quote, backspace, form feed, line 196 | // feed, carriage return, and tab characters. 197 | result += '\\"\b\f\n\r\t'.indexOf(symbol) > -1 ? Escapes[symbol] : 198 | // If the character is a control character, append its Unicode escape 199 | // sequence; otherwise, append the character as-is. 200 | (Escapes[symbol] = symbol < " " ? "\\u00" + toPaddedString(2, symbol.charCodeAt(0).toString(16)) : symbol); 201 | } 202 | return result + '"'; 203 | }; 204 | 205 | // Internal: detects if an object is a DOM element. 206 | // http://stackoverflow.com/questions/384286/javascript-isdom-how-do-you-check-if-a-javascript-object-is-a-dom-object 207 | var isElement = function(o) { 208 | return ( 209 | typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 210 | o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string" 211 | ); 212 | }; 213 | 214 | // Internal: Recursively serializes an object. Implements the 215 | // `Str(key, holder)`, `JO(value)`, and `JA(value)` operations. 216 | var serialize = function (property, object, callback, properties, whitespace, indentation, stack) { 217 | var value = object[property], originalClassName, className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, any, result, 218 | regExpSource, regExpModifiers = ""; 219 | if( value instanceof Error || value instanceof Function) { 220 | throw new KaminoException(); 221 | } 222 | if( isElement( value ) ) { 223 | throw new KaminoException(); 224 | } 225 | if (typeof value == "object" && value) { 226 | originalClassName = getClass.call(value); 227 | if (originalClassName == "[object Date]" && !isProperty.call(value, "toJSON")) { 228 | if (value > -1 / 0 && value < 1 / 0) { 229 | value = value.toUTCString().replace("GMT", "UTC"); 230 | } else { 231 | value = null; 232 | } 233 | } else if (typeof value.toJSON == "function" && ((originalClassName != "[object Number]" && originalClassName != "[object String]" && originalClassName != "[object Array]") || isProperty.call(value, "toJSON"))) { 234 | // Prototype <= 1.6.1 adds non-standard `toJSON` methods to the 235 | // `Number`, `String`, `Date`, and `Array` prototypes. JSON 3 236 | // ignores all `toJSON` methods on these objects unless they are 237 | // defined directly on an instance. 238 | value = value.toJSON(property); 239 | } 240 | } 241 | if (callback) { 242 | // If a replacement function was provided, call it to obtain the value 243 | // for serialization. 244 | value = callback.call(object, property, value); 245 | } 246 | if (value === null) { 247 | return "null"; 248 | } 249 | if (value === undefined) { 250 | return undefined; 251 | } 252 | className = getClass.call(value); 253 | if (className == "[object Boolean]") { 254 | // Booleans are represented literally. 255 | return "" + value; 256 | } else if (className == "[object Number]") { 257 | // Kamino numbers must be finite. `Infinity` and `NaN` are serialized as 258 | // `"null"`. 259 | if( value === Number.POSITIVE_INFINITY ) { 260 | return "Infinity"; 261 | } else if( value === Number.NEGATIVE_INFINITY ) { 262 | return "NInfinity"; 263 | } else if( isNaN( value ) ) { 264 | return "NaN"; 265 | } 266 | return "" + value; 267 | } else if (className == "[object RegExp]") { 268 | // Strings are double-quoted and escaped. 269 | regExpSource = value.source; 270 | regExpModifiers += value.ignoreCase ? "i" : ""; 271 | regExpModifiers += value.global ? "g" : ""; 272 | regExpModifiers += value.multiline ? "m" : ""; 273 | 274 | regExpSource = quote(charIndexBuggy ? regExpSource.split("") : regExpSource); 275 | regExpModifiers = quote(charIndexBuggy ? regExpModifiers.split("") : regExpModifiers); 276 | 277 | // Adds the RegExp prefix. 278 | value = '^' + regExpSource + regExpModifiers; 279 | 280 | return value; 281 | } else if (className == "[object String]") { 282 | // Strings are double-quoted and escaped. 283 | value = quote(charIndexBuggy ? value.split("") : value); 284 | 285 | if( originalClassName == "[object Date]") { 286 | // Adds the Date prefix. 287 | value = '%' + value; 288 | } 289 | 290 | return value; 291 | } 292 | // Recursively serialize objects and arrays. 293 | if (typeof value == "object") { 294 | // Check for cyclic structures. This is a linear search; performance 295 | // is inversely proportional to the number of unique nested objects. 296 | for (length = stack.length; length--;) { 297 | if (stack[length] === value) { 298 | return "&" + length; 299 | } 300 | } 301 | // Add the object to the stack of traversed objects. 302 | stack.push(value); 303 | results = []; 304 | // Save the current indentation level and indent one additional level. 305 | prefix = indentation; 306 | indentation += whitespace; 307 | if (className == "[object Array]") { 308 | // Recursively serialize array elements. 309 | for (index = 0, length = value.length; index < length; any || (any = true), index++) { 310 | element = serialize(index, value, callback, properties, whitespace, indentation, stack); 311 | results.push(element === undef ? "null" : element); 312 | } 313 | result = any ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]"; 314 | } else { 315 | // Recursively serialize object members. Members are selected from 316 | // either a user-specified list of property names, or the object 317 | // itself. 318 | forEach(properties || value, function (property) { 319 | var element = serialize(property, value, callback, properties, whitespace, indentation, stack); 320 | if (element !== undef) { 321 | // According to ES 5.1 section 15.12.3: "If `gap` {whitespace} 322 | // is not the empty string, let `member` {quote(property) + ":"} 323 | // be the concatenation of `member` and the `space` character." 324 | // The "`space` character" refers to the literal space 325 | // character, not the `space` {width} argument provided to 326 | // `JSON.stringify`. 327 | results.push(quote(charIndexBuggy ? property.split("") : property) + ":" + (whitespace ? " " : "") + element); 328 | } 329 | any || (any = true); 330 | }); 331 | result = any ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}"; 332 | } 333 | return result; 334 | } 335 | }; 336 | 337 | // Public: `Kamino.stringify`. See ES 5.1 section 15.12.3. 338 | Kamino.stringify = function (source, filter, width) { 339 | var whitespace, callback, properties; 340 | if (typeof filter == "function" || typeof filter == "object" && filter) { 341 | if (getClass.call(filter) == "[object Function]") { 342 | callback = filter; 343 | } else if (getClass.call(filter) == "[object Array]") { 344 | // Convert the property names array into a makeshift set. 345 | properties = {}; 346 | for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((getClass.call(value) == "[object String]" || getClass.call(value) == "[object Number]") && (properties[value] = 1))); 347 | } 348 | } 349 | if (width) { 350 | if (getClass.call(width) == "[object Number]") { 351 | // Convert the `width` to an integer and create a string containing 352 | // `width` number of space characters. 353 | if ((width -= width % 1) > 0) { 354 | for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " "); 355 | } 356 | } else if (getClass.call(width) == "[object String]") { 357 | whitespace = width.length <= 10 ? width : width.slice(0, 10); 358 | } 359 | } 360 | // Opera <= 7.54u2 discards the values associated with empty string keys 361 | // (`""`) only if they are used directly within an object member list 362 | // (e.g., `!("" in { "": 1})`). 363 | return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []); 364 | }; 365 | 366 | // Public: Parses a source string. 367 | var fromCharCode = String.fromCharCode; 368 | 369 | // Internal: A map of escaped control characters and their unescaped 370 | // equivalents. 371 | var Unescapes = { 372 | "\\": "\\", 373 | '"': '"', 374 | "/": "/", 375 | "b": "\b", 376 | "t": "\t", 377 | "n": "\n", 378 | "f": "\f", 379 | "r": "\r" 380 | }; 381 | 382 | // Internal: Stores the parser state. 383 | var Index, Source, stack; 384 | 385 | // Internal: Resets the parser state and throws a `SyntaxError`. 386 | var abort = function() { 387 | Index = Source = null; 388 | throw SyntaxError(); 389 | }; 390 | 391 | var parseString = function(prefix) { 392 | prefix = prefix || ""; 393 | var source = Source, length = source.length, value, symbol, begin, position; 394 | // Advance to the next character and parse a Kamino string at the 395 | // current position. String tokens are prefixed with the sentinel 396 | // `@` character to distinguish them from punctuators. 397 | for (value = prefix, Index++; Index < length;) { 398 | symbol = source[Index]; 399 | if (symbol < " ") { 400 | // Unescaped ASCII control characters are not permitted. 401 | abort(); 402 | } else if (symbol == "\\") { 403 | // Parse escaped Kamino control characters, `"`, `\`, `/`, and 404 | // Unicode escape sequences. 405 | symbol = source[++Index]; 406 | if ('\\"/btnfr'.indexOf(symbol) > -1) { 407 | // Revive escaped control characters. 408 | value += Unescapes[symbol]; 409 | Index++; 410 | } else if (symbol == "u") { 411 | // Advance to the first character of the escape sequence. 412 | begin = ++Index; 413 | // Validate the Unicode escape sequence. 414 | for (position = Index + 4; Index < position; Index++) { 415 | symbol = source[Index]; 416 | // A valid sequence comprises four hexdigits that form a 417 | // single hexadecimal value. 418 | if (!(symbol >= "0" && symbol <= "9" || symbol >= "a" && symbol <= "f" || symbol >= "A" && symbol <= "F")) { 419 | // Invalid Unicode escape sequence. 420 | abort(); 421 | } 422 | } 423 | // Revive the escaped character. 424 | value += fromCharCode("0x" + source.slice(begin, Index)); 425 | } else { 426 | // Invalid escape sequence. 427 | abort(); 428 | } 429 | } else { 430 | if (symbol == '"') { 431 | // An unescaped double-quote character marks the end of the 432 | // string. 433 | break; 434 | } 435 | // Append the original character as-is. 436 | value += symbol; 437 | Index++; 438 | } 439 | } 440 | if (source[Index] == '"') { 441 | Index++; 442 | // Return the revived string. 443 | return value; 444 | } 445 | // Unterminated string. 446 | abort(); 447 | }; 448 | 449 | // Internal: Returns the next token, or `"$"` if the parser has reached 450 | // the end of the source string. A token may be a string, number, `null` 451 | // literal, `NaN` literal or Boolean literal. 452 | var lex = function () { 453 | var source = Source, length = source.length, symbol, value, begin, position, sign, 454 | dateString, regExpSource, regExpModifiers; 455 | while (Index < length) { 456 | symbol = source[Index]; 457 | if ("\t\r\n ".indexOf(symbol) > -1) { 458 | // Skip whitespace tokens, including tabs, carriage returns, line 459 | // feeds, and space characters. 460 | Index++; 461 | } else if ("{}[]:,".indexOf(symbol) > -1) { 462 | // Parse a punctuator token at the current position. 463 | Index++; 464 | return symbol; 465 | } else if (symbol == '"') { 466 | // Parse strings. 467 | return parseString("@"); 468 | } else if (symbol == '%') { 469 | // Parse dates. 470 | Index++; 471 | symbol = source[Index]; 472 | if(symbol == '"') { 473 | dateString = parseString(); 474 | return new Date( dateString ); 475 | } 476 | abort(); 477 | } else if (symbol == '^') { 478 | // Parse regular expressions. 479 | Index++; 480 | symbol = source[Index]; 481 | if(symbol == '"') { 482 | regExpSource = parseString(); 483 | 484 | symbol = source[Index]; 485 | if(symbol == '"') { 486 | regExpModifiers = parseString(); 487 | 488 | return new RegExp( regExpSource, regExpModifiers ); 489 | } 490 | } 491 | abort(); 492 | } else if (symbol == '&') { 493 | // Parse object references. 494 | Index++; 495 | symbol = source[Index]; 496 | if (symbol >= "0" && symbol <= "9") { 497 | Index++; 498 | return stack[symbol]; 499 | } 500 | abort(); 501 | } else { 502 | // Parse numbers and literals. 503 | begin = Index; 504 | // Advance the scanner's position past the sign, if one is 505 | // specified. 506 | if (symbol == "-") { 507 | sign = true; 508 | symbol = source[++Index]; 509 | } 510 | // Parse an integer or floating-point value. 511 | if (symbol >= "0" && symbol <= "9") { 512 | // Leading zeroes are interpreted as octal literals. 513 | if (symbol == "0" && (symbol = source[Index + 1], symbol >= "0" && symbol <= "9")) { 514 | // Illegal octal literal. 515 | abort(); 516 | } 517 | sign = false; 518 | // Parse the integer component. 519 | for (; Index < length && (symbol = source[Index], symbol >= "0" && symbol <= "9"); Index++); 520 | // Floats cannot contain a leading decimal point; however, this 521 | // case is already accounted for by the parser. 522 | if (source[Index] == ".") { 523 | position = ++Index; 524 | // Parse the decimal component. 525 | for (; position < length && (symbol = source[position], symbol >= "0" && symbol <= "9"); position++); 526 | if (position == Index) { 527 | // Illegal trailing decimal. 528 | abort(); 529 | } 530 | Index = position; 531 | } 532 | // Parse exponents. 533 | symbol = source[Index]; 534 | if (symbol == "e" || symbol == "E") { 535 | // Skip past the sign following the exponent, if one is 536 | // specified. 537 | symbol = source[++Index]; 538 | if (symbol == "+" || symbol == "-") { 539 | Index++; 540 | } 541 | // Parse the exponential component. 542 | for (position = Index; position < length && (symbol = source[position], symbol >= "0" && symbol <= "9"); position++); 543 | if (position == Index) { 544 | // Illegal empty exponent. 545 | abort(); 546 | } 547 | Index = position; 548 | } 549 | // Coerce the parsed value to a JavaScript number. 550 | return +source.slice(begin, Index); 551 | } 552 | // A negative sign may only precede numbers. 553 | if (sign) { 554 | abort(); 555 | } 556 | // `true`, `false`, `Infinity`, `-Infinity`, `NaN` and `null` literals. 557 | if (source.slice(Index, Index + 4) == "true") { 558 | Index += 4; 559 | return true; 560 | } else if (source.slice(Index, Index + 5) == "false") { 561 | Index += 5; 562 | return false; 563 | } else if (source.slice(Index, Index + 8) == "Infinity") { 564 | Index += 8; 565 | return Infinity; 566 | } else if (source.slice(Index, Index + 9) == "NInfinity") { 567 | Index += 9; 568 | return -Infinity; 569 | } else if (source.slice(Index, Index + 3) == "NaN") { 570 | Index += 3; 571 | return NaN; 572 | } else if (source.slice(Index, Index + 4) == "null") { 573 | Index += 4; 574 | return null; 575 | } 576 | // Unrecognized token. 577 | abort(); 578 | } 579 | } 580 | // Return the sentinel `$` character if the parser has reached the end 581 | // of the source string. 582 | return "$"; 583 | }; 584 | 585 | // Internal: Parses a Kamino `value` token. 586 | var get = function (value) { 587 | var results, any, key; 588 | if (value == "$") { 589 | // Unexpected end of input. 590 | abort(); 591 | } 592 | if (typeof value == "string") { 593 | if (value[0] == "@") { 594 | // Remove the sentinel `@` character. 595 | return value.slice(1); 596 | } 597 | // Parse object and array literals. 598 | if (value == "[") { 599 | // Parses a Kamino array, returning a new JavaScript array. 600 | results = []; 601 | stack[stack.length] = results; 602 | for (;; any || (any = true)) { 603 | value = lex(); 604 | // A closing square bracket marks the end of the array literal. 605 | if (value == "]") { 606 | break; 607 | } 608 | // If the array literal contains elements, the current token 609 | // should be a comma separating the previous element from the 610 | // next. 611 | if (any) { 612 | if (value == ",") { 613 | value = lex(); 614 | if (value == "]") { 615 | // Unexpected trailing `,` in array literal. 616 | abort(); 617 | } 618 | } else { 619 | // A `,` must separate each array element. 620 | abort(); 621 | } 622 | } 623 | // Elisions and leading commas are not permitted. 624 | if (value == ",") { 625 | abort(); 626 | } 627 | results.push(get(typeof value == "string" && charIndexBuggy ? value.split("") : value)); 628 | } 629 | return results; 630 | } else if (value == "{") { 631 | // Parses a Kamino object, returning a new JavaScript object. 632 | results = {}; 633 | stack[stack.length] = results; 634 | for (;; any || (any = true)) { 635 | value = lex(); 636 | // A closing curly brace marks the end of the object literal. 637 | if (value == "}") { 638 | break; 639 | } 640 | // If the object literal contains members, the current token 641 | // should be a comma separator. 642 | if (any) { 643 | if (value == ",") { 644 | value = lex(); 645 | if (value == "}") { 646 | // Unexpected trailing `,` in object literal. 647 | abort(); 648 | } 649 | } else { 650 | // A `,` must separate each object member. 651 | abort(); 652 | } 653 | } 654 | // Leading commas are not permitted, object property names must be 655 | // double-quoted strings, and a `:` must separate each property 656 | // name and value. 657 | if (value == "," || typeof value != "string" || value[0] != "@" || lex() != ":") { 658 | abort(); 659 | } 660 | var result = lex(); 661 | results[value.slice(1)] = get(typeof result == "string" && charIndexBuggy ? result.split("") : result); 662 | } 663 | return results; 664 | } 665 | // Unexpected token encountered. 666 | abort(); 667 | } 668 | return value; 669 | }; 670 | 671 | // Internal: Updates a traversed object member. 672 | var update = function(source, property, callback) { 673 | var element = walk(source, property, callback); 674 | if (element === undef) { 675 | delete source[property]; 676 | } else { 677 | source[property] = element; 678 | } 679 | }; 680 | 681 | // Internal: Recursively traverses a parsed Kamino object, invoking the 682 | // `callback` function for each value. This is an implementation of the 683 | // `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2. 684 | var walk = function (source, property, callback) { 685 | var value = source[property], length; 686 | if (typeof value == "object" && value) { 687 | if (getClass.call(value) == "[object Array]") { 688 | for (length = value.length; length--;) { 689 | update(value, length, callback); 690 | } 691 | } else { 692 | // `forEach` can't be used to traverse an array in Opera <= 8.54, 693 | // as `Object#hasOwnProperty` returns `false` for array indices 694 | // (e.g., `![1, 2, 3].hasOwnProperty("0")`). 695 | forEach(value, function (property) { 696 | update(value, property, callback); 697 | }); 698 | } 699 | } 700 | return callback.call(source, property, value); 701 | }; 702 | 703 | // Public: `Kamino.parse`. See ES 5.1 section 15.12.2. 704 | Kamino.parse = function (source, callback) { 705 | var result, value; 706 | Index = 0; 707 | Source = "" + source; 708 | stack = []; 709 | if (charIndexBuggy) { 710 | Source = source.split(""); 711 | } 712 | result = get(lex()); 713 | // If a Kamino string contains multiple tokens, it is invalid. 714 | if (lex() != "$") { 715 | abort(); 716 | } 717 | // Reset the parser state. 718 | Index = Source = null; 719 | return callback && getClass.call(callback) == "[object Function]" ? walk((value = {}, value[""] = result, value), "", callback) : result; 720 | }; 721 | 722 | Kamino.clone = function(source) { 723 | return Kamino.parse( Kamino.stringify(source) ); 724 | }; 725 | })(this); 726 | -------------------------------------------------------------------------------- /www/sw_assets/message.js: -------------------------------------------------------------------------------- 1 | MessageEvent = function(eventInitDict) { 2 | Event.call(this, 'message'); 3 | if (eventInitDict) { 4 | if (eventInitDict.data) { 5 | Object.defineProperty(this, 'data', {value: eventInitDict.data}); 6 | } 7 | if (eventInitDict.origin) { 8 | Object.defineProperty(this, 'origin', {value: eventInitDict.origin}); 9 | } 10 | if (eventInitDict.source) { 11 | Object.defineProperty(this, 'source', {value: eventInitDict.source}); 12 | } 13 | } 14 | }; 15 | MessageEvent.prototype = Object.create(Event.prototype); 16 | MessageEvent.constructor = MessageEvent; 17 | 18 | --------------------------------------------------------------------------------