@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.
Files changed (66) hide show
  1. package/package.json +1 -1
  2. package/src/{Context.js → AbstractContext.js} +1 -9
  3. package/src/deployment-pi/origins/index.js +1 -1
  4. package/src/index.js +1 -9
  5. package/src/runtime-pi/HttpEvent.js +101 -81
  6. package/src/runtime-pi/HttpUser.js +126 -0
  7. package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
  8. package/src/runtime-pi/MessagingOverChannel.js +85 -0
  9. package/src/runtime-pi/MessagingOverSocket.js +106 -0
  10. package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
  11. package/src/runtime-pi/WebfloCookieStorage.js +27 -0
  12. package/src/runtime-pi/WebfloEventTarget.js +39 -0
  13. package/src/runtime-pi/WebfloMessageEvent.js +58 -0
  14. package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
  15. package/src/runtime-pi/{Router.js → WebfloRouter.js} +3 -34
  16. package/src/runtime-pi/WebfloRuntime.js +52 -0
  17. package/src/runtime-pi/WebfloStorage.js +109 -0
  18. package/src/runtime-pi/client/ClientMessaging.js +5 -0
  19. package/src/runtime-pi/client/Context.js +2 -6
  20. package/src/runtime-pi/client/CookieStorage.js +17 -0
  21. package/src/runtime-pi/client/Router.js +3 -13
  22. package/src/runtime-pi/client/SessionStorage.js +33 -0
  23. package/src/runtime-pi/client/Url.js +24 -72
  24. package/src/runtime-pi/client/WebfloClient.js +544 -0
  25. package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
  26. package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
  27. package/src/runtime-pi/client/WebfloSubClient.js +165 -0
  28. package/src/runtime-pi/client/Workport.js +89 -161
  29. package/src/runtime-pi/client/generate.js +1 -1
  30. package/src/runtime-pi/client/index.js +13 -18
  31. package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
  32. package/src/runtime-pi/client/worker/Context.js +2 -6
  33. package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
  34. package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
  35. package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
  36. package/src/runtime-pi/client/worker/Workport.js +13 -73
  37. package/src/runtime-pi/client/worker/index.js +7 -18
  38. package/src/runtime-pi/index.js +1 -8
  39. package/src/runtime-pi/server/ClientMessaging.js +18 -0
  40. package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
  41. package/src/runtime-pi/server/Context.js +2 -6
  42. package/src/runtime-pi/server/CookieStorage.js +17 -0
  43. package/src/runtime-pi/server/Router.js +2 -68
  44. package/src/runtime-pi/server/SessionStorage.js +53 -0
  45. package/src/runtime-pi/server/WebfloServer.js +755 -0
  46. package/src/runtime-pi/server/index.js +7 -18
  47. package/src/runtime-pi/util-http.js +268 -32
  48. package/src/runtime-pi/xURL.js +25 -22
  49. package/src/runtime-pi/xfetch.js +2 -2
  50. package/src/runtime-pi/Application.js +0 -29
  51. package/src/runtime-pi/Cookies.js +0 -82
  52. package/src/runtime-pi/Runtime.js +0 -21
  53. package/src/runtime-pi/client/Application.js +0 -76
  54. package/src/runtime-pi/client/Runtime.js +0 -525
  55. package/src/runtime-pi/client/createStorage.js +0 -58
  56. package/src/runtime-pi/client/worker/Application.js +0 -44
  57. package/src/runtime-pi/client/worker/Runtime.js +0 -275
  58. package/src/runtime-pi/server/Application.js +0 -101
  59. package/src/runtime-pi/server/Runtime.js +0 -558
  60. package/src/runtime-pi/xFormData.js +0 -24
  61. package/src/runtime-pi/xHeaders.js +0 -146
  62. package/src/runtime-pi/xRequest.js +0 -46
  63. package/src/runtime-pi/xRequestHeaders.js +0 -109
  64. package/src/runtime-pi/xResponse.js +0 -33
  65. package/src/runtime-pi/xResponseHeaders.js +0 -117
  66. 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
- * @imports
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
- export default class Workport {
10
-
11
- constructor(file, params = {}, env = {}) {
12
- this.ready = navigator.serviceWorker ? navigator.serviceWorker.ready : new Promise(() => {});
13
-
14
- // --------
15
- // Registration and lifecycle
16
- // --------
17
- this.registration = new Promise((resolve, reject) => {
18
- if (!navigator.serviceWorker) return;
19
- const register = () => {
20
- navigator.serviceWorker.register(file, { scope: params.scope || '/' }).then(async registration => {
21
-
22
- // Helper that updates instance's state
23
- const state = target => {
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
- if (params.startMessages) {
54
- navigator.serviceWorker.startMessages();
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
- // Post messaging
60
- // --------
61
- const postSendCallback = (message, callback, onAvailability = 1) => {
62
- if (this.active) {
63
- if (_isFunction(message)) message = message();
64
- callback(this.active, message);
65
- } else if (onAvailability) {
66
- // Availability Handling
67
- const availabilityHandler = Observer.observe(this, 'active', entry => {
68
- if (_isFunction(message)) message = message();
69
- callback(entry.value, message);
70
- if (onAvailability !== 2) { availabilityHandler.abort(); }
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
- this.messaging = {
75
- post: (message, onAvailability = 1) => {
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
- // Notifications
121
- // --------
122
- this.notifications = {
123
- requestPermission: () => {
124
- return new Promise(async (resolve, reject) => {
125
- const permissionResult = Notification.requestPermission(resolve);
126
- if (permissionResult) { permissionResult.then(resolve, reject); }
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
- fire: async (title, params = {}) => {
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
- * @imports
4
- */
5
- import Context from './Context.js';
6
- import Application from './Application.js';
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
- * @APIS
20
- */
21
- export * as APIS from './Runtime.js';
12
+ export { WebfloSubClient } from './WebfloSubClient.js';
13
+ export {
14
+ WebfloRootClient1,
15
+ WebfloRootClient2
16
+ }
@@ -0,0 +1,5 @@
1
+ import { MessagingOverBroadcast } from '../../MessagingOverBroadcast.js';
2
+
3
+ export class ClientMessaging extends MessagingOverBroadcast {
4
+ get runtime() { return this.parentNode; }
5
+ }
@@ -1,7 +1,3 @@
1
+ import { Context as AbstractContext } from '../Context.js';
1
2
 
2
- /**
3
- * @imports
4
- */
5
- import _Context from '../Context.js';
6
-
7
- export default class Context extends _Context {}
3
+ export class Context extends AbstractContext {}
@@ -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,13 @@
1
+ import { WebfloStorage } from '../../WebfloStorage.js';
2
+
3
+ export class SessionStorage extends WebfloStorage {
4
+ #type;
5
+
6
+ static async create(request) {
7
+ return new this;
8
+ }
9
+
10
+ async commit(response) {
11
+ super.commit();
12
+ }
13
+ }
@@ -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
+ }