@webqit/webflo 1.0.19 → 1.0.21

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 (32) hide show
  1. package/package.json +7 -4
  2. package/src/config-pi/runtime/Client.js +50 -46
  3. package/src/config-pi/runtime/Server.js +77 -14
  4. package/src/config-pi/runtime/client/Worker.js +22 -20
  5. package/src/runtime-pi/HttpEvent.js +34 -19
  6. package/src/runtime-pi/HttpUser.js +8 -84
  7. package/src/runtime-pi/WebfloCookieStorage.js +28 -12
  8. package/src/runtime-pi/WebfloRouter.js +2 -2
  9. package/src/runtime-pi/WebfloRuntime.js +9 -4
  10. package/src/runtime-pi/WebfloStorage.js +91 -34
  11. package/src/runtime-pi/client/Capabilities.js +211 -0
  12. package/src/runtime-pi/client/CookieStorage.js +3 -3
  13. package/src/runtime-pi/client/SessionStorage.js +8 -25
  14. package/src/runtime-pi/client/WebfloClient.js +15 -23
  15. package/src/runtime-pi/client/WebfloRootClient1.js +55 -34
  16. package/src/runtime-pi/client/WebfloRootClient2.js +2 -2
  17. package/src/runtime-pi/client/WebfloSubClient.js +9 -5
  18. package/src/runtime-pi/client/Workport.js +64 -91
  19. package/src/runtime-pi/client/generate.js +25 -16
  20. package/src/runtime-pi/client/index.js +3 -2
  21. package/src/runtime-pi/client/worker/CookieStorage.js +6 -4
  22. package/src/runtime-pi/client/worker/SessionStorage.js +3 -7
  23. package/src/runtime-pi/client/worker/WebfloWorker.js +70 -56
  24. package/src/runtime-pi/client/worker/index.js +3 -2
  25. package/src/runtime-pi/server/CookieStorage.js +6 -4
  26. package/src/runtime-pi/server/SessionStorage.js +17 -19
  27. package/src/runtime-pi/server/WebfloServer.js +66 -12
  28. package/src/runtime-pi/server/index.js +1 -0
  29. package/src/runtime-pi/util-http.js +15 -2
  30. package/src/services-pi/index.js +2 -0
  31. package/src/services-pi/push/index.js +23 -0
  32. package/src/static-pi/index.js +1 -1
@@ -2,6 +2,11 @@ import { _isObject } from '@webqit/util/js/index.js';
2
2
 
3
3
  export class WebfloRuntime {
4
4
 
5
+ async setup(httpEvent) {
6
+ const router = new this.constructor.Router(this.cx, httpEvent.url.pathname);
7
+ return await router.route(['SETUP'], httpEvent);
8
+ }
9
+
5
10
  async dispatch(httpEvent, context, crossLayerFetch) {
6
11
  const requestLifecycle = {};
7
12
  requestLifecycle.responsePromise = new Promise(async (res) => {
@@ -25,16 +30,16 @@ export class WebfloRuntime {
25
30
  return await requestLifecycle.responsePromise;
26
31
  }
27
32
 
28
- async normalizeResponse(httpEvent, response, forceCommit = false) {
33
+ async normalizeResponse(httpEvent, response) {
29
34
  // Normalize response
30
35
  if (!(response instanceof Response)) {
31
36
  response = typeof response === 'undefined'
32
37
  ? new Response(null, { status: 404 })
33
38
  : Response.create(response);
34
39
  }
35
- // Commit data
36
- for (const storage of [httpEvent.cookies, httpEvent.session, httpEvent.storage]) {
37
- await storage?.commit?.(response, forceCommit);
40
+ // Commit data in the exact order. Reason: in how they depend on each other
41
+ for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
42
+ await storage?.commit?.(response);
38
43
  }
39
44
  return response;
40
45
  }
@@ -1,46 +1,109 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
2
  import { _even } from '@webqit/util/obj/index.js';
3
3
 
4
- export class WebfloStorage extends Map {
4
+ export class WebfloStorage {
5
5
 
6
6
  #request;
7
7
  #session;
8
+ #registry;
9
+ #key;
10
+ #store;
8
11
 
9
- constructor(request, session, iterable = []) {
10
- super(iterable);
12
+ constructor(registry, key, request, session = null) {
13
+ this.#registry = registry;
14
+ this.#key = key;
11
15
  this.#request = request;
12
16
  this.#session = session === true ? this : session;
13
17
  }
14
18
 
15
- #originals;
16
- saveOriginals() { this.#originals = new Map(this); }
17
-
18
- getDeleted() {
19
- if (!this.#originals) return [];
20
- return [...this.#originals.keys()].filter((k) => {
21
- return !this.has(k);
22
- });
19
+ async store() {
20
+ if (!this.#key) {
21
+ return this.#registry;
22
+ }
23
+ if (!this.#store && !(this.#store = await this.#registry.get(this.#key))) {
24
+ this.#store = {};
25
+ await this.#registry.set(this.#key, this.#store);
26
+ }
27
+ return this.#store;
28
+ }
29
+
30
+ async commit() {
31
+ if (!this.#store || !this.#key) return;
32
+ await this.#registry.set(this.#key, this.#store);
33
+ }
34
+
35
+ get size() { return this.store().then((store) => Object.keys(store).length); }
36
+
37
+ [ Symbol.iterator ]() { return this.entries().then((entries) => entries[ Symbol.iterator ]()); }
38
+
39
+ async json(arg = null) {
40
+ if (!arguments.length || typeof arg === 'boolean') {
41
+ return { ...(await this.store()) };
42
+ }
43
+ if (!_isObject(arg)) {
44
+ throw new Error(`Argument must be a valid JSON object`);
45
+ }
46
+ return await Promise.all(Object.entries(arg).map(([key, value]) => {
47
+ return this.set(key, value);
48
+ }));
49
+ }
50
+
51
+ async get(key) { return Reflect.get(await this.store(), key); }
52
+
53
+ async has(key) { return Reflect.has(await this.store(), key); }
54
+
55
+ async keys() { return Object.keys(await this.store()); }
56
+
57
+ async values() { return Object.values(await this.store()); }
58
+
59
+ async entries() { return Object.entries(await this.store()); }
60
+
61
+ async forEach(callback) { (await this.entries()).forEach(callback); }
62
+
63
+ async set(key, value) {
64
+ Reflect.set(await this.store(), key, value);
65
+ await this.emit(key, value);
66
+ return this;
67
+ }
68
+
69
+ async delete(key) {
70
+ Reflect.deleteProperty(await this.store(), key);
71
+ await this.emit(key);
72
+ return this;
23
73
  }
24
74
 
25
- getAdded() {
26
- if (!this.#originals) return [...this.keys()];
27
- return [...new Set([...this.keys(), ...this.#originals.keys()])].filter((k) => {
28
- return !this.#originals.has(k) || (this.has(k) && ((a, b) => _isObject(a) && _isObject(b) ? !_even(a, b) : a !== b)(this.get(k, true), this.#originals.get(k)));
29
- });
75
+ async clear() {
76
+ for (const key of await this.keys()) {
77
+ Reflect.deleteProperty(await this.store(), key);
78
+ }
79
+ await this.emit();
80
+ return this;
30
81
  }
31
82
 
32
- commit() {
33
- this.saveOriginals();
83
+ #listeners = new Set;
84
+ observe(attr, handler) {
85
+ const args = { attr, handler };
86
+ this.#listeners.add(args);
87
+ return () => {
88
+ this.#listeners.delete(args);
89
+ }
90
+ }
91
+
92
+ async emit(attr, value) {
93
+ const returnValues = [];
94
+ for (const { attr: $attr, handler } of this.#listeners) {
95
+ if (arguments.length && $attr !== attr) continue;
96
+ if (arguments.length > 1) {
97
+ returnValues.push(handler(value));
98
+ } else {
99
+ returnValues.push(handler());
100
+ }
101
+ }
102
+ return Promise.all(returnValues);
34
103
  }
35
104
 
36
105
  #handlers = new Map;
37
- #reverseHandlers = new Map;
38
106
  defineHandler(attr, ...handlers) {
39
- let registry = this.#handlers;
40
- if (handlers[0] === false) {
41
- registry = this.#reverseHandlers;
42
- handlers.shift();
43
- }
44
107
  const $handlers = [];
45
108
  for (let handler of handlers) {
46
109
  if (typeof handler === 'function') {
@@ -52,21 +115,15 @@ export class WebfloStorage extends Map {
52
115
  }
53
116
  $handlers.push(handler);
54
117
  }
55
- registry.set(attr, $handlers);
56
- }
57
-
58
- defineReverseHandler(attr, ...handlers) {
59
- return this.defineHandler(attr, false, ...handlers);
118
+ this.#handlers.set(attr, $handlers);
60
119
  }
61
120
 
62
121
  getHandlers() { return this.#handlers; }
63
122
 
64
- getReverseHandlers() { return this.#reverseHandlers; }
65
-
66
123
  async require(attrs, callback = null, noNulls = false) {
67
124
  const entries = [];
68
125
  main: for await (const attr of [].concat(attrs)) {
69
- if (!this.has(attr) || (noNulls && [undefined, null].includes(this.get(attr)))) {
126
+ if (!(await this.has(attr)) || (noNulls && [undefined, null].includes(await this.get(attr)))) {
70
127
  const handlers = this.#handlers.get(attr);
71
128
  if (!handlers) {
72
129
  throw new Error(`No handler defined for the user attribute: ${attr}`);
@@ -94,14 +151,14 @@ export class WebfloStorage extends Map {
94
151
  }
95
152
  const messageID = (0 | Math.random() * 9e6).toString(36);
96
153
  urlRewrite.searchParams.set('redirect-message', messageID);
97
- this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
154
+ await this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
98
155
  }
99
156
  return new Response(null, { status: 302, headers: {
100
157
  Location: urlRewrite
101
158
  }});
102
159
  }
103
160
  }
104
- entries.push(this.get(attr));
161
+ entries.push(await this.get(attr));
105
162
  }
106
163
  if (callback) return await callback(...entries);
107
164
  return entries;
@@ -0,0 +1,211 @@
1
+ const { Observer } = webqit;
2
+
3
+ export class Capabilities {
4
+
5
+ #params;
6
+
7
+ #exposed = {};
8
+ get exposed() { return this.#exposed; }
9
+
10
+ #cleanups = [];
11
+
12
+ static async initialize(params) {
13
+ const instance = new this;
14
+ instance.#params = params;
15
+ instance.#params.generic_public_webhook_url = instance.#params.generic_public_webhook_url_variable && instance.#params.env[instance.#params.generic_public_webhook_url_variable];
16
+ instance.#params.vapid_public_key = instance.#params.vapid_public_key_variable && instance.#params.env[instance.#params.vapid_public_key_variable];
17
+ // --------
18
+ // Custom install
19
+ const onbeforeinstallprompt = (e) => {
20
+ if (instance.#params.custom_install && instance.#exposed.custom_install !== 'granted') {
21
+ e.preventDefault();
22
+ Observer.set(instance.#exposed, 'custom_install', e);
23
+ }
24
+ };
25
+ window.addEventListener('beforeinstallprompt', onbeforeinstallprompt);
26
+ instance.#cleanups.push(() => window.removeEventListener('beforeinstallprompt', onbeforeinstallprompt));
27
+ // --------
28
+ // Webhooks
29
+ if (instance.#params.generic_public_webhook_url) {
30
+ // --------
31
+ // app.installed
32
+ const onappinstalled = () => {
33
+ fetch(instance.#params.generic_public_webhook_url, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ type: 'app.installed', data: true })
37
+ }).catch(() => {});
38
+ };
39
+ window.addEventListener('appinstalled', onappinstalled);
40
+ instance.#cleanups.push(() => window.removeEventListener('appinstalled', onappinstalled));
41
+ // --------
42
+ // push.subscribe/unsubscribe
43
+ if (instance.#params.webpush) {
44
+ try {
45
+ const pushPermissionStatus = await navigator.permissions.query({ name: 'push', userVisibleOnly: true });
46
+ const pushPermissionStatusHandler = async () => {
47
+ const pushManager = (await navigator.serviceWorker.getRegistration()).pushManager;
48
+ const eventPayload = pushPermissionStatus.state === 'granted'
49
+ ? { type: 'push.subscribe', data: await pushManager.getSubscription() }
50
+ : { type: 'push.unsubscribe' };
51
+ if (eventPayload.type === 'push.subscribe' && !eventPayload.data) {
52
+ return window.queueMicrotask(pushPermissionStatusHandler);
53
+ }
54
+ fetch(instance.#params.generic_public_webhook_url, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(eventPayload)
58
+ }).catch(() => {});
59
+ }
60
+ pushPermissionStatus.addEventListener('change', pushPermissionStatusHandler);
61
+ instance.#cleanups.push(() => pushPermissionStatus.removeEventListener('change', pushPermissionStatusHandler));
62
+ } catch(e) {}
63
+ }
64
+ }
65
+ // --------
66
+ // Exposure
67
+ if (Array.isArray(instance.#params.exposed) && instance.#params.exposed.length) {
68
+ const [permissions, cleanup] = await instance.query(instance.#params.exposed.map((s) => s.trim()), true);
69
+ instance.#exposed = permissions;
70
+ instance.#cleanups.push(cleanup);
71
+ }
72
+ return instance;
73
+ }
74
+
75
+ async query(query, live = false) {
76
+ const permissions = {}, cleanups = [];
77
+ for (let q of [].concat(query)) {
78
+ q = this.resolveQuery(q);
79
+ // ------
80
+ // Display mode
81
+ if (q.name === 'display-mode') {
82
+ const handleDisplayMode = () => {
83
+ if (document.referrer.startsWith('android-app://')) {
84
+ Observer.set(permissions, 'display_mode', 'twa');
85
+ return;
86
+ }
87
+ for (const dm of ['browser', 'standalone', 'minimal-ui', 'fullscreen', 'window-controls-overlay']) {
88
+ const mediaQuery = window.matchMedia(`(display-mode: ${dm})`);
89
+ if (mediaQuery.matches) {
90
+ Observer.set(permissions, 'display_mode', dm);
91
+ if (live) {
92
+ mediaQuery.addEventListener('change', handleDisplayMode, { once: true });
93
+ cleanups.push(() => mediaQuery.removeEventListener('change', handleDisplayMode));
94
+ }
95
+ return;
96
+ }
97
+ }
98
+ };
99
+ handleDisplayMode();
100
+ continue;
101
+ }
102
+ // ------
103
+ // Others
104
+ try {
105
+ const permissionStatus = await navigator.permissions.query(q);
106
+ permissions[permissionStatus.name.replace(/-/g, '_')] = permissionStatus.state;
107
+ if (live) {
108
+ const onchange = () => {
109
+ Observer.set(permissions, permissionStatus.name.replace(/-/g, '_'), permissionStatus.state);
110
+ };
111
+ permissionStatus.addEventListener('change', onchange);
112
+ cleanups.push(() => permissionStatus.removeEventListener('change', onchange));
113
+ }
114
+ } catch(e) {
115
+ permissions[q.name.replace(/-/g, '_')] = 'unsupported';
116
+ console.log(e);
117
+ }
118
+ }
119
+ if (live) {
120
+ return [
121
+ permissions,
122
+ () => cleanups.forEach((c) => c())
123
+ ];
124
+ }
125
+ return permissions;
126
+ }
127
+
128
+ async request(name, params = {}) {
129
+ params = this.resolveRequest(name, params);
130
+ // ------
131
+ // install
132
+ if (name === 'install') {
133
+ let returnValue;
134
+ if (this.#exposed.custom_install === 'granted') return;
135
+ if (this.#exposed.custom_install) {
136
+ returnValue = await this.#exposed.custom_install.prompt?.();
137
+ }
138
+ Observer.set(this.#exposed, 'custom_install', 'granted');
139
+ return returnValue;
140
+ }
141
+ // ------
142
+ // notification
143
+ if (name === 'notification') {
144
+ return await new Promise(async (resolve, reject) => {
145
+ const permissionResult = Notification.requestPermission(resolve);
146
+ if (permissionResult) {
147
+ permissionResult.then(resolve, reject);
148
+ }
149
+ });
150
+ }
151
+ // ------
152
+ // push
153
+ if (name === 'push') {
154
+ const pushManager = (await navigator.serviceWorker.getRegistration()).pushManager;
155
+ const subscription = (await pushManager.getSubscription()) || await pushManager.subscribe(params);
156
+ return subscription;
157
+ }
158
+ }
159
+
160
+ async supports(q) {
161
+ try {
162
+ await navigator.permissions.query(this.resolveQuery(q));
163
+ return true;
164
+ } catch(e) {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ resolveQuery(q) {
170
+ if (typeof q === 'string') {
171
+ q = { name: q };
172
+ }
173
+ if (q.name === 'push' && !q.userVisibleOnly) {
174
+ q = { ...q, userVisibleOnly: true };
175
+ }
176
+ if (q.name === 'top-level-storage-access' && !q.requestedOrigin) {
177
+ q = { ...q, requestedOrigin: window.location.origin };
178
+ }
179
+ return q;
180
+ }
181
+
182
+ resolveRequest(name, params = {}) {
183
+ if (name === 'push') {
184
+ if (!params.userVisibleOnly) {
185
+ params = { ...params, userVisibleOnly: true };
186
+ }
187
+ if (!params.applicationServerKey && this.#params.vapid_public_key) {
188
+ params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.vapid_public_key) };
189
+ }
190
+ }
191
+ return params;
192
+ }
193
+
194
+ close() {
195
+ this.#cleanups.forEach((c) => c());
196
+ }
197
+ }
198
+
199
+ // Public base64 to Uint
200
+ function urlBase64ToUint8Array(base64String) {
201
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
202
+ const base64 = (base64String + padding)
203
+ .replace(/\-/g, '+')
204
+ .replace(/_/g, '/');
205
+ const rawData = window.atob(base64);
206
+ const outputArray = new Uint8Array(rawData.length);
207
+ for (let i = 0; i < rawData.length; ++i) {
208
+ outputArray[i] = rawData.charCodeAt(i);
209
+ }
210
+ return outputArray;
211
+ }
@@ -8,10 +8,10 @@ export class CookieStorage extends WebfloCookieStorage {
8
8
  );
9
9
  }
10
10
 
11
- commit(response) {
12
- for (const cookieStr of this.render()) {
11
+ async commit(response) {
12
+ for (const cookieStr of await this.render()) {
13
13
  document.cookie = cookieStr;
14
14
  }
15
- super.commit();
15
+ await super.commit();
16
16
  }
17
17
  }
@@ -1,33 +1,16 @@
1
1
  import { WebfloStorage } from '../WebfloStorage.js';
2
2
 
3
3
  export class SessionStorage extends WebfloStorage {
4
- static get type() { return 'session'; }
5
-
6
4
  static create(request) {
7
- const keys = [];
8
- const storeType = this.type === 'user' ? 'localStorage' : 'sessionStorage';
9
- for(let i = 0; i < window[storeType].length; i ++){
10
- keys.push(window[storeType].key(i));
11
- };
12
- const instance = new this(
5
+ const registry = {
6
+ async get(key) { return localStorage.getItem(key) },
7
+ async set(key, value) { return localStorage.setItem(key, value) },
8
+ };
9
+ return new this(
10
+ registry,
11
+ 'session',
13
12
  request,
14
- keys.map((key) => [key, window[storeType].getItem(key)])
13
+ true
15
14
  );
16
- return instance;
17
- }
18
-
19
- constructor(request, iterable) {
20
- super(request, true, iterable);
21
- }
22
-
23
- commit() {
24
- const storeType = this.constructor.type === 'user' ? 'localStorage' : 'sessionStorage';
25
- for (const key of this.getAdded()) {
26
- window[storeType].setItem(key, this.get(key));
27
- }
28
- for (const key of this.getDeleted()) {
29
- window[storeType].removeItem(key);
30
- }
31
- super.commit();
32
15
  }
33
16
  }
@@ -32,9 +32,6 @@ export class WebfloClient extends WebfloRuntime {
32
32
  #host;
33
33
  get host() { return this.#host; }
34
34
 
35
- #network;
36
- get network() { return this.#network; }
37
-
38
35
  #location;
39
36
  get location() { return this.#location; }
40
37
 
@@ -55,7 +52,6 @@ export class WebfloClient extends WebfloRuntime {
55
52
  super();
56
53
  this.#host = host;
57
54
  Object.defineProperty(this.host, 'webfloRuntime', { get: () => this });
58
- this.#network = { status: window.navigator.onLine };
59
55
  this.#location = new Url/*NOT URL*/(this.host.location);
60
56
  this.#navigator = {
61
57
  requesting: null,
@@ -72,7 +68,7 @@ export class WebfloClient extends WebfloRuntime {
72
68
  };
73
69
  }
74
70
 
75
- initialize() {
71
+ async initialize() {
76
72
  this.#backgroundMessaging = new MultiportMessagingAPI(this, { runtime: this });
77
73
  // Bind response and redirect handlers
78
74
  const responseHandler = (e) => {
@@ -92,19 +88,13 @@ export class WebfloClient extends WebfloRuntime {
92
88
  });
93
89
  }, 10);
94
90
  };
95
- this.backgroundMessaging.handleMessages('response', responseHandler);
96
- this.backgroundMessaging.handleMessages('redirect', responseHandler);
97
- // Bind network status handlers
98
- const onlineHandler = () => Observer.set(this.network, 'status', window.navigator.onLine);
99
- window.addEventListener('online', onlineHandler);
100
- window.addEventListener('offline', onlineHandler);
101
- // Start controlling
102
- const uncontrols = this.control();
91
+ const responseHandler1Cleanup = this.backgroundMessaging.handleMessages('response', responseHandler);
92
+ const responseHandler2Cleanup = this.backgroundMessaging.handleMessages('redirect', responseHandler);
93
+ const controlCleanup = this.control();
103
94
  return () => {
104
- this.#backgroundMessaging.close();
105
- window.removeEventListener('online', onlineHandler);
106
- window.removeEventListener('offline', onlineHandler);
107
- uncontrols();
95
+ responseHandler1Cleanup();
96
+ responseHandler2Cleanup();
97
+ controlCleanup();
108
98
  };
109
99
  }
110
100
 
@@ -128,7 +118,7 @@ export class WebfloClient extends WebfloRuntime {
128
118
  // -----------------------
129
119
  // Capture all link-clicks
130
120
  const clickHandler = (e) => {
131
- if (!this._canIntercept(e)) return;
121
+ if (!this._canIntercept(e) || e.defaultPrevented) return;
132
122
  var anchorEl = e.target.closest('a');
133
123
  if (!anchorEl || !anchorEl.href || (anchorEl.target && !anchorEl.target.startsWith('_webflo:')) || anchorEl.download || !this.isSpaRoute(anchorEl)) return;
134
124
  const resolvedUrl = new URL(anchorEl.hasAttribute('href') ? anchorEl.getAttribute('href') : '', this.location.href);
@@ -176,7 +166,7 @@ export class WebfloClient extends WebfloRuntime {
176
166
  // -----------------------
177
167
  // Capture all form-submits
178
168
  const submitHandler = (e) => {
179
- if (!this._canIntercept(e)) return;
169
+ if (!this._canIntercept(e) || e.defaultPrevented) return;
180
170
  // ---------------
181
171
  // Declare form submission modifyers
182
172
  const form = e.target.closest('form');
@@ -348,8 +338,10 @@ export class WebfloClient extends WebfloRuntime {
348
338
  cookies: scope.cookies,
349
339
  session: scope.session,
350
340
  user: scope.user,
351
- client: scope.clientMessaging
341
+ client: scope.clientMessaging,
342
+ sdk: {}
352
343
  });
344
+ await this.setup(scope.httpEvent);
353
345
  scope.httpEvent.onRequestClone = () => this.createRequest(scope.url, scope.init);
354
346
  // Ste pre-request states
355
347
  Observer.set(this.navigator, {
@@ -385,11 +377,11 @@ export class WebfloClient extends WebfloRuntime {
385
377
  });
386
378
  // ---------------
387
379
  // Response processing
388
- scope.hasBackgroundActivity = scope.clientMessaging.isMessaging() || scope.eventLifecyclePromises.size || (scope.redirectMessage && !(scope.response instanceof Response && scope.response.headers.get('Location')));
389
380
  scope.response = await this.normalizeResponse(scope.httpEvent, scope.response);
381
+ scope.hasBackgroundActivity = scope.clientMessaging.isMessaging() || scope.eventLifecyclePromises.size || (scope.redirectMessage && !scope.response.headers.get('Location'));
390
382
  if (scope.response.headers.get('Location')) {
391
383
  if (scope.redirectMessage) {
392
- scope.session.set(`redirect-message:${scope.redirectMessageID}`, scope.redirectMessage);
384
+ await scope.session.set(`redirect-message:${scope.redirectMessageID}`, scope.redirectMessage);
393
385
  }
394
386
  } else {
395
387
  if (scope.redirectMessage) {
@@ -539,8 +531,8 @@ export class WebfloClient extends WebfloRuntime {
539
531
  navigator: this.navigator,
540
532
  location: this.location,
541
533
  network: this.network, // request, redirect, error, status, remote
534
+ capabilities: this.capabilities,
542
535
  transition: this.transition,
543
- background: null
544
536
  }, { diff: true, merge });
545
537
  let overridenKeys;
546
538
  if (_isObject(data) && (overridenKeys = ['env', 'navigator', 'location', 'network', 'transition', 'background'].filter((k) => k in data)).length) {