@webqit/webflo 1.0.18 → 1.0.20

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 +4 -3
  2. package/src/config-pi/runtime/Client.js +50 -46
  3. package/src/config-pi/runtime/Server.js +37 -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 +35 -36
  7. package/src/runtime-pi/WebfloCookieStorage.js +8 -8
  8. package/src/runtime-pi/WebfloRouter.js +4 -3
  9. package/src/runtime-pi/WebfloRuntime.js +27 -19
  10. package/src/runtime-pi/WebfloStorage.js +47 -16
  11. package/src/runtime-pi/client/Capabilities.js +211 -0
  12. package/src/runtime-pi/client/CookieStorage.js +2 -2
  13. package/src/runtime-pi/client/SessionStorage.js +2 -2
  14. package/src/runtime-pi/client/WebfloClient.js +17 -25
  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 +2 -2
  22. package/src/runtime-pi/client/worker/SessionStorage.js +1 -1
  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 +2 -2
  26. package/src/runtime-pi/server/SessionStorage.js +3 -3
  27. package/src/runtime-pi/server/WebfloServer.js +32 -13
  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
@@ -7,9 +7,12 @@ export class WebfloStorage extends Map {
7
7
  #session;
8
8
 
9
9
  constructor(request, session, iterable = []) {
10
- super(iterable);
10
+ super();
11
11
  this.#request = request;
12
12
  this.#session = session === true ? this : session;
13
+ for (const [k, v] of iterable) {
14
+ this.set(k, v);
15
+ }
13
16
  }
14
17
 
15
18
  #originals;
@@ -29,18 +32,52 @@ export class WebfloStorage extends Map {
29
32
  });
30
33
  }
31
34
 
32
- commit() {
35
+ async commit() {
33
36
  this.saveOriginals();
34
37
  }
35
38
 
39
+ #listeners = new Set;
40
+ observe(attr, handler) {
41
+ const args = { attr, handler };
42
+ this.#listeners.add(args);
43
+ return () => {
44
+ this.#listeners.delete(args);
45
+ }
46
+ }
47
+
48
+ async emit(attr, value) {
49
+ const returnValues = [];
50
+ for (const { attr: $attr, handler } of this.#listeners) {
51
+ if (arguments.length && $attr !== attr) continue;
52
+ if (arguments.length > 1) {
53
+ returnValues.push(handler(value));
54
+ } else {
55
+ returnValues.push(handler());
56
+ }
57
+ }
58
+ return Promise.all(returnValues);
59
+ }
60
+
61
+ async set(attr, value) {
62
+ const returnValue = super.set(attr, value);
63
+ await this.emit(attr, value);
64
+ return returnValue;
65
+ }
66
+
67
+ async delete(attr) {
68
+ const returnValue = super.delete(attr);
69
+ await this.emit(attr);
70
+ return returnValue;
71
+ }
72
+
73
+ async clear() {
74
+ const returnValue = super.clear();
75
+ await this.emit();
76
+ return returnValue;
77
+ }
78
+
36
79
  #handlers = new Map;
37
- #reverseHandlers = new Map;
38
80
  defineHandler(attr, ...handlers) {
39
- let registry = this.#handlers;
40
- if (handlers[0] === false) {
41
- registry = this.#reverseHandlers;
42
- handlers.shift();
43
- }
44
81
  const $handlers = [];
45
82
  for (let handler of handlers) {
46
83
  if (typeof handler === 'function') {
@@ -52,17 +89,11 @@ export class WebfloStorage extends Map {
52
89
  }
53
90
  $handlers.push(handler);
54
91
  }
55
- registry.set(attr, $handlers);
56
- }
57
-
58
- defineReverseHandler(attr, ...handlers) {
59
- return this.defineHandler(attr, false, ...handlers);
92
+ this.#handlers.set(attr, $handlers);
60
93
  }
61
94
 
62
95
  getHandlers() { return this.#handlers; }
63
96
 
64
- getReverseHandlers() { return this.#reverseHandlers; }
65
-
66
97
  async require(attrs, callback = null, noNulls = false) {
67
98
  const entries = [];
68
99
  main: for await (const attr of [].concat(attrs)) {
@@ -94,7 +125,7 @@ export class WebfloStorage extends Map {
94
125
  }
95
126
  const messageID = (0 | Math.random() * 9e6).toString(36);
96
127
  urlRewrite.searchParams.set('redirect-message', messageID);
97
- this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
128
+ await this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
98
129
  }
99
130
  return new Response(null, { status: 302, headers: {
100
131
  Location: urlRewrite
@@ -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.app_public_webhook_url = instance.#params.app_public_webhook_url_variable && instance.#params.env[instance.#params.app_public_webhook_url_variable];
16
+ instance.#params.app_vapid_public_key = instance.#params.app_vapid_public_key_variable && instance.#params.env[instance.#params.app_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.app_public_webhook_url) {
30
+ // --------
31
+ // app.installed
32
+ const onappinstalled = () => {
33
+ fetch(instance.#params.app_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.app_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.app_vapid_public_key) {
188
+ params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.app_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) {
11
+ async commit(response) {
12
12
  for (const cookieStr of this.render()) {
13
13
  document.cookie = cookieStr;
14
14
  }
15
- super.commit();
15
+ await super.commit();
16
16
  }
17
17
  }
@@ -20,7 +20,7 @@ export class SessionStorage extends WebfloStorage {
20
20
  super(request, true, iterable);
21
21
  }
22
22
 
23
- commit() {
23
+ async commit() {
24
24
  const storeType = this.constructor.type === 'user' ? 'localStorage' : 'sessionStorage';
25
25
  for (const key of this.getAdded()) {
26
26
  window[storeType].setItem(key, this.get(key));
@@ -28,6 +28,6 @@ export class SessionStorage extends WebfloStorage {
28
28
  for (const key of this.getDeleted()) {
29
29
  window[storeType].removeItem(key);
30
30
  }
31
- super.commit();
31
+ await super.commit();
32
32
  }
33
33
  }
@@ -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, {
@@ -369,7 +361,7 @@ export class WebfloClient extends WebfloRuntime {
369
361
  scope.context = {};
370
362
  if (window.webqit?.oohtml?.configs) {
371
363
  const { BINDINGS_API: { api: bindingsConfig } = {}, } = window.webqit.oohtml.configs;
372
- scope.context = this.host[bindingsConfig.bindings].data || {};
364
+ scope.context = this.host[bindingsConfig.bindings] || {};
373
365
  }
374
366
  if (scope.request.method === 'GET' || (scope.request.method === 'POST' && scope.url.pathname !== this.location.pathname)) {
375
367
  // Ping existing background process
@@ -385,11 +377,11 @@ export class WebfloClient extends WebfloRuntime {
385
377
  });
386
378
  // ---------------
387
379
  // Response processing
388
- scope.hasBackgroundActivity = 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) {
@@ -436,7 +428,7 @@ export class WebfloClient extends WebfloRuntime {
436
428
  // Only render now
437
429
  if ([202/*Accepted*/, 304/*Not Modified*/].includes(scope.response.status)) {
438
430
  if (scope.backgroundMessaging) {
439
- scope.backgroundMessaging.addEventListener('response', () => {
431
+ scope.backgroundMessaging.addEventListener('response', (e) => {
440
432
  scope.resetStates();
441
433
  });
442
434
  return;
@@ -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) {
@@ -1,6 +1,7 @@
1
1
  import { WebfloClient } from './WebfloClient.js';
2
2
  import { Context } from './Context.js';
3
3
  import { Workport } from './Workport.js';
4
+ import { Capabilities } from './Capabilities.js';
4
5
 
5
6
  const { Observer } = webqit;
6
7
 
@@ -10,6 +11,8 @@ export class WebfloRootClient1 extends WebfloClient {
10
11
 
11
12
  static get Workport() { return Workport; }
12
13
 
14
+ static get Capabilities() { return Capabilities; }
15
+
13
16
  static create(host, cx = {}) {
14
17
  return new this(host, this.Context.create(cx));
15
18
  }
@@ -17,6 +20,15 @@ export class WebfloRootClient1 extends WebfloClient {
17
20
  #cx;
18
21
  get cx() { return this.#cx; }
19
22
 
23
+ #network;
24
+ get network() { return this.#network; }
25
+
26
+ #workport;
27
+ get workport() { return this.#workport; }
28
+
29
+ #capabilities;
30
+ get capabilities() { return this.#capabilities; }
31
+
20
32
  constructor(host, cx) {
21
33
  if (!(host instanceof Document)) {
22
34
  throw new Error('Argument #1 must be a Document instance');
@@ -26,11 +38,42 @@ export class WebfloRootClient1 extends WebfloClient {
26
38
  throw new Error('Argument #2 must be a Webflo Context instance');
27
39
  }
28
40
  this.#cx = cx;
41
+ this.#network = { status: window.navigator.onLine };
29
42
  }
30
43
 
31
- initialize() {
32
- // Main initializations
33
- let undoControl = super.initialize();
44
+ async initialize() {
45
+ // --------
46
+ // INITIALIZATIONS
47
+ const cleanups = [await super.initialize()];
48
+ // --------
49
+ // Service Worker && Capabilities
50
+ if (this.cx.params.capabilities?.service_worker?.filename) {
51
+ const { service_worker: { filename, ...restServiceWorkerParams } = {} } = this.cx.params.capabilities;
52
+ this.#workport = await this.constructor.Workport.initialize(null, (this.cx.params.public_base_url || '') + filename, restServiceWorkerParams);
53
+ cleanups.push(() => this.#workport.close());
54
+ }
55
+ this.#capabilities = await this.constructor.Capabilities.initialize({ ...this.cx.params.capabilities, env: this.cx.params.env });
56
+ cleanups.push(() => this.#capabilities.close());
57
+ // --------
58
+ // Bind network status handlers
59
+ const onlineHandler = () => Observer.set(this.network, 'status', window.navigator.onLine);
60
+ window.addEventListener('online', onlineHandler);
61
+ window.addEventListener('offline', onlineHandler);
62
+ cleanups.push(() => {
63
+ window.removeEventListener('online', onlineHandler);
64
+ window.removeEventListener('offline', onlineHandler);
65
+ });
66
+ // --------
67
+ // Window opener pinging
68
+ let beforeunloadCleanup;
69
+ if (window.opener) {
70
+ const beforeunloadHandler = () => window.opener.postMessage('close');
71
+ window.addEventListener('beforeunload', beforeunloadHandler);
72
+ cleanups.push(() => {
73
+ window.removeEventListener('beforeunload', beforeunloadHandler);
74
+ });
75
+ }
76
+ // --------
34
77
  // Bind global prompt handlers
35
78
  const promptsHandler = (e) => {
36
79
  e.stopPropagation();
@@ -49,9 +92,10 @@ export class WebfloRootClient1 extends WebfloClient {
49
92
  });
50
93
  }, 10);
51
94
  };
52
- this.backgroundMessaging.handleMessages('confirm', promptsHandler);
53
- this.backgroundMessaging.handleMessages('prompt', promptsHandler);
54
- // Respond to background activity request at pageload
95
+ cleanups.push(this.backgroundMessaging.handleMessages('confirm', promptsHandler));
96
+ cleanups.push(this.backgroundMessaging.handleMessages('prompt', promptsHandler));
97
+ // --------
98
+ // HYDRATION
55
99
  const scope = {};
56
100
  if (scope.backgroundMessagingMeta = document.querySelector('meta[name="X-Background-Messaging"]')) {
57
101
  scope.backgroundMessaging = this.$createBackgroundMessagingFrom(scope.backgroundMessagingMeta.content);
@@ -66,32 +110,9 @@ export class WebfloRootClient1 extends WebfloClient {
66
110
  });
67
111
  } catch(e) {}
68
112
  }
69
- // Service Worker && COMM
70
- if (this.cx.params.service_worker?.filename) {
71
- const { public_base_url: base, service_worker: { filename, ...restServiceWorkerParams } } = this.cx.params;
72
- const { vapid_key_env, push_registration_url_env, ..._restServiceWorkerParams } = restServiceWorkerParams;
73
- const swParams = {
74
- ..._restServiceWorkerParams,
75
- VAPID_PUBLIC_KEY: this.cx.params.env[vapid_key_env],
76
- PUSH_REGISTRATION_PUBLIC_URL: this.cx.params.env[push_registration_url_env],
77
- startMessages: true
78
- };
79
- this.workport = new this.constructor.Workport;
80
- this.workport.registerServiceWorker(base + filename, swParams);
81
- }
82
- if (window.opener) {
83
- // Window opener pinging
84
- const $undoControl = undoControl;
85
- const beforeunloadHandler = () => {
86
- window.opener.postMessage('close');
87
- };
88
- window.addEventListener('beforeunload', beforeunloadHandler);
89
- undoControl = () => {
90
- window.removeEventListener('beforeunload', beforeunloadHandler);
91
- $undoControl();
92
- };
93
- }
94
- return undoControl
113
+ // --------
114
+ // CLEAN UP
115
+ return () => cleanups.forEach((c) => c());
95
116
  }
96
117
 
97
118
  /**
@@ -102,7 +123,7 @@ export class WebfloRootClient1 extends WebfloClient {
102
123
 
103
124
  control() {
104
125
  // IMPORTANT: we're calling super.controlClassic()
105
- const undoControl = super.controlClassic((newHref) => {
126
+ const cleanupSuper = super.controlClassic((newHref) => {
106
127
  try {
107
128
  // Save current scroll position
108
129
  window.history.replaceState({
@@ -134,7 +155,7 @@ export class WebfloRootClient1 extends WebfloClient {
134
155
  window.addEventListener('popstate', popstateHandler);
135
156
  return () => {
136
157
  this.host.removeEventListener('popstate', popstateHandler);
137
- undoControl();
158
+ cleanupSuper();
138
159
  };
139
160
  }
140
161
 
@@ -9,14 +9,14 @@ export class WebfloRootClient2 extends WebfloRootClient1 {
9
9
  let navigationOrigins = [];
10
10
  // Capture all link-clicks
11
11
  const clickHandler = (e) => {
12
- if (!this._canIntercept(e)) return;
12
+ if (!this._canIntercept(e) || e.defaultPrevented) return;
13
13
  let anchorEl = e.target.closest('a');
14
14
  if (!anchorEl || !anchorEl.href || anchorEl.target) return;
15
15
  navigationOrigins = [anchorEl, null, anchorEl.closest('[navigationcontext]')];
16
16
  };
17
17
  // Capture all form-submits
18
18
  const submitHandler = (e) => {
19
- if (!this._canIntercept(e)) return;
19
+ if (!this._canIntercept(e) || e.defaultPrevented) return;
20
20
  navigationOrigins = [e.submitter, e.target.closest('form'), e.target.closest('[navigationcontext]')];
21
21
  };
22
22
  // Handle navigation event which happens after the above
@@ -50,9 +50,9 @@ export class WebfloSubClient extends WebfloClient {
50
50
  this.location = newValue;
51
51
  }
52
52
 
53
- connectedCallback() {
53
+ async connectedCallback() {
54
54
  this.#superRuntime = (this.parentNode?.closest(embedTagNames) || document).webfloRuntime;
55
- this.#webfloControllerUninitialize = WebfloSubClient.create(this, this.#superRuntime).initialize();
55
+ this.#webfloControllerUninitialize = await WebfloSubClient.create(this, this.#superRuntime).initialize();
56
56
  }
57
57
 
58
58
  disconnectedCallback() {
@@ -70,8 +70,12 @@ export class WebfloSubClient extends WebfloClient {
70
70
 
71
71
  get cx() { return this.#superRuntime.cx; }
72
72
 
73
+ get network() { return this.#superRuntime.network; }
74
+
73
75
  get workport() { return this.#superRuntime.workport; }
74
76
 
77
+ get capabilities() { return this.#superRuntime.capabilities; }
78
+
75
79
  get withViewTransitions() { return this.host.hasAttribute('viewtransitions'); }
76
80
 
77
81
  constructor(host, superRuntime) {
@@ -85,11 +89,11 @@ export class WebfloSubClient extends WebfloClient {
85
89
  this.#superRuntime = superRuntime;
86
90
  }
87
91
 
88
- initialize() {
92
+ async initialize() {
89
93
  if (this.host.location.origin !== window.location.origin) {
90
94
  throw new Error(`Webflo embeddable origin violation in "${window.location}"`);
91
95
  }
92
- const uncontrols = super.initialize();
96
+ const cleanupSuper = await super.initialize();
93
97
  this.backgroundMessaging.setParent(this.#superRuntime.backgroundMessaging);
94
98
  if (this.host.getAttribute('location')) {
95
99
  this.navigate(this.location.href);
@@ -98,7 +102,7 @@ export class WebfloSubClient extends WebfloClient {
98
102
  if (this.backgroundMessaging.parentNode === this.#superRuntime.backgroundMessaging) {
99
103
  this.backgroundMessaging.setParent(null);
100
104
  }
101
- uncontrols();
105
+ cleanupSuper();
102
106
  };
103
107
  }
104
108