@webqit/webflo 0.11.61 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/{Context.js → AbstractContext.js} +1 -9
- package/src/deployment-pi/origins/index.js +1 -1
- package/src/index.js +1 -9
- package/src/runtime-pi/HttpEvent.js +101 -81
- package/src/runtime-pi/HttpUser.js +126 -0
- package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
- package/src/runtime-pi/MessagingOverChannel.js +85 -0
- package/src/runtime-pi/MessagingOverSocket.js +106 -0
- package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
- package/src/runtime-pi/WebfloCookieStorage.js +27 -0
- package/src/runtime-pi/WebfloEventTarget.js +39 -0
- package/src/runtime-pi/WebfloMessageEvent.js +58 -0
- package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
- package/src/runtime-pi/{Router.js → WebfloRouter.js} +3 -34
- package/src/runtime-pi/WebfloRuntime.js +52 -0
- package/src/runtime-pi/WebfloStorage.js +109 -0
- package/src/runtime-pi/client/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/Context.js +2 -6
- package/src/runtime-pi/client/CookieStorage.js +17 -0
- package/src/runtime-pi/client/Router.js +3 -13
- package/src/runtime-pi/client/SessionStorage.js +33 -0
- package/src/runtime-pi/client/Url.js +24 -72
- package/src/runtime-pi/client/WebfloClient.js +544 -0
- package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
- package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
- package/src/runtime-pi/client/WebfloSubClient.js +165 -0
- package/src/runtime-pi/client/Workport.js +89 -161
- package/src/runtime-pi/client/generate.js +1 -1
- package/src/runtime-pi/client/index.js +13 -18
- package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/worker/Context.js +2 -6
- package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
- package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
- package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
- package/src/runtime-pi/client/worker/Workport.js +13 -73
- package/src/runtime-pi/client/worker/index.js +7 -18
- package/src/runtime-pi/index.js +1 -8
- package/src/runtime-pi/server/ClientMessaging.js +18 -0
- package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
- package/src/runtime-pi/server/Context.js +2 -6
- package/src/runtime-pi/server/CookieStorage.js +17 -0
- package/src/runtime-pi/server/Router.js +2 -68
- package/src/runtime-pi/server/SessionStorage.js +53 -0
- package/src/runtime-pi/server/WebfloServer.js +755 -0
- package/src/runtime-pi/server/index.js +7 -18
- package/src/runtime-pi/util-http.js +268 -32
- package/src/runtime-pi/xURL.js +25 -22
- package/src/runtime-pi/xfetch.js +2 -2
- package/src/runtime-pi/Application.js +0 -29
- package/src/runtime-pi/Cookies.js +0 -82
- package/src/runtime-pi/Runtime.js +0 -21
- package/src/runtime-pi/client/Application.js +0 -76
- package/src/runtime-pi/client/Runtime.js +0 -525
- package/src/runtime-pi/client/createStorage.js +0 -58
- package/src/runtime-pi/client/worker/Application.js +0 -44
- package/src/runtime-pi/client/worker/Runtime.js +0 -275
- package/src/runtime-pi/server/Application.js +0 -101
- package/src/runtime-pi/server/Runtime.js +0 -558
- package/src/runtime-pi/xFormData.js +0 -24
- package/src/runtime-pi/xHeaders.js +0 -146
- package/src/runtime-pi/xRequest.js +0 -46
- package/src/runtime-pi/xRequestHeaders.js +0 -109
- package/src/runtime-pi/xResponse.js +0 -33
- package/src/runtime-pi/xResponseHeaders.js +0 -117
- package/src/runtime-pi/xxHttpMessage.js +0 -102
|
@@ -1,176 +1,104 @@
|
|
|
1
|
+
export class Workport {
|
|
1
2
|
|
|
3
|
+
#swFile;
|
|
4
|
+
#swParams = {};
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { _isFunction, _isObject } from '@webqit/util/js/index.js';
|
|
7
|
-
import { Observer } from './Runtime.js';
|
|
6
|
+
#swReady;
|
|
7
|
+
get swReady() { return this.#swReady; }
|
|
8
|
+
#swRegistration;
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// instance2.state can be any of: "installing", "installed", "activating", "activated", "redundant"
|
|
25
|
-
const equivState = target.state === 'installed' ? 'waiting' :
|
|
26
|
-
(target.state === 'activating' || target.state === 'activated' ? 'active' : target.state)
|
|
27
|
-
Observer.set(this, equivState, target);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// We're always installing at first for a new service worker.
|
|
31
|
-
// An existing service would immediately be active
|
|
32
|
-
const worker = registration.active || registration.waiting || registration.installing;
|
|
33
|
-
state(worker);
|
|
34
|
-
worker.addEventListener('statechange', e => state(e.target));
|
|
35
|
-
|
|
36
|
-
// "updatefound" event - a new worker that will control
|
|
37
|
-
// this page is installing somewhere
|
|
38
|
-
registration.addEventListener('updatefound', () => {
|
|
39
|
-
// If updatefound is fired, it means that there's
|
|
40
|
-
// a new service worker being installed.
|
|
41
|
-
state(registration.installing);
|
|
42
|
-
registration.installing.addEventListener('statechange', e => state(e.target));
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
resolve(registration);
|
|
46
|
-
}).catch(e => reject(e));
|
|
47
|
-
};
|
|
48
|
-
if (params.onWindowLoad) {
|
|
49
|
-
window.addEventListener('load', register);
|
|
50
|
-
} else {
|
|
51
|
-
register();
|
|
10
|
+
async registerServiceWorker(file, params = {}) {
|
|
11
|
+
if (this.#swRegistration) {
|
|
12
|
+
throw new Error('Service worker already registered');
|
|
13
|
+
}
|
|
14
|
+
this.#swFile = file;
|
|
15
|
+
this.#swParams = params;
|
|
16
|
+
this.#swReady = navigator.serviceWorker ? navigator.serviceWorker.ready : new Promise(() => {});
|
|
17
|
+
this.#swRegistration = await navigator.serviceWorker.register(this.#swFile, { scope: this.#swParams.scope || '/' });
|
|
18
|
+
// Helper that updates instance's state
|
|
19
|
+
const stateChange = (target) => {
|
|
20
|
+
// target.state can be any of: "parsed", "installing", "installed", "activating", "activated", "redundant"
|
|
21
|
+
if (target.state === 'redundant') {
|
|
22
|
+
this.remove(target);
|
|
23
|
+
} else if (target.state === 'activated') {
|
|
24
|
+
this.add(target);
|
|
52
25
|
}
|
|
53
|
-
|
|
54
|
-
|
|
26
|
+
}
|
|
27
|
+
// We're always installing at first for a new service worker.
|
|
28
|
+
// An existing service would immediately be active
|
|
29
|
+
const worker = this.#swRegistration.active || this.#swRegistration.waiting || this.#swRegistration.installing;
|
|
30
|
+
if (worker) {
|
|
31
|
+
stateChange(worker);
|
|
32
|
+
worker.addEventListener('statechange', (e) => stateChange(e.target));
|
|
33
|
+
// "updatefound" event - a new worker that will control
|
|
34
|
+
// this page is installing somewhere
|
|
35
|
+
this.#swRegistration.addEventListener('updatefound', () => {
|
|
36
|
+
// If updatefound is fired, it means that there's
|
|
37
|
+
// a new service worker being installed.
|
|
38
|
+
stateChange(this.#swRegistration.installing);
|
|
39
|
+
this.#swRegistration.installing.addEventListener('statechange', (e) => stateChange(e.target));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async requestNotificationPermission() {
|
|
45
|
+
return await new Promise(async (resolve, reject) => {
|
|
46
|
+
const permissionResult = Notification.requestPermission(resolve);
|
|
47
|
+
if (permissionResult) {
|
|
48
|
+
permissionResult.then(resolve, reject);
|
|
55
49
|
}
|
|
56
50
|
});
|
|
51
|
+
}
|
|
57
52
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
async showNotification(title, params = {}) {
|
|
54
|
+
await this.#swReady;
|
|
55
|
+
if (this.#swRegistration) {
|
|
56
|
+
return (await this.#swRegistration).showNotification(title, params);
|
|
57
|
+
}
|
|
58
|
+
return new Notification(title, params);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async pushSubscription(autoPrompt = true) {
|
|
62
|
+
if (!this.#swRegistration) {
|
|
63
|
+
throw new Error(`Service worker not registered`);
|
|
64
|
+
}
|
|
65
|
+
await this.#swReady;
|
|
66
|
+
const pushManager = (await this.#swRegistration).pushManager;
|
|
67
|
+
let subscription = await pushManager.getSubscription();
|
|
68
|
+
if (!subscription && autoPrompt && this.#swParams.VAPID_PUBLIC_KEY) {
|
|
69
|
+
subscription = await pushManager.subscribe({
|
|
70
|
+
userVisibleOnly: true,
|
|
71
|
+
applicationServerKey: urlBase64ToUint8Array(this.#swParams.VAPID_PUBLIC_KEY),
|
|
72
|
+
});
|
|
73
|
+
if (this.#swParams.PUSH_REGISTRATION_PUBLIC_URL) {
|
|
74
|
+
await fetch(this.#swParams.PUSH_REGISTRATION_PUBLIC_URL, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json', },
|
|
77
|
+
body: JSON.stringify(subscription)
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
postSendCallback(message, (active, message) => {
|
|
77
|
-
active.postMessage(message);
|
|
78
|
-
}, onAvailability);
|
|
79
|
-
return this;
|
|
80
|
-
},
|
|
81
|
-
listen: callback => {
|
|
82
|
-
if (navigator.serviceWorker) {
|
|
83
|
-
navigator.serviceWorker.addEventListener('message', evt => {
|
|
84
|
-
const response = callback(evt);
|
|
85
|
-
let responsePort = evt.ports[0];
|
|
86
|
-
if (responsePort) {
|
|
87
|
-
if (response instanceof Promise) {
|
|
88
|
-
response.then(data => {
|
|
89
|
-
responsePort.postMessage(data);
|
|
90
|
-
});
|
|
91
|
-
} else {
|
|
92
|
-
responsePort.postMessage(response);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return this;
|
|
98
|
-
},
|
|
99
|
-
request: (message, onAvailability = 1) => {
|
|
100
|
-
return new Promise(res => {
|
|
101
|
-
postSendCallback(message, (active, message) => {
|
|
102
|
-
let messageChannel = new MessageChannel();
|
|
103
|
-
active.postMessage(message, [ messageChannel.port2 ]);
|
|
104
|
-
messageChannel.port1.onmessage = e => res(e.data);
|
|
105
|
-
}, onAvailability);
|
|
106
|
-
});
|
|
107
|
-
},
|
|
108
|
-
channel(channelId) {
|
|
109
|
-
if (!this.channels.has(channelId)) { this.channels.set(channelId, new BroadcastChannel(channel)); }
|
|
110
|
-
let channel = this.channels.get(channelId);
|
|
111
|
-
return {
|
|
112
|
-
broadcast: message => channel.postMessage(message),
|
|
113
|
-
listen: callback => channel.addEventListener('message', callback),
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
channels: new Map,
|
|
117
|
-
};
|
|
80
|
+
}
|
|
81
|
+
return subscription;
|
|
82
|
+
}
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
84
|
+
async pushUnsubscribe() {
|
|
85
|
+
if (!this.#swRegistration) {
|
|
86
|
+
throw new Error(`Service worker not registered`);
|
|
87
|
+
}
|
|
88
|
+
await this.#swReady;
|
|
89
|
+
const pushManager = (await this.#swRegistration).pushManager;
|
|
90
|
+
const subscription = await pushManager.getSubscription();
|
|
91
|
+
if (subscription) {
|
|
92
|
+
subscription.unsubscribe();
|
|
93
|
+
if (subscription && this.#swParams.PUSH_REGISTRATION_PUBLIC_URL) {
|
|
94
|
+
await fetch(this.#swParams.PUSH_REGISTRATION_PUBLIC_URL, {
|
|
95
|
+
method: 'DELETE',
|
|
96
|
+
headers: { 'Content-Type': 'application/json', },
|
|
97
|
+
body: JSON.stringify(subscription)
|
|
127
98
|
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
await this.ready;
|
|
131
|
-
return (await this.registration).showNotification(title, params);
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// --------
|
|
136
|
-
// Push notifications
|
|
137
|
-
// --------
|
|
138
|
-
this.push = {
|
|
139
|
-
getSubscription: async (autoPrompt = true) => {
|
|
140
|
-
await this.ready;
|
|
141
|
-
let subscription = await (await this.registration).pushManager.getSubscription();
|
|
142
|
-
let VAPID_PUBLIC_KEY, PUSH_REGISTRATION_PUBLIC_URL;
|
|
143
|
-
if (!subscription && autoPrompt && (VAPID_PUBLIC_KEY = env[params.vapid_key_env])) {
|
|
144
|
-
subscription = await (await this.registration).pushManager.subscribe({
|
|
145
|
-
userVisibleOnly: true,
|
|
146
|
-
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
|
|
147
|
-
});
|
|
148
|
-
if (PUSH_REGISTRATION_PUBLIC_URL = env[params.push_registration_url_env]) {
|
|
149
|
-
await fetch(PUSH_REGISTRATION_PUBLIC_URL, {
|
|
150
|
-
method: 'POST',
|
|
151
|
-
headers: { 'Content-Type': 'application/json', },
|
|
152
|
-
body: JSON.stringify(subscription),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return subscription;
|
|
157
|
-
},
|
|
158
|
-
unsubscribe: async () => {
|
|
159
|
-
await this.ready;
|
|
160
|
-
const subscription = await (await this.registration).pushManager.getSubscription();
|
|
161
|
-
subscription?.unsubscribe();
|
|
162
|
-
let PUSH_REGISTRATION_PUBLIC_URL;
|
|
163
|
-
if (subscription && (PUSH_REGISTRATION_PUBLIC_URL = env[params.push_registration_url_env])) {
|
|
164
|
-
await fetch(PUSH_REGISTRATION_PUBLIC_URL, {
|
|
165
|
-
method: 'DELETE',
|
|
166
|
-
headers: { 'Content-Type': 'application/json', },
|
|
167
|
-
body: JSON.stringify(subscription),
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
172
101
|
}
|
|
173
|
-
|
|
174
102
|
}
|
|
175
103
|
|
|
176
104
|
// Public base64 to Uint
|
|
@@ -46,7 +46,7 @@ export async function generate() {
|
|
|
46
46
|
}
|
|
47
47
|
const envConfig = await (new cx.config.deployment.Env(cx)).read();
|
|
48
48
|
for (const key in envConfig.entries) {
|
|
49
|
-
if (!key.includes('PUBLIC_')) continue;
|
|
49
|
+
if (!key.includes('PUBLIC_') && !key.includes('_PUBLIC')) continue;
|
|
50
50
|
if (clientConfig.bundle_public_env) {
|
|
51
51
|
if (!clientConfig.env) { clientConfig.env = {}; }
|
|
52
52
|
clientConfig.env[key] = envConfig.entries[key];
|
|
@@ -1,21 +1,16 @@
|
|
|
1
|
+
import { WebfloRootClient1 } from './WebfloRootClient1.js';
|
|
2
|
+
import { WebfloRootClient2 } from './WebfloRootClient2.js';
|
|
3
|
+
import { WebfloSubClient } from './WebfloSubClient.js';
|
|
1
4
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import Runtime from './Runtime.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @start
|
|
11
|
-
*/
|
|
12
|
-
export async function start(applicationInstance = null) {
|
|
13
|
-
const cx = this || {};
|
|
14
|
-
const defaultApplicationInstance = _cx => new Application(_cx);
|
|
15
|
-
return new Runtime(Context.create(cx), applicationInstance || defaultApplicationInstance);
|
|
5
|
+
export function start() {
|
|
6
|
+
const Controller = window.navigation ? WebfloRootClient2 : WebfloRootClient1;
|
|
7
|
+
const instance = Controller.create(document, this || {});
|
|
8
|
+
instance.initialize();
|
|
9
|
+
WebfloSubClient.defineElement();
|
|
16
10
|
}
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
export { WebfloSubClient } from './WebfloSubClient.js';
|
|
13
|
+
export {
|
|
14
|
+
WebfloRootClient1,
|
|
15
|
+
WebfloRootClient2
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WebfloCookieStorage } from '../../WebfloCookieStorage.js';
|
|
2
|
+
|
|
3
|
+
export class CookieStorage extends WebfloCookieStorage {
|
|
4
|
+
static create(request) {
|
|
5
|
+
return new this(
|
|
6
|
+
request,
|
|
7
|
+
request.headers.get('Cookie', true).map((c) => [c.name, c])
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
commit(response) {
|
|
12
|
+
for (const cookieStr of this.render()) {
|
|
13
|
+
response.headers.append('Set-Cookie', cookieStr);
|
|
14
|
+
}
|
|
15
|
+
super.commit();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { _any } from '@webqit/util/arr/index.js';
|
|
2
|
+
import { _isObject } from '@webqit/util/js/index.js';
|
|
3
|
+
import { pattern } from '../../util-url.js';
|
|
4
|
+
import { WebfloRuntime } from '../../WebfloRuntime.js';
|
|
5
|
+
import { ClientMessaging } from './ClientMessaging.js';
|
|
6
|
+
import { CookieStorage } from './CookieStorage.js';
|
|
7
|
+
import { SessionStorage } from './SessionStorage.js';
|
|
8
|
+
import { HttpEvent } from '../../HttpEvent.js';
|
|
9
|
+
import { HttpUser } from '../../HttpUser.js';
|
|
10
|
+
import { Workport } from './Workport.js';
|
|
11
|
+
import { Context } from './Context.js';
|
|
12
|
+
import { Router } from '../Router.js';
|
|
13
|
+
import xfetch from '../../xfetch.js';
|
|
14
|
+
import '../../util-http.js';
|
|
15
|
+
|
|
16
|
+
export class WebfloWorker extends WebfloRuntime {
|
|
17
|
+
|
|
18
|
+
static get Context() { return Context; }
|
|
19
|
+
|
|
20
|
+
static get Router() { return Router; }
|
|
21
|
+
|
|
22
|
+
static get HttpEvent() { return HttpEvent; }
|
|
23
|
+
|
|
24
|
+
static get CookieStorage() { return CookieStorage; }
|
|
25
|
+
|
|
26
|
+
static get SessionStorage() { return SessionStorage; }
|
|
27
|
+
|
|
28
|
+
static get HttpUser() { return HttpUser; }
|
|
29
|
+
|
|
30
|
+
static get Workport() { return Workport; }
|
|
31
|
+
|
|
32
|
+
static create(cx) {
|
|
33
|
+
return new this(this.Context.create(cx));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#cx;
|
|
37
|
+
get cx() { return this.#cx; }
|
|
38
|
+
|
|
39
|
+
constructor(cx) {
|
|
40
|
+
super();
|
|
41
|
+
if (!(cx instanceof this.constructor.Context)) {
|
|
42
|
+
throw new Error('Argument #1 must be a Webflo Context instance');
|
|
43
|
+
}
|
|
44
|
+
this.#cx = cx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
initialize() {
|
|
48
|
+
// ONINSTALL
|
|
49
|
+
const installHandler = (event) => {
|
|
50
|
+
if (this.cx.params.skip_waiting) self.skipWaiting();
|
|
51
|
+
// Manage CACHE
|
|
52
|
+
if (this.cx.params.cache_name && (this.cx.params.cache_only_urls || []).length) {
|
|
53
|
+
// Add files to cache
|
|
54
|
+
event.waitUntil(self.caches.open(this.cx.params.cache_name).then(async cache => {
|
|
55
|
+
if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Pre-caching resources.'); }
|
|
56
|
+
for (const urls of [ 'cache_first_urls', 'cache_only_urls' ]) {
|
|
57
|
+
const _urls = (this.cx.params[urls] || []).map(c => c.trim()).filter(c => c && !pattern(c, self.origin).isPattern());
|
|
58
|
+
await cache.addAll(_urls);
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// ONACTIVATE
|
|
64
|
+
const activateHandler = (event) => {
|
|
65
|
+
event.waitUntil(new Promise(async resolve => {
|
|
66
|
+
if (this.cx.params.skip_waiting) { await self.clients.claim(); }
|
|
67
|
+
// Manage CACHE
|
|
68
|
+
if (this.cx.params.cache_name) {
|
|
69
|
+
// Clear outdated CACHES
|
|
70
|
+
await self.caches.keys().then(keyList => {
|
|
71
|
+
return Promise.all(keyList.map(key => {
|
|
72
|
+
if (key !== this.cx.params.cache_name && key !== this.cx.params.cache_name + '_json') {
|
|
73
|
+
if (this.cx.logger) { this.cx.logger.log('[ServiceWorker] Removing old cache:', key); }
|
|
74
|
+
return self.caches.delete(key);
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
resolve();
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
self.addEventListener('install', installHandler);
|
|
83
|
+
self.addEventListener('activate', activateHandler);
|
|
84
|
+
const uncontrols = this.control();
|
|
85
|
+
return () => {
|
|
86
|
+
self.removeEventListener('install', installHandler);
|
|
87
|
+
self.removeEventListener('activate', activateHandler);
|
|
88
|
+
uncontrols();
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
control() {
|
|
93
|
+
// ONFETCH
|
|
94
|
+
const fetchHandler = (event) => {
|
|
95
|
+
// URL schemes that might arrive here but not supported; e.g.: chrome-extension://
|
|
96
|
+
if (!event.request.url.startsWith('http')) return;
|
|
97
|
+
// Handle external requests
|
|
98
|
+
if (!event.request.url.startsWith(self.origin)) {
|
|
99
|
+
return event.respondWith(this.remoteFetch(event.request));
|
|
100
|
+
}
|
|
101
|
+
if (event.request.mode === 'navigate' || event.request.cache === 'force-cache'/* && event.request.mode === 'navigate' - even webflo client init call also comes with that... needs investigation */) {
|
|
102
|
+
// Now, the following is key:
|
|
103
|
+
// The browser likes to use "force-cache" for "navigate" requests, when, e.g: re-entering your site with the back button
|
|
104
|
+
// Problem here, force-cache forces out JSON not HTML as per webflo's design.
|
|
105
|
+
// So, we detect this scenerio and avoid it.
|
|
106
|
+
event.respondWith((async (event) => {
|
|
107
|
+
const { url, ...requestInit } = await Request.copy(event.request);
|
|
108
|
+
requestInit.cache = 'default';
|
|
109
|
+
return await this.navigate(url, requestInit, { event });
|
|
110
|
+
})(event));
|
|
111
|
+
} else {
|
|
112
|
+
event.respondWith(this.navigate(event.request.url, event.request, { event }));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
self.addEventListener('fetch', fetchHandler);
|
|
116
|
+
return () => {
|
|
117
|
+
self.removeEventListener('fetch', fetchHandler);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
createRequest(href, init = {}) {
|
|
122
|
+
return new Request(href, init);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async navigate(url, init = {}, detail = {}) {
|
|
126
|
+
// Resolve inputs
|
|
127
|
+
const scope = { url, init, detail };
|
|
128
|
+
if (typeof scope.url === 'string') {
|
|
129
|
+
scope.url = new URL(scope.url, self.location.origin);
|
|
130
|
+
}
|
|
131
|
+
scope.response = await new Promise(async (resolveResponse) => {
|
|
132
|
+
scope.handleRespondWith = async (response) => {
|
|
133
|
+
if (scope.finalResponseSeen) {
|
|
134
|
+
throw new Error('Final response already sent');
|
|
135
|
+
}
|
|
136
|
+
if (scope.initialResponseSeen) {
|
|
137
|
+
return await this.execPush(scope.clientMessaging, response);
|
|
138
|
+
}
|
|
139
|
+
resolveResponse(response);
|
|
140
|
+
};
|
|
141
|
+
// Create and route request
|
|
142
|
+
scope.request = this.createRequest(scope.url, scope.init);
|
|
143
|
+
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
144
|
+
scope.session = this.constructor.SessionStorage.create(scope.request, { secret: this.cx.env.entries.SESSION_KEY });
|
|
145
|
+
const portID = crypto.randomUUID();
|
|
146
|
+
scope.clientMessaging = new ClientMessaging(this, portID, { isPrimary: true });
|
|
147
|
+
scope.user = this.constructor.HttpUser.create(
|
|
148
|
+
scope.request,
|
|
149
|
+
scope.session,
|
|
150
|
+
scope.clientMessaging
|
|
151
|
+
);
|
|
152
|
+
scope.httpEvent = this.constructor.HttpEvent.create(scope.handleRespondWith, {
|
|
153
|
+
request: scope.request,
|
|
154
|
+
detail: scope.detail,
|
|
155
|
+
cookies: scope.cookies,
|
|
156
|
+
session: scope.session,
|
|
157
|
+
user: scope.user,
|
|
158
|
+
client: scope.clientMessaging
|
|
159
|
+
});
|
|
160
|
+
// Restore session before dispatching
|
|
161
|
+
if (scope.request.method === 'GET'
|
|
162
|
+
&& (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
|
|
163
|
+
&& (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
164
|
+
scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
|
|
165
|
+
}
|
|
166
|
+
// Dispatch for response
|
|
167
|
+
scope.$response = await this.dispatch(scope.httpEvent, {}, async (event) => {
|
|
168
|
+
// Was this nexted()? Tell the next layer we're in JSON mode by default
|
|
169
|
+
if (event !== scope.httpEvent && !event.request.headers.has('Accept')) {
|
|
170
|
+
event.request.headers.set('Accept', 'application/json');
|
|
171
|
+
}
|
|
172
|
+
return await this.remoteFetch(event.request);
|
|
173
|
+
});
|
|
174
|
+
// Final reponse!!!
|
|
175
|
+
scope.finalResponseSeen = true;
|
|
176
|
+
if (scope.initialResponseSeen) {
|
|
177
|
+
// Send via background port
|
|
178
|
+
if (typeof scope.$response !== 'undefined') {
|
|
179
|
+
await this.execPush(scope.clientMessaging, scope.$response);
|
|
180
|
+
}
|
|
181
|
+
scope.clientMessaging.close();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Send normally
|
|
185
|
+
resolveResponse(scope.$response);
|
|
186
|
+
});
|
|
187
|
+
scope.initialResponseSeen = true;
|
|
188
|
+
scope.hasBackgroundActivity = !scope.finalResponseSeen || (scope.redirectMessage && !(scope.response instanceof Response && scope.response.headers.get('Location')));
|
|
189
|
+
scope.response = await this.normalizeResponse(scope.httpEvent, scope.response, scope.hasBackgroundActivity);
|
|
190
|
+
if (scope.hasBackgroundActivity) {
|
|
191
|
+
scope.response.headers.set('X-Background-Messaging', `ch:${scope.clientMessaging.port.name}`);
|
|
192
|
+
}
|
|
193
|
+
if (scope.response instanceof Response && scope.response.headers.get('Location')) {
|
|
194
|
+
if (scope.redirectMessage) {
|
|
195
|
+
scope.session.set(`redirect-message:${scope.redirectMessageID}`, scope.redirectMessage);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
if (scope.redirectMessage) {
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
this.execPush(scope.clientMessaging, scope.redirectMessage);
|
|
201
|
+
if (scope.finalResponseSeen) {
|
|
202
|
+
scope.clientMessaging.close();
|
|
203
|
+
}
|
|
204
|
+
}, 500);
|
|
205
|
+
} else if (scope.finalResponseSeen) {
|
|
206
|
+
scope.clientMessaging.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return scope.response;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async remoteFetch(request, ...args) {
|
|
213
|
+
if (arguments.length > 1) {
|
|
214
|
+
request = this.createRequest(request, ...args);
|
|
215
|
+
}
|
|
216
|
+
const scope = {};
|
|
217
|
+
const matchUrl = (patterns, url) => _any((patterns || []).map(p => p.trim()).filter(p => p), p => pattern(p, self.origin).test(url));
|
|
218
|
+
if (matchUrl(this.cx.params.cache_only_urls, request.url)) {
|
|
219
|
+
scope.strategy = 'cache-only';
|
|
220
|
+
scope.response = this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
|
|
221
|
+
} else if (matchUrl(this.cx.params.network_only_urls, request.url)) {
|
|
222
|
+
scope.strategy = 'network-only';
|
|
223
|
+
scope.response = this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
|
|
224
|
+
} else if (matchUrl(this.cx.params.cache_first_urls, request.url)) {
|
|
225
|
+
scope.strategy = 'cache-first';
|
|
226
|
+
scope.response = this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
|
|
227
|
+
} else if (matchUrl(this.cx.params.network_first_urls, request.url) || !this.cx.params.default_fetching_strategy) {
|
|
228
|
+
scope.strategy = 'network-first';
|
|
229
|
+
scope.response = this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
|
|
230
|
+
} else {
|
|
231
|
+
scope.strategy = this.cx.params.default_fetching_strategy;
|
|
232
|
+
switch (this.cx.params.default_fetching_strategy) {
|
|
233
|
+
case 'cache-only':
|
|
234
|
+
scope.response = this.cacheFetch(request, { networkFallback: false, cacheRefresh: false });
|
|
235
|
+
break;
|
|
236
|
+
case 'network-only':
|
|
237
|
+
scope.response = this.networkFetch(request, { cacheFallback: false, cacheRefresh: false });
|
|
238
|
+
break;
|
|
239
|
+
case 'cache-first':
|
|
240
|
+
scope.response = this.cacheFetch(request, { networkFallback: true, cacheRefresh: true });
|
|
241
|
+
break;
|
|
242
|
+
case 'network-first':
|
|
243
|
+
scope.response = this.networkFetch(request, { cacheFallback: true, cacheRefresh: true });
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return await scope.response;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async networkFetch(request, params = {}) {
|
|
251
|
+
if (!params.cacheFallback) {
|
|
252
|
+
return xfetch(request);
|
|
253
|
+
}
|
|
254
|
+
return xfetch(request).then((response) => {
|
|
255
|
+
if (params.cacheRefresh) this.refreshCache(request, response);
|
|
256
|
+
return response;
|
|
257
|
+
}).catch((e) => this.getRequestCache(request).then(cache => {
|
|
258
|
+
return cache.match(request);
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async cacheFetch(request, params = {}) {
|
|
263
|
+
return this.getRequestCache(request).then(cache => cache.match(request).then((response) => {
|
|
264
|
+
// Nothing cache, use network
|
|
265
|
+
if (!response && params.networkFallback) return this.networkFetch(request, { ...params, cacheFallback: false });
|
|
266
|
+
// Note: fetch, but for refreshing purposes only... not the returned response
|
|
267
|
+
if (response && params.cacheRefresh) this.networkFetch(request, { ...params, justRefreshing: true });
|
|
268
|
+
return response;
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async refreshCache(request, response) {
|
|
273
|
+
// Check if we received a valid response
|
|
274
|
+
if (request.method !== 'GET' || !response || response.status !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
// IMPORTANT: Clone the response. A response is a stream
|
|
278
|
+
// and because we want the browser to consume the response
|
|
279
|
+
// as well as the cache consuming the response, we need
|
|
280
|
+
// to clone it so we have two streams.
|
|
281
|
+
var responseToCache = response.clone();
|
|
282
|
+
this.getRequestCache(request).then(cache => {
|
|
283
|
+
cache.put(request, responseToCache);
|
|
284
|
+
});
|
|
285
|
+
return response;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async getRequestCache(request) {
|
|
289
|
+
const cacheName = request.headers.get('Accept') === 'application/json'
|
|
290
|
+
? this.cx.params.cache_name + '_json'
|
|
291
|
+
: this.cx.params.cache_name;
|
|
292
|
+
return self.caches.open(cacheName);
|
|
293
|
+
}
|
|
294
|
+
}
|