@webqit/webflo 0.20.4-next.2 → 0.20.4-next.4

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 (56) hide show
  1. package/package.json +13 -34
  2. package/site/docs/concepts/realtime.md +45 -44
  3. package/site/docs/getting-started.md +40 -40
  4. package/src/{Context.js → CLIContext.js} +9 -8
  5. package/src/build-pi/esbuild-plugin-uselive-transform.js +42 -0
  6. package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +148 -142
  7. package/src/index.js +3 -1
  8. package/src/init-pi/index.js +7 -4
  9. package/src/init-pi/templates/pwa/.gitignore +6 -0
  10. package/src/init-pi/templates/pwa/.webqit/webflo/client.json +15 -0
  11. package/src/init-pi/templates/pwa/.webqit/webflo/layout.json +7 -0
  12. package/src/init-pi/templates/pwa/package.json +2 -2
  13. package/src/init-pi/templates/pwa/public/manifest.json +2 -2
  14. package/src/init-pi/templates/web/.gitignore +6 -0
  15. package/src/init-pi/templates/web/.webqit/webflo/client.json +12 -0
  16. package/src/init-pi/templates/web/.webqit/webflo/layout.json +7 -0
  17. package/src/init-pi/templates/web/package.json +2 -2
  18. package/src/runtime-pi/AppBootstrap.js +38 -0
  19. package/src/runtime-pi/WebfloRuntime.js +68 -56
  20. package/src/runtime-pi/apis.js +9 -0
  21. package/src/runtime-pi/index.js +2 -4
  22. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
  23. package/src/runtime-pi/webflo-client/WebfloClient.js +33 -36
  24. package/src/runtime-pi/webflo-client/WebfloRootClient1.js +23 -17
  25. package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
  26. package/src/runtime-pi/webflo-client/WebfloSubClient.js +14 -14
  27. package/src/runtime-pi/webflo-client/bootstrap.js +38 -0
  28. package/src/runtime-pi/webflo-client/index.js +2 -8
  29. package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
  30. package/src/runtime-pi/webflo-fetch/LiveResponse.js +154 -116
  31. package/src/runtime-pi/webflo-fetch/index.js +436 -5
  32. package/src/runtime-pi/webflo-messaging/wq-message-port.js +1 -1
  33. package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
  34. package/src/runtime-pi/webflo-routing/HttpEvent.js +12 -11
  35. package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
  36. package/src/runtime-pi/webflo-routing/WebfloRouter.js +12 -7
  37. package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
  38. package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
  39. package/src/runtime-pi/webflo-server/WebfloServer.js +138 -200
  40. package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
  41. package/src/runtime-pi/webflo-server/index.js +2 -6
  42. package/src/runtime-pi/webflo-server/webflo-devmode.js +24 -31
  43. package/src/runtime-pi/webflo-url/Url.js +1 -1
  44. package/src/runtime-pi/webflo-url/xURL.js +1 -1
  45. package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
  46. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
  47. package/src/runtime-pi/webflo-worker/bootstrap.js +39 -0
  48. package/src/runtime-pi/webflo-worker/index.js +3 -7
  49. package/src/webflo-cli.js +1 -2
  50. package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
  51. package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
  52. package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
  53. package/src/runtime-pi/webflo-fetch/headers.js +0 -151
  54. package/src/runtime-pi/webflo-fetch/message.js +0 -49
  55. package/src/runtime-pi/webflo-fetch/request.js +0 -62
  56. package/src/runtime-pi/webflo-fetch/response.js +0 -110
@@ -1,5 +1,436 @@
1
- import './formdata.js';
2
- import './request.js';
3
- import './response.js';
4
- import './LiveResponse.js';
5
- import './headers.js';
1
+ import { _isObject, _isTypeObject, _isNumeric } from '@webqit/util/js/index.js';
2
+ import { _from as _arrFrom } from '@webqit/util/arr/index.js';
3
+ import { _before, _after } from '@webqit/util/str/index.js';
4
+ import { DeepURLSearchParams } from '../webflo-url/util.js';
5
+ import { dataType } from './util.js';
6
+ import { _wq } from '../../util.js';
7
+ import { Observer } from '@webqit/use-live';
8
+ import { LiveResponse } from './LiveResponse.js';
9
+
10
+ // ----- env & globalize
11
+
12
+ export const env = {};
13
+
14
+ export function shim(prefix = 'wq') {
15
+ const apis = [Request, Response, Headers, FormData];
16
+ const descs = [request, response, headers, formData];
17
+ const patch = (api, desc) => {
18
+ const _descs = Object.fromEntries(Object.entries(desc).map(([key, value]) => {
19
+ if (prefix && key in api) {
20
+ key = `${prefix}${key[0].toUpperCase()}${key.slice(1)}`;
21
+ }
22
+ return [key, value];
23
+ }));
24
+ Object.defineProperties(api, _descs);
25
+ };
26
+ for (let i = 0; i < apis.length; i++) {
27
+ const api = apis[i];
28
+ const { prototype, ...statics } = descs[i];
29
+ patch(api, statics);
30
+ if (prototype) {
31
+ patch(api.prototype, prototype);
32
+ }
33
+ }
34
+ globalThis.LiveResponse = LiveResponse;
35
+ globalThis.Observer = Observer;
36
+ }
37
+
38
+ // ----- request
39
+
40
+ const requestOriginals = { prototype: { clone: Request.prototype.clone } };
41
+
42
+ export const request = {
43
+ from: {
44
+ value: function (url, init = {}) {
45
+ if (url instanceof Request) return url;
46
+ let $$type, $$body = init.body;
47
+ if ('body' in init) {
48
+ const { body, headers, $type } = renderHttpMessageInit(init);
49
+ init = { ...init, body, headers };
50
+ $$type = $type;
51
+ }
52
+ const instance = new Request(url, init);
53
+ const responseMeta = _wq(instance, 'meta');
54
+ responseMeta.set('body', $$body);
55
+ responseMeta.set('type', $$type);
56
+ return instance;
57
+ }
58
+ },
59
+ copy: {
60
+ value: async function (request, init = {}) {
61
+ const attrs = ['method', 'headers', 'mode', 'credentials', 'cache', 'redirect', 'referrer', 'integrity'];
62
+ const requestInit = attrs.reduce(($init, prop) => (
63
+ {
64
+ ...$init,
65
+ [prop]: prop in init
66
+ ? init[prop]
67
+ : (prop === 'headers'
68
+ ? new Headers(request[prop])
69
+ : request[prop])
70
+ }
71
+ ), {});
72
+ if (!['GET', 'HEAD'].includes(init.method?.toUpperCase() || request.method)) {
73
+ if ('body' in init) {
74
+ requestInit.body = init.body
75
+ if (!('headers' in init)) {
76
+ requestInit.headers.delete('Content-Type');
77
+ requestInit.headers.delete('Content-Length');
78
+ }
79
+ } else {
80
+ requestInit.body = await request.clone().arrayBuffer();
81
+ }
82
+ }
83
+ if (requestInit.mode === 'navigate') {
84
+ requestInit.mode = 'cors';
85
+ }
86
+ return { url: request.url, ...requestInit };
87
+ }
88
+ },
89
+ prototype: {
90
+ carries: { get: function () { return new Set(_wq(this, 'meta').get('carries') || []); } },
91
+ parse: { value: async function () { return await parseHttpMessage(this); } },
92
+ clone: {
93
+ value: function (init = {}) {
94
+ const clone = requestOriginals.prototype.clone.call(this, init);
95
+ const requestMeta = _wq(this, 'meta');
96
+ _wq(clone).set('meta', requestMeta);
97
+ return clone;
98
+ }
99
+ },
100
+ }
101
+ };
102
+
103
+ // ----- response
104
+
105
+ const responseOriginals = {
106
+ json: Response.json,
107
+ prototype: {
108
+ status: Object.getOwnPropertyDescriptor(Response.prototype, 'status'),
109
+ clone: Response.prototype.clone,
110
+ },
111
+ };
112
+
113
+ export const response = {
114
+ json: {
115
+ value: function (data, options = {}) {
116
+ const instance = responseOriginals.json(data, options);
117
+ const responseMeta = _wq(instance, 'meta');
118
+ responseMeta.set('body', data);
119
+ responseMeta.set('type', 'json');
120
+ return instance;
121
+ }
122
+ },
123
+ from: {
124
+ value: function (body, init = {}) {
125
+ if (body instanceof Response) return body;
126
+ let $type, $body = body;
127
+ if (body || body === 0) {
128
+ let headers;
129
+ ({ body, headers, $type } = renderHttpMessageInit({ body, headers: init.headers }));
130
+ init = { ...init, headers };
131
+ }
132
+ const instance = new Response(body, init);
133
+ const responseMeta = _wq(instance, 'meta');
134
+ responseMeta.set('body', $body);
135
+ responseMeta.set('type', $type);
136
+ return instance;
137
+ }
138
+ },
139
+ redirectWith: {
140
+ value: function (url, { status = 302, request = null, response = null }) {
141
+ if (typeof status !== 'string') {
142
+ throw new Error('Redirect code must be an object!');
143
+ }
144
+ if (request && !_isObject(request) || response && !_isObject(response)) {
145
+ throw new Error('Carries (redirect requests and responses) must be an object!');
146
+ }
147
+ const responseInstance = this.redirect(url, status);
148
+ if (request || response) {
149
+ const responseMeta = _wq(responseInstance, 'meta');
150
+ responseMeta.set('carry', { request, response });
151
+ }
152
+ return responseInstance;
153
+ }
154
+ },
155
+ prototype: {
156
+ status: { get: function () { return _wq(this, 'meta').get('status') || responseOriginals.prototype.status.get.call(this); } },
157
+ carry: { get: function () { return _wq(this, 'meta').get('carry'); } },
158
+ parse: { value: async function () { return await parseHttpMessage(this); } },
159
+ clone: {
160
+ value: function (init = {}) {
161
+ const clone = responseOriginals.prototype.clone.call(this, init);
162
+ const responseMeta = _wq(this, 'meta');
163
+ _wq(clone).set('meta', responseMeta);
164
+ return clone;
165
+ }
166
+ },
167
+ }
168
+ };
169
+
170
+ // ----- headers
171
+
172
+ const headersOriginals = {
173
+ set: Headers.prototype.set,
174
+ get: Headers.prototype.get,
175
+ append: Headers.prototype.append,
176
+ };
177
+
178
+ export const headers = {
179
+ set: {
180
+ value: function (name, value) {
181
+
182
+ // Format "Set-Cookie" response header
183
+ if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
184
+ value = renderCookieObjToString(value);
185
+ }
186
+
187
+ // Format "Cookie" request header
188
+ if (/Cookie/i.test(name) && _isTypeObject(value)) {
189
+ value = [].concat(value).map(renderCookieObjToString).join(';');
190
+ }
191
+
192
+ // Format "Content-Range" response header?
193
+ if (/^Content-Range$/i.test(name) && Array.isArray(value)) {
194
+ if (value.length < 2 || !value[0].includes('-')) {
195
+ throw new Error(`A Content-Range array must be in the format: [ 'start-end', 'total' ]`);
196
+ }
197
+ value = `bytes ${value.join('/')}`;
198
+ }
199
+
200
+ // Format "Range" request header?
201
+ if (/^Range$/i.test(name)) {
202
+ let rangeArr = [];
203
+ _arrFrom(value).forEach((range, i) => {
204
+ let rangeStr = Array.isArray(range) ? range.join('-') : range + '';
205
+ if (i === 0 && !rangeStr.includes('bytes=')) {
206
+ rangeStr = `bytes=${rangeStr}`;
207
+ }
208
+ rangeArr.push(rangeStr);
209
+ });
210
+ value = rangeArr.join(', ');
211
+ }
212
+
213
+ // Format "Accept" request header?
214
+ if (/^Accept$/i.test(name) && Array.isArray(value)) {
215
+ value = value.join(',');
216
+ }
217
+
218
+ return headersOriginals.set.call(this, name, value);
219
+ }
220
+ },
221
+ append: {
222
+ value: function (name, value) {
223
+
224
+ // Format "Set-Cookie" response header
225
+ if (/^Set-Cookie$/i.test(name) && _isObject(value)) {
226
+ value = renderCookieObjToString(value);
227
+ }
228
+
229
+ return headersOriginals.append.call(this, name, value);
230
+ }
231
+ },
232
+ get: {
233
+ value: function (name, parsed = false) {
234
+ let value = headersOriginals.get.call(this, name);
235
+
236
+ // Parse "Set-Cookie" response header
237
+ if (/^Set-Cookie$/i.test(name) && parsed) {
238
+ value = this.getSetCookie()/*IMPORTANT*/.map((str) => {
239
+ const [cookieDefinition, attrsStr] = str.split(';');
240
+ const [name, value] = cookieDefinition.split('=').map((s) => s.trim());
241
+ const cookieObj = { name, value: /*decodeURIComponent*/(value), };
242
+ attrsStr && attrsStr.split(/\;/g).map(attrStr => attrStr.trim().split('=')).forEach(attrsArr => {
243
+ cookieObj[attrsArr[0][0].toLowerCase() + attrsArr[0].substring(1).replace('-', '')] = attrsArr.length === 1 ? true : attrsArr[1];
244
+ });
245
+ return cookieObj;
246
+ });
247
+ }
248
+
249
+ // Parse "Cookie" request header
250
+ if (/^Cookie$/i.test(name) && parsed) {
251
+ value = value?.split(';').map((str) => {
252
+ const [name, value] = str.split('=').map((s) => s.trim());
253
+ return { name, value: /*decodeURIComponent*/(value), };
254
+ }) || [];
255
+ }
256
+
257
+ // Parse "Content-Range" response header?
258
+ if (/^Content-Range$/i.test(name) && value && parsed) {
259
+ value = _after(value, 'bytes ').split('/');
260
+ }
261
+
262
+ // Parse "Range" request header?
263
+ if (/^Range$/i.test(name) && parsed) {
264
+ value = !value ? [] : _after(value, 'bytes=').split(',').map((rangeStr) => {
265
+ const range = rangeStr.trim().split('-').map((s) => s ? parseInt(s, 10) : null);
266
+ range.render = (totalLength) => {
267
+ if (range[1] === null) {
268
+ range[1] = totalLength - 1;
269
+ }
270
+ if (range[0] === null) {
271
+ range[0] = range[1] ? totalLength - range[1] - 1 : 0;
272
+ }
273
+ return range
274
+ };
275
+ range.isValid = (currentStart, totalLength) => {
276
+ // Start higher than end or vice versa?
277
+ if (range[0] > range[1] || range[1] < range[0]) return false;
278
+ // Stretching beyond valid start/end?
279
+ if (range[0] < currentStart || range[1] > totalLength) return false;
280
+ return true;
281
+ };
282
+ return range;
283
+ });
284
+ }
285
+
286
+ // Parse "Accept" request header?
287
+ if (/^Accept$/i.test(name) && value && parsed) {
288
+ const parseSpec = (spec) => {
289
+ const [mime, q] = spec.trim().split(';').map((s) => s.trim());
290
+ return [mime, parseFloat((q || 'q=1').replace('q=', ''))];
291
+ };
292
+ const list = value.split(',')
293
+ .map((spec) => parseSpec(spec))
294
+ .sort((a, b) => a[1] > b[1] ? -1 : 1) || [];
295
+ const $value = value;
296
+ value = {
297
+ match(mime) {
298
+ if (!mime) return 0;
299
+ const splitMime = (mime) => mime.split('/').map((s) => s.trim());
300
+ const $mime = splitMime(mime + '');
301
+ return list.reduce((prev, [entry, q]) => {
302
+ if (prev) return prev;
303
+ const $entry = splitMime(entry);
304
+ return [0, 1].every((i) => (($mime[i] === $entry[i]) || $mime[i] === '*' || $entry[i] === '*')) ? q : 0;
305
+ }, 0);
306
+ },
307
+ toString() {
308
+ return $value;
309
+ }
310
+ };
311
+ }
312
+
313
+ return value;
314
+ }
315
+ }
316
+ };
317
+
318
+ // ----- formData
319
+
320
+ export const formData = {
321
+ json: { value: createFormDataFromJson },
322
+ prototype: {
323
+ json: {
324
+ value: async function (data = {}) {
325
+ const result = await renderFormDataToJson(this, ...arguments);
326
+ return result;
327
+ }
328
+ }
329
+ }
330
+ };
331
+
332
+ // ----- Utils
333
+
334
+ export function renderHttpMessageInit(httpMessageInit) {
335
+ // JSONfy headers
336
+ const headers = (httpMessageInit.headers instanceof Headers) ? [...httpMessageInit.headers.entries()].reduce((_headers, [name, value]) => {
337
+ return { ..._headers, [name/* lower-cased */]: _headers[name] ? [].concat(_headers[name], value) : value };
338
+ }, {}) : Object.keys(httpMessageInit.headers || {}).reduce((_headers, name) => {
339
+ return { ..._headers, [name.toLowerCase()]: httpMessageInit.headers[name] };
340
+ }, {});
341
+ // Process body
342
+ let body = httpMessageInit.body,
343
+ type = dataType(httpMessageInit.body);
344
+
345
+ if (['Blob', 'File'].includes(type)) {
346
+ !headers['content-type'] && (headers['content-type'] = body.type);
347
+ !headers['content-length'] && (headers['content-length'] = body.size);
348
+ } else if (['Uint8Array', 'Uint16Array', 'Uint32Array', 'ArrayBuffer'].includes(type)) {
349
+ !headers['content-length'] && (headers['content-length'] = body.byteLength);
350
+ } else if (type === 'json' && _isTypeObject(body)/*JSON object*/) {
351
+ const [_body, isJsonfiable] = createFormDataFromJson(body, true/*jsonfy*/, true/*getIsJsonfiable*/);
352
+ if (isJsonfiable) {
353
+ body = JSON.stringify(body, (k, v) => v instanceof Error ? { ...v, message: v.message } : v);
354
+ headers['content-type'] = 'application/json';
355
+ headers['content-length'] = (new Blob([body])).size;
356
+ } else {
357
+ body = _body;
358
+ type = 'FormData';
359
+ }
360
+ } else if (type === 'json'/*JSON string*/ && !headers['content-length']) {
361
+ (headers['content-length'] = (body + '').length);
362
+ }
363
+ return { body, headers, $type: type };
364
+ }
365
+
366
+ export async function parseHttpMessage(httpMessage) {
367
+ let result;
368
+ const contentType = httpMessage.headers.get('Content-Type') || '';
369
+ if (contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/form-data')) {
370
+ const fd = await httpMessage.formData();
371
+ result = fd && await formData.json.value(fd);
372
+ } else if (contentType.startsWith('application/json')/*can include charset*/) {
373
+ result = await httpMessage.json();
374
+ } else /*if (contentType === 'text/plain')*/ {
375
+ result = httpMessage.body;
376
+ }
377
+ return result;
378
+ }
379
+
380
+ // -----
381
+
382
+ export function createFormDataFromJson(data = {}, jsonfy = true, getIsJsonfiable = false) {
383
+ const formData = new FormData;
384
+ let isJsonfiable = true;
385
+ DeepURLSearchParams.reduceValue(data, '', (value, contextPath, suggestedKeys = undefined) => {
386
+ if (suggestedKeys) {
387
+ const isJson = dataType(value) === 'json';
388
+ isJsonfiable = isJsonfiable && isJson;
389
+ return isJson && suggestedKeys;
390
+ }
391
+ if (jsonfy && [true, false, null].includes(value)) {
392
+ value = new Blob([value], { type: 'application/json' });
393
+ }
394
+ formData.append(contextPath, value);
395
+ });
396
+ if (getIsJsonfiable) return [formData, isJsonfiable];
397
+ return formData;
398
+ }
399
+
400
+ export async function renderFormDataToJson(formData, jsonfy = true, getIsJsonfiable = false) {
401
+ let isJsonfiable = true;
402
+ let json;
403
+ for (let [name, value] of formData.entries()) {
404
+ if (!json) { json = _isNumeric(_before(name, '[')) ? [] : {}; }
405
+ let type = dataType(value);
406
+ if (jsonfy && ['Blob', 'File'].includes(type) && value.type === 'application/json') {
407
+ let _value = await value.text();
408
+ value = JSON.parse(_value);
409
+ type = 'json';
410
+ }
411
+ isJsonfiable = isJsonfiable && type === 'json';
412
+ DeepURLSearchParams.set(json, name, value);
413
+ }
414
+ if (getIsJsonfiable) return [json, isJsonfiable];
415
+ return json;
416
+ }
417
+
418
+ // -----
419
+
420
+ export function renderCookieObjToString(cookieObj) {
421
+ const attrsArr = [`${cookieObj.name}=${/*encodeURIComponent*/(cookieObj.value)}`];
422
+ for (const attrName in cookieObj) {
423
+ if (['name', 'value'].includes(attrName)) continue;
424
+ let _attrName = attrName[0].toUpperCase() + attrName.substring(1);
425
+ if (_attrName === 'MaxAge') { _attrName = 'Max-Age' };
426
+ attrsArr.push(cookieObj[attrName] === true ? _attrName : `${_attrName}=${cookieObj[attrName]}`);
427
+ }
428
+ return attrsArr.join(';');
429
+ }
430
+
431
+ // ----- shim
432
+
433
+ const importUrl = new URL(import.meta.url);
434
+ if (importUrl.searchParams.has('shim')) {
435
+ shim(importUrl.searchParams.get('shim')?.trim());
436
+ }
@@ -2,7 +2,7 @@ import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
2
2
  import { WQMessagePort, WQMessagePortInstanceTag } from './WQMessagePort.js';
3
3
  import { isTypeStream } from '../webflo-fetch/util.js';
4
4
  import { WQMessageEvent } from './WQMessageEvent.js';
5
- import { Observer } from '@webqit/quantum-js';
5
+ import { Observer } from '@webqit/use-live';
6
6
  import { _wq } from '../../util.js';
7
7
 
8
8
  /**
@@ -1,5 +1,5 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
- import { renderCookieObjToString } from '../webflo-fetch/cookies.js';
2
+ import { renderCookieObjToString } from '../webflo-fetch/index.js';
3
3
  import { HttpState } from './HttpState.js';
4
4
 
5
5
  export class HttpCookies extends HttpState {
@@ -1,5 +1,6 @@
1
- import { _difference } from '@webqit/util/arr/index.js';
2
1
  import { _isObject } from '@webqit/util/js/index.js';
2
+ import { _difference } from '@webqit/util/arr/index.js';
3
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
3
4
  import { xURL } from '../webflo-url/xURL.js';
4
5
  import { _wq } from '../../util.js';
5
6
 
@@ -14,9 +15,9 @@ export class HttpEvent {
14
15
  #init;
15
16
  #abortController = new AbortController;
16
17
 
17
- constructor(parentEvent, { request, cookies, session, user, realtime, sdk, detail, signal, state, ...rest }) {
18
+ constructor(parentEvent, { request, cookies, session, user, client, detail, signal, state, ...rest }) {
18
19
  this.#parentEvent = parentEvent;
19
- this.#init = { request, cookies, session, user, realtime, sdk, detail, signal, state, ...rest };
20
+ this.#init = { request, cookies, session, user, client, detail, signal, state, ...rest };
20
21
  this.#url = new xURL(this.#init.request.url);
21
22
  this.#parentEvent?.signal.addEventListener('abort', () => this.#abortController.abort(), { once: true });
22
23
  this.#init.request.signal?.addEventListener('abort', () => this.#abortController.abort(), { once: true });
@@ -29,7 +30,7 @@ export class HttpEvent {
29
30
 
30
31
  get request() { return this.#init.request; }
31
32
 
32
- get realtime() { return this.#init.realtime; }
33
+ get client() { return this.#init.client; }
33
34
 
34
35
  get cookies() { return this.#init.cookies; }
35
36
 
@@ -37,8 +38,6 @@ export class HttpEvent {
37
38
 
38
39
  get user() { return this.#init.user; }
39
40
 
40
- get sdk() { return this.#init.sdk; }
41
-
42
41
  get detail() { return this.#init.detail; }
43
42
 
44
43
  get signal() { return this.#abortController.signal; }
@@ -46,6 +45,8 @@ export class HttpEvent {
46
45
  get state() { return { ...(this.#init.state || {}) }; }
47
46
 
48
47
  #lifecyclePromises = new Set;
48
+ get lifecyclePromises() { return this.#lifecyclePromises; }
49
+
49
50
  #lifeCycleResolve;
50
51
  #lifeCycleReject;
51
52
  #lifeCycleResolutionPromise = new Promise((resolve, reject) => {
@@ -81,12 +82,12 @@ export class HttpEvent {
81
82
  && !this.#lifecyclePromises.size;
82
83
  }
83
84
 
84
- waitUntil(promise) {
85
- return this.#extendLifecycle(promise);
85
+ async waitUntil(promise) {
86
+ return await this.#extendLifecycle(promise);
86
87
  }
87
88
 
88
89
  waitUntilNavigate() {
89
- return this.waitUntil(new Promise(() => { }));
90
+ this.waitUntil(new Promise(() => { }));
90
91
  }
91
92
 
92
93
  #internalLiveResponse = new LiveResponse(null, { done: false });
@@ -101,8 +102,8 @@ export class HttpEvent {
101
102
  }
102
103
 
103
104
  extend(init = {}) {
104
- const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...init });
105
- this.#extendLifecycle(instance.lifeCycleComplete(true));
105
+ const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
106
+ if (init !== false) this.#extendLifecycle(instance.lifeCycleComplete(true));
106
107
  return instance;
107
108
  }
108
109
 
@@ -2,19 +2,19 @@ import { HttpState } from './HttpState.js';
2
2
 
3
3
  export class HttpUser extends HttpState {
4
4
 
5
- static create({ store, request, realtime, session }) {
6
- return new this({ store, request, realtime, session });
5
+ static create({ store, request, client, session }) {
6
+ return new this({ store, request, client, session });
7
7
  }
8
8
 
9
- #realtime;
9
+ #client;
10
10
 
11
- constructor({ store, request, realtime, session }) {
11
+ constructor({ store, request, client, session }) {
12
12
  super({
13
13
  store,
14
14
  request,
15
15
  session
16
16
  });
17
- this.#realtime = realtime;
17
+ this.#client = client;
18
18
  }
19
19
 
20
20
  async isSignedIn() {
@@ -34,7 +34,7 @@ export class HttpUser extends HttpState {
34
34
 
35
35
  async confirm(data, callback, options = {}) {
36
36
  return await new Promise((resolve) => {
37
- this.#realtime.postRequest(
37
+ this.#client.postRequest(
38
38
  data,
39
39
  (event) => resolve(callback ? callback(event) : event),
40
40
  { ...options, wqEventOptions: { type: 'confirm' } }
@@ -44,7 +44,7 @@ export class HttpUser extends HttpState {
44
44
 
45
45
  async prompt(data, callback, options = {}) {
46
46
  return await new Promise((resolve) => {
47
- this.#realtime.postRequest(
47
+ this.#client.postRequest(
48
48
  data,
49
49
  (event) => resolve(callback ? callback(event) : event),
50
50
  { ...options, wqEventOptions: { type: 'prompt' } }
@@ -1,8 +1,7 @@
1
- import { State } from '@webqit/quantum-js';
2
1
  import { _isFunction, _isArray, _isObject } from '@webqit/util/js/index.js';
3
2
  import { _from as _arrFrom } from '@webqit/util/arr/index.js';
3
+ import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
4
4
  import { path as Path } from '../webflo-url/util.js';
5
- import { isGenerator } from '../webflo-fetch/LiveResponse.js';
6
5
 
7
6
  export class WebfloRouter {
8
7
 
@@ -170,11 +169,11 @@ export class WebfloRouter {
170
169
  const returnValue = await handler.call(thisContext, thisTick.event, $next/*next*/, $fetch/*fetch*/);
171
170
 
172
171
  // Handle cleanup on abort
173
- if (returnValue instanceof State) {
172
+ if (LiveResponse.test(returnValue) === 'LiveMode') {
174
173
  thisTick.event.signal.addEventListener('abort', () => {
175
- returnValue.dispose();
174
+ returnValue.abort();
176
175
  });
177
- } else if (isGenerator(returnValue)) {
176
+ } else if (LiveResponse.test(returnValue) === 'Generator') {
178
177
  thisTick.event.signal.addEventListener('abort', () => {
179
178
  if (typeof returnValue.return === 'function') {
180
179
  returnValue.return();
@@ -187,13 +186,19 @@ export class WebfloRouter {
187
186
  resolved = 2;
188
187
  resolve(returnValue);
189
188
  } else if (typeof returnValue !== 'undefined') {
190
- await thisTick.event.internalLiveResponse.replaceWith(returnValue, { done: true });
189
+ thisTick.event.internalLiveResponse.replaceWith(returnValue, { done: true });
191
190
  }
192
191
  });
193
192
  }
193
+ let returnValue;
194
194
  if (_default) {
195
- return await _default.call(thisContext, thisTick.event, remoteFetch);
195
+ returnValue = await _default.call(thisContext, thisTick.event, remoteFetch);
196
196
  }
197
+ try {
198
+ // IMPORTANT: Explicitly terminate the event lifecycle if nothing extends it
199
+ await thisTick.event.waitUntil();
200
+ } catch(e) {}
201
+ return returnValue;
197
202
  };
198
203
 
199
204
  return next({
@@ -1,10 +1,12 @@
1
+ import { headers as headersShim } from '../webflo-fetch/index.js';
1
2
  import { HttpCookies } from '../webflo-routing/HttpCookies.js';
2
3
 
3
4
  export class ServerSideCookies extends HttpCookies {
4
5
  static create({ request }) {
6
+ const cookies = headersShim.get.value.call(request.headers, 'Cookie', true);
5
7
  return new this({
6
8
  request,
7
- entries: request.headers.get('Cookie', true).map((c) => [c.name, c])
9
+ entries: cookies.map((c) => [c.name, c])
8
10
  });
9
11
  }
10
12
 
@@ -1,4 +1,5 @@
1
1
  import { HttpSession } from '../webflo-routing/HttpSession.js';
2
+ import { headers as headersShim } from '../webflo-fetch/index.js';
2
3
 
3
4
  export class ServerSideSession extends HttpSession {
4
5
 
@@ -29,7 +30,7 @@ export class ServerSideSession extends HttpSession {
29
30
  }
30
31
 
31
32
  async commit(response = null, devMode = false) {
32
- if (response && !response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
33
+ if (response && !headersShim.get.value.call(response.headers, 'Set-Cookie', true).find((c) => c.name === '__sessid')) {
33
34
  // expires six months
34
35
  response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; ${!devMode ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${this.#ttl ? `; Max-Age=${this.#ttl}` : ''}`);
35
36
  }