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