@webqit/webflo 0.20.16 → 0.20.18

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 CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.20.16",
15
+ "version": "0.20.18",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -15,13 +15,13 @@ export class WebfloRuntime {
15
15
 
16
16
  static get Router() { return WebfloRouter; }
17
17
 
18
- static get HttpEvent() { return HttpEvent; }
18
+ static get HttpEvent() { return HttpEvent; }
19
19
 
20
- static get HttpThread() { return HttpThread; }
20
+ static get HttpThread() { return HttpThread; }
21
21
 
22
- static get HttpSession() { return HttpSession; }
22
+ static get HttpSession() { return HttpSession; }
23
23
 
24
- static get HttpUser() { return HttpUser; }
24
+ static get HttpUser() { return HttpUser; }
25
25
 
26
26
  static create(bootstrap) { return new this(bootstrap); }
27
27
 
@@ -109,7 +109,7 @@ export class WebfloRuntime {
109
109
 
110
110
  // Dispatch event
111
111
  const router = new this.constructor.Router(this, httpEvent.url.pathname);
112
- await router.route(['SETUP'], httpEvent.extend());
112
+ await router.route(['SETUP'], httpEvent.spawn());
113
113
 
114
114
  // Do proper routing for respone
115
115
  const response = await new Promise(async (resolve) => {
@@ -141,8 +141,8 @@ export class WebfloRuntime {
141
141
  : responseShim.from.value(response);
142
142
  }
143
143
 
144
- // Any "carry" data?
145
- await this.handleCarries(httpEvent, response);
144
+ // Any "status" in thread?
145
+ await this.handleThreadStatus(httpEvent, response);
146
146
 
147
147
  // Resolve now...
148
148
  if (autoLiveResponse) {
@@ -192,8 +192,11 @@ export class WebfloRuntime {
192
192
 
193
193
  // On ROOT event complete:
194
194
  // Close httpEvent.client
195
- httpEvent.lifeCycleComplete(true).then(() => {
195
+ httpEvent.lifeCycleComplete(true).then(async () => {
196
196
  httpEvent.client.close();
197
+ if (!httpEvent.thread.extended) {
198
+ await httpEvent.thread.clear();
199
+ }
197
200
  });
198
201
  }
199
202
 
@@ -205,16 +208,17 @@ export class WebfloRuntime {
205
208
  return response;
206
209
  }
207
210
 
208
- async handleCarries(httpEvent, response) {
211
+ async handleThreadStatus(httpEvent, response) {
209
212
  if (!response.headers.get('Location')) {
210
- const status = await httpEvent.thread.consume('status');
211
- //await httpEvent.thread.clear();
212
- if (!status) return;
213
+ const status = await httpEvent.thread.consume('status', true);
214
+ if (!status.length) return;
213
215
  // Fire redirect message?
214
216
  httpEvent.waitUntil(new Promise((resolve) => {
215
217
  httpEvent.client.wqLifecycle.open.then(async () => {
216
- httpEvent.client.postMessage(status, { wqEventOptions: { type: 'alert' } });
217
- resolve();
218
+ setTimeout(() => {
219
+ httpEvent.client.postMessage(status, { wqEventOptions: { type: 'alert' } });
220
+ resolve();
221
+ }, 500); // half a sec
218
222
  }, { once: true });
219
223
  }));
220
224
  }
@@ -58,20 +58,24 @@ export class WebfloClient extends WebfloRuntime {
58
58
  const instanceController = await super.initialize();
59
59
  // Bind prompt handlers
60
60
  const promptsHandler = (e) => {
61
- const message = e.data?.message
62
- ? e.data.message
63
- : e.data;
64
- const execPromp = () => {
61
+ window.queueMicrotask(() => {
65
62
  if (e.defaultPrevented) return;
66
63
  if (e.type === 'confirm') {
67
- e.wqRespondWith(confirm(message));
64
+ if (e.data?.message) {
65
+ e.wqRespondWith(confirm(e.data.message));
66
+ }
68
67
  } else if (e.type === 'prompt') {
69
- e.wqRespondWith(prompt(message));
68
+ if (e.data?.message) {
69
+ e.wqRespondWith(prompt(e.data.message));
70
+ }
70
71
  } else if (e.type === 'alert') {
71
- alert(message);
72
+ for (const item of [].concat(e.data)) {
73
+ if (item?.message) {
74
+ alert(item.message);
75
+ }
76
+ }
72
77
  }
73
- };
74
- window.queueMicrotask(execPromp);
78
+ });
75
79
  };
76
80
  this.background.addEventListener('confirm', promptsHandler, { signal: instanceController.signal });
77
81
  this.background.addEventListener('prompt', promptsHandler, { signal: instanceController.signal });
@@ -318,6 +322,7 @@ export class WebfloClient extends WebfloRuntime {
318
322
  method: null
319
323
  });
320
324
  };
325
+
321
326
  // Ping existing background processes
322
327
  // !IMPORTANT: Posting to the group when empty will keep the event until next addition
323
328
  // and we don't want that
@@ -325,6 +330,9 @@ export class WebfloClient extends WebfloRuntime {
325
330
  const url = { ...Url.copy(scopeObj.url), method: scopeObj.request.method };
326
331
  this.#background.postMessage(url, { wqEventOptions: { type: 'navigate' } });
327
332
  }
333
+
334
+ console.log('_______,', scopeObj.detail.navigationType);
335
+
328
336
  // Dispatch for response
329
337
  scopeObj.response = await this.dispatchNavigationEvent({
330
338
  httpEvent: scopeObj.httpEvent,
@@ -338,20 +346,15 @@ export class WebfloClient extends WebfloRuntime {
338
346
  clientPortB: wqMessageChannel.port2,
339
347
  originalRequestInit: scopeObj.init
340
348
  });
349
+
341
350
  // Decode response
342
351
  scopeObj.finalUrl = scopeObj.response.url || scopeObj.request.url;
343
352
  if (scopeObj.response.redirected || scopeObj.detail.navigationType === 'rdr' || scopeObj.detail.isHoisted) {
344
353
  const stateData = { ...(this.currentEntry()?.getState() || {}), redirected: true, };
345
354
  await this.updateCurrentEntry({ state: stateData }, scopeObj.finalUrl);
346
355
  }
356
+
347
357
  // Transition UI
348
- Observer.set(this.transition.from, Url.copy(this.location));
349
- Observer.set(this.transition.to, 'href', scopeObj.finalUrl);
350
- Observer.set(this.transition, 'rel', this.transition.from.pathname === this.transition.to.pathname ? 'unchanged' : (
351
- `${this.transition.from.pathname}/`.startsWith(`${this.transition.to.pathname}/`) ? 'parent' : (
352
- `${this.transition.to.pathname}/`.startsWith(`${this.transition.from.pathname}/`) ? 'child' : 'unrelated'
353
- )
354
- ));
355
358
  await this.transitionUI(async () => {
356
359
  // Set post-request states
357
360
  Observer.set(this.location, 'href', scopeObj.finalUrl);
@@ -370,26 +373,45 @@ export class WebfloClient extends WebfloRuntime {
370
373
  !(['GET'].includes(scopeObj.request.method) || scopeObj.response.redirected || scopeObj.detail.navigationType === 'rdr')
371
374
  );
372
375
  await this.applyPostRenderState(scopeObj.httpEvent);
373
- });
376
+ }, scopeObj.finalUrl, scopeObj.detail);
374
377
  }
375
378
 
376
379
  async dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB, originalRequestInit, processObj = {} }) {
377
- const response = await super.dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB });
378
- // Obtain and connect clientPortB as first thing
379
- const backgroundPort = LiveResponse.getBackground(response);
380
- if (backgroundPort) {
381
- this.background.addPort(backgroundPort);
382
- }
380
+ let response = await super.dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB });
381
+
382
+ // Extract interactive. mode handling
383
+ const handleInteractiveMode = async (resolve) => {
384
+ const liveResponse = await LiveResponse.from(response);
385
+ this.background.addPort(liveResponse.background);
386
+ liveResponse.addEventListener('replace', () => {
387
+ if (liveResponse.headers.get('Location')) {
388
+ this.processRedirect(liveResponse);
389
+ } else {
390
+ resolve?.(liveResponse);
391
+ }
392
+ }, { signal: httpEvent.signal });
393
+ return liveResponse;
394
+ };
395
+
383
396
  // Await a response with an "Accepted" or redirect status
384
397
  const statusCode = responseShim.prototype.status.get.call(response);
385
- if (statusCode === 202 || (response.headers.get('Location') && this.processRedirect(response))) {
398
+ if (statusCode === 202 && LiveResponse.hasBackground(response)) {
399
+ return new Promise(handleInteractiveMode);
400
+ }
401
+
402
+ // Handle redirects
403
+ if (response.headers.get('Location')) {
404
+ // Never resolves...
386
405
  return new Promise(async (resolve) => {
387
- if (LiveResponse.hasBackground(response)) {
388
- const liveResponse = await LiveResponse.from(response);
389
- liveResponse.addEventListener('replace', () => resolve(liveResponse), { once: true, signal: httpEvent.signal });
390
- } // Never resolves otherwise
406
+ const redirectHandlingMode = this.processRedirect(response);
407
+ // ...except processRedirect() says keep-alive
408
+ if (redirectHandlingMode === 3/* keep-alive */
409
+ && LiveResponse.hasBackground(response)) {
410
+ await handleInteractiveMode(resolve);
411
+ }
391
412
  });
392
413
  }
414
+
393
415
  // Handle "retry" directives
394
416
  if (response.headers.has('Retry-After')) {
395
417
  if (!processObj.recurseController) {
@@ -400,13 +422,21 @@ export class WebfloClient extends WebfloRuntime {
400
422
  // Ensure a previous recursion hasn't aborted the process
401
423
  if (!processObj.recurseController.signal.aborted) {
402
424
  await new Promise((res) => setTimeout(res, parseInt(response.headers.get('Retry-After')) * 1000));
403
- const eventClone = httpEvent.cloneWith({ request: this.createRequest(httpEvent.url, originalRequestInit) });
425
+ const eventClone = httpEvent.clone({ request: this.createRequest(httpEvent.url, originalRequestInit) });
404
426
  return await this.dispatchNavigationEvent({ httpEvent: eventClone, crossLayerFetch, clientPortB, originalRequestInit, processObj });
405
427
  }
406
- } else if (processObj.recurseController) {
407
- // Abort the signal. This is the end of the loop
408
- processObj.recurseController.abort();
428
+ } else {
429
+ if (processObj.recurseController) {
430
+ // Abort the signal. This is the end of the loop
431
+ processObj.recurseController.abort();
432
+ }
433
+
434
+ // Obtain and connect clientPortB as first thing
435
+ if (LiveResponse.hasBackground(response)) {
436
+ response = await handleInteractiveMode();
437
+ }
409
438
  }
439
+
410
440
  return response;
411
441
  }
412
442
 
@@ -419,39 +449,48 @@ export class WebfloClient extends WebfloRuntime {
419
449
  responseMeta.set('status', xActualRedirectCode); // @NOTE 1
420
450
  statusCode = xActualRedirectCode;
421
451
  }
452
+
422
453
  // Trigger redirect
423
454
  if ([302, 301].includes(statusCode)) {
424
455
  const location = new URL(response.headers.get('Location'), this.location.origin);
425
456
  if (this.isSpaRoute(location)) {
426
457
  this.navigate(location, {}, { navigationType: 'rdr' });
427
- } else {
428
- this.redirect(location, LiveResponse.getBackground(response));
458
+ return 1;
429
459
  }
430
- return true;
460
+ return this.redirect(location, response);
431
461
  }
462
+
463
+ return 0; // No actual redirect
432
464
  }
433
465
 
434
- redirect(location, responseBackground) {
435
- if (responseBackground) {
436
- // Redundant as this is a window reload anyways
437
- responseBackground.close();
438
- }
466
+ redirect(location) {
439
467
  window.location = location;
468
+ return 2; // Window reload
440
469
  }
441
470
 
442
- async transitionUI(updateCallback) {
443
- if (document.startViewTransition && this.withViewTransitions) {
471
+ async transitionUI(updateCallback, finalUrl, detail) {
472
+ // Set initial states
473
+ Observer.set(this.transition.from, Url.copy(this.location));
474
+ Observer.set(this.transition.to, 'href', finalUrl);
475
+ const viewTransitionRel = this.transition.from.pathname === this.transition.to.pathname ? 'same' : (
476
+ `${this.transition.from.pathname}/`.startsWith(`${this.transition.to.pathname}/`) ? 'out' : (
477
+ `${this.transition.to.pathname}/`.startsWith(`${this.transition.from.pathname}/`) ? 'in' : 'other'
478
+ )
479
+ );
480
+ Observer.set(this.transition, 'rel', viewTransitionRel);
481
+ // Trigger transition
482
+ if (document.startViewTransition && this.withViewTransitions && !detail.hasUAVisualTransition) {
444
483
  const synthesizeWhile = window.webqit?.realdom?.synthesizeWhile || ((callback) => callback());
445
484
  return new Promise(async (resolve) => {
446
485
  await synthesizeWhile(async () => {
447
- Observer.set(this.transition, 'phase', 1);
448
- const viewTransition = document.startViewTransition(updateCallback);
486
+ Observer.set(this.transition, 'phase', 'old');
487
+ const viewTransition = document.startViewTransition({ update: updateCallback, styles: ['navigation', viewTransitionRel] });
449
488
  try { await viewTransition.updateCallbackDone; } catch (e) { console.log(e); }
450
- Observer.set(this.transition, 'phase', 2);
489
+ Observer.set(this.transition, 'phase', 'new');
451
490
  try { await viewTransition.ready; } catch (e) { console.log(e); }
452
- Observer.set(this.transition, 'phase', 3);
491
+ Observer.set(this.transition, 'phase', 'start');
453
492
  try { await viewTransition.finished; } catch (e) { console.log(e); }
454
- Observer.set(this.transition, 'phase', 0);
493
+ Observer.set(this.transition, 'phase', 'end');
455
494
  resolve();
456
495
  });
457
496
  });
@@ -482,8 +521,9 @@ export class WebfloClient extends WebfloRuntime {
482
521
  transition: this.transition,
483
522
  }, { diff: true, merge });
484
523
  $response.addEventListener('replace', (e) => {
485
- if ($response.headers.get('Location') && this.processRedirect($response)) return;
486
- this.host[bindingsConfig.bindings].data = $response.body;
524
+ if (!$response.headers.get('Location')) {
525
+ this.host[bindingsConfig.bindings].data = $response.body;
526
+ }
487
527
  });
488
528
  }
489
529
  if (modulesContextAttrs) {
@@ -24,12 +24,16 @@ export class WebfloRootClient2 extends WebfloRootClient1 {
24
24
  if (!e.canIntercept
25
25
  || e.downloadRequest !== null
26
26
  || !this.isSpaRoute(e.destination.url)
27
- || e.navigationType === 'reload') return;
27
+ || ['reload'].includes(e.navigationType)) return;
28
28
  if (e.hashChange) {
29
29
  Observer.set(this.location, 'href', e.destination.url);
30
30
  return;
31
31
  }
32
- const { navigationType, destination, signal, formData, info, userInitiated } = e;
32
+ if (e.navigationType === 'replace') {
33
+ e.intercept({});
34
+ return;
35
+ }
36
+ const { navigationType, destination, signal, formData, info, userInitiated, hasUAVisualTransition } = e;
33
37
  if (formData && navigationOrigins[1]?.hasAttribute('webflo-no-intercept')) return;
34
38
  if (formData && (navigationOrigins[0] || {}).name) { formData.set(navigationOrigins[0].name, navigationOrigins[0].value); }
35
39
  // Navigation details
@@ -39,6 +43,7 @@ export class WebfloRootClient2 extends WebfloRootClient1 {
39
43
  destination,
40
44
  source: this.currentEntry(),
41
45
  userInitiated,
46
+ hasUAVisualTransition,
42
47
  info
43
48
  };
44
49
  navigationOrigins = [];
@@ -50,18 +55,11 @@ export class WebfloRootClient2 extends WebfloRootClient1 {
50
55
  body: formData,
51
56
  //signal TODO: auto-aborts on a redirect response which thus fails to parse
52
57
  };
53
- this.updateCurrentEntry({
54
- state: {
55
- ...(this.currentEntry().getState() || {}),
56
- scrollPosition: [window.scrollX, window.scrollY],
57
- }
58
- });
59
58
  const runtime = this;
60
59
  e.intercept({
61
60
  scroll: 'after-transition',
62
61
  focusReset: 'after-transition',
63
62
  async handler() {
64
- if (navigationType === 'replace') return;
65
63
  await runtime.navigate(url, init, detail);
66
64
  },
67
65
  });
@@ -90,16 +90,18 @@ export class WebfloSubClient extends WebfloClient {
90
90
  (this.host.querySelector('[autofocus]') || this.host).focus();
91
91
  }
92
92
 
93
- redirect(location, responseBackground = null) {
93
+ redirect(location, response = null) {
94
94
  location = typeof location === 'string' ? new URL(location, this.location.origin) : location;
95
95
  const width = Math.min(800, window.innerWidth);
96
96
  const height = Math.min(600, window.innerHeight);
97
97
  const left = (window.outerWidth - width) / 2;
98
98
  const top = (window.outerHeight - height) / 2;
99
99
  const popup = window.open(location, '_blank', `popup=true,width=${width},height=${height},left=${left},top=${top}`);
100
- if (responseBackground) {
100
+ if (response && LiveResponse.hasBackground(response)) {
101
101
  Observer.set(this.navigator, 'redirecting', new Url/*NOT URL*/(location), { diff: true });
102
- responseBackground.addEventListener('close', (e) => {
102
+ const backgroundPort = LiveResponse.getBackground(response);
103
+ backgroundPort.postMessage(true, { wqEventOptions: { type: 'keep-alive' } });
104
+ backgroundPort.addEventListener('close', (e) => {
103
105
  window.removeEventListener('message', windowMessageHandler);
104
106
  Observer.set(this.navigator, 'redirecting', null);
105
107
  popup.postMessage('timeout:5');
@@ -109,10 +111,12 @@ export class WebfloSubClient extends WebfloClient {
109
111
  }, { once: true });
110
112
  const windowMessageHandler = (e) => {
111
113
  if (e.source === popup && e.data === 'close') {
112
- responseBackground.close();
114
+ backgroundPort.close();
113
115
  }
114
116
  };
115
117
  window.addEventListener('message', windowMessageHandler);
116
118
  }
119
+
120
+ return 3; // keep-alive
117
121
  }
118
122
  }
@@ -50,7 +50,8 @@ export class LiveResponse extends EventTarget {
50
50
  throw new Error('Argument must be a Response instance.');
51
51
  }
52
52
 
53
- const body = await responseShim.prototype.parse.value.call(response);
53
+ let body = null;
54
+ try { body = await responseShim.prototype.parse.value.call(response); } catch(e) {}
54
55
 
55
56
  // Instance
56
57
  const instance = new this(body, {
@@ -87,11 +87,6 @@ export const request = {
87
87
  }
88
88
  },
89
89
  prototype: {
90
- carries: {
91
- get: function () {
92
- return _wq(this, 'meta').get('carries') || [];
93
- }
94
- },
95
90
  parse: { value: async function () { return await parseHttpMessage(this); } },
96
91
  clone: {
97
92
  value: function (init = {}) {
@@ -149,7 +144,6 @@ export const response = {
149
144
  : this.status);
150
145
  }
151
146
  },
152
- carry: { get: function () { return _wq(this, 'meta').get('carry'); } },
153
147
  parse: { value: async function () { return await parseHttpMessage(this); } },
154
148
  clone: {
155
149
  value: function (init = {}) {
@@ -132,7 +132,7 @@ export class HttpEvent {
132
132
  }
133
133
  //-----
134
134
  const urlRewrite = new URL(url, this.request.url);
135
- const newThread = this.thread.extend(urlRewrite.searchParams.get('_thread'));
135
+ const newThread = this.thread.spawn(urlRewrite.searchParams.get('_thread'));
136
136
  urlRewrite.searchParams.set('_thread', newThread.threadID);
137
137
  await newThread.append('back', this.request.url.replace(urlRewrite.origin, ''));
138
138
  for (const [key, value] of Object.entries(data)) {
@@ -146,9 +146,8 @@ export class HttpEvent {
146
146
  return this.constructor.create(this.#parentEvent, { ...this.#init, ...init });
147
147
  }
148
148
 
149
- extend(init = {}) {
150
- const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
151
- return instance;
149
+ spawn(init = {}) {
150
+ return this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
152
151
  }
153
152
 
154
153
  abort() {
@@ -140,7 +140,7 @@ export class HttpState {
140
140
  return new Promise(() => { });
141
141
  }
142
142
  const urlRewrite = new URL(handler.url, this.#request.url);
143
- const newThread = this.#thread.extend(urlRewrite.searchParams.get('_thread'));
143
+ const newThread = this.#thread.spawn(urlRewrite.searchParams.get('_thread')/* show */);
144
144
  urlRewrite.searchParams.set('_thread', newThread.threadID);
145
145
  await newThread.append('back', this.#request.url.replace(urlRewrite.origin, ''));
146
146
  if (handler.with) {
@@ -10,6 +10,7 @@ export class HttpThread {
10
10
  #store;
11
11
  #threadID;
12
12
  #realm;
13
+ #extended = false;
13
14
 
14
15
  get threadID() { return this.#threadID; }
15
16
 
@@ -19,7 +20,7 @@ export class HttpThread {
19
20
  this.#realm = realm;
20
21
  }
21
22
 
22
- extend(_threadID = null) {
23
+ spawn(_threadID = null) {
23
24
  return this.constructor.create({
24
25
  store: this.#store,
25
26
  threadID: _threadID,
@@ -78,4 +79,8 @@ export class HttpThread {
78
79
  await this.#store.delete(this.#threadID);
79
80
  return this;
80
81
  }
82
+
83
+ get extended() { return this.#extended; }
84
+
85
+ extend(set = true) { this.#extended = !!set; }
81
86
  }
@@ -132,7 +132,7 @@ export class WebfloRouter {
132
132
 
133
133
  // Set context parameters
134
134
  nextTick.method = request.method;
135
- nextTick.event = await thisTick.event.extend({ request });
135
+ nextTick.event = await thisTick.event.spawn({ request });
136
136
  nextTick.source = thisTick.destination.join('/');
137
137
  nextTick.destination = url.pathname.split('/').map((a) => a.trim()).filter((a) => a);
138
138
  nextTick.trail = !urlStr_isRelative ? [] : thisTick.trail.reduce((_commonRoot, _seg, i) => _commonRoot.length === i && _seg === nextTick.destination[i] ? _commonRoot.concat(_seg) : _commonRoot, []);
@@ -141,7 +141,7 @@ export class WebfloRouter {
141
141
  if (isFetch) {
142
142
  throw new Error('fetch() cannot be called without arguments!');
143
143
  }
144
- nextTick.event = thisTick.event.extend();
144
+ nextTick.event = thisTick.event.spawn();
145
145
  }
146
146
  const result = await next(nextTick);
147
147
  if (asResponse) {