@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
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "1.0.19",
15
+ "version": "1.0.21",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -35,18 +35,21 @@
35
35
  "webflo-certbot-http-cleanup-hook": "src/services-pi/cert/http-cleanup-hook.js"
36
36
  },
37
37
  "dependencies": {
38
+ "@linked-db/linked-ql": "^0.30.13",
38
39
  "@octokit/webhooks": "^7.15.1",
39
- "@webqit/backpack": "^0.1.6",
40
+ "@webqit/backpack": "^0.1.8",
40
41
  "@webqit/observer": "^2.0.7",
41
42
  "@webqit/oohtml-ssr": "^2.1.1",
42
43
  "@webqit/util": "^0.8.11",
43
- "client-sessions": "^0.8.0",
44
44
  "esbuild": "^0.14.38",
45
+ "ioredis": "^5.5.0",
45
46
  "jsdom": "^21.1.1",
46
47
  "mime-types": "^2.1.33",
48
+ "pg": "^8.13.3",
47
49
  "simple-git": "^2.20.1",
48
50
  "stream-slice": "^0.1.2",
49
- "urlpattern-polyfill": "^4.0.3"
51
+ "urlpattern-polyfill": "^4.0.3",
52
+ "web-push": "^3.6.7"
50
53
  },
51
54
  "devDependencies": {
52
55
  "chai": "^4.3.6",
@@ -19,31 +19,34 @@ export default class Client extends Dotfile {
19
19
  // Defaults merger
20
20
  withDefaults(config) {
21
21
  return this.merge({
22
+ spa_routing: true,
22
23
  bundle_filename: 'bundle.js',
23
24
  public_base_url: '/',
24
- spa_routing: true,
25
- service_worker: {
26
- filename: 'worker.js',
27
- scope: '/',
28
- support_push: false,
29
- vapid_key_env: 'VAPID_PUBLIC_KEY',
30
- push_registration_url_env: 'PUSH_REGISTRATION_PUBLIC_URL',
25
+ copy_public_variables: true,
26
+ capabilities: {
27
+ service_worker: true,
28
+ webpush: false,
29
+ custom_install: false,
30
+ exposed: ['display-mode', 'notifications'],
31
+ vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
32
+ generic_public_webhook_url_variable: 'GENERIC_PUBLIC_WEBHOOK_URL',
31
33
  },
32
- bundle_public_env: false,
33
34
  }, config, 'patch');
34
35
  }
35
36
 
36
37
  // Questions generator
37
38
  getSchema(config, choices = {}) {
38
- // Choices
39
- const CHOICES = this.merge({
40
- webqit_dependencies: [
41
- {value: 'externalize', title: 'Externalize'},
42
- {value: 'internalize', title: 'Internalize'},
43
- ],
44
- }, choices, 'patch');
45
39
  // Questions
46
40
  return [
41
+ {
42
+ name: 'spa_routing',
43
+ type: 'toggle',
44
+ message: '[spa_routing]: Enable Single Page Routing Mode',
45
+ active: 'YES',
46
+ inactive: 'NO',
47
+ initial: config.spa_routing,
48
+ validation: ['important'],
49
+ },
47
50
  {
48
51
  name: 'bundle_filename',
49
52
  type: 'text',
@@ -58,59 +61,60 @@ export default class Client extends Dotfile {
58
61
  validation: ['important'],
59
62
  },
60
63
  {
61
- name: 'bundle_public_env',
64
+ name: 'copy_public_variables',
62
65
  type: 'toggle',
63
- message: '[spa_routing]: Enable Single Page Routing Mode',
66
+ message: '[copy_public_variables]: Bundle public ENV variables?',
64
67
  active: 'YES',
65
68
  inactive: 'NO',
66
- initial: config.spa_routing,
69
+ initial: config.copy_public_variables,
67
70
  validation: ['important'],
68
71
  },
69
72
  {
70
- name: 'service_worker',
73
+ name: 'capabilities',
71
74
  controls: {
72
- name: 'service_worker',
75
+ name: 'capabilities',
73
76
  },
74
- initial: config.service_worker,
77
+ initial: config.capabilities,
75
78
  schema: [
76
79
  {
77
- name: 'filename',
78
- type: 'text',
79
- message: 'Specify the Service Worker filename',
80
+ name: 'service_worker',
81
+ type: 'toggle',
82
+ message: 'Enable service worker?',
83
+ active: 'YES',
84
+ inactive: 'NO',
80
85
  },
81
86
  {
82
- name: 'scope',
83
- type: 'text',
84
- message: 'Specify the Service Worker scope',
87
+ name: 'webpush',
88
+ type: 'toggle',
89
+ message: 'Support push-notifications?',
90
+ active: 'YES',
91
+ inactive: 'NO',
85
92
  },
86
93
  {
87
- name: 'support_push',
94
+ name: 'custom_install',
88
95
  type: 'toggle',
89
- message: 'Support push-notifications?',
96
+ message: 'Enable custom PWA install prompt?',
90
97
  active: 'YES',
91
98
  inactive: 'NO',
92
99
  },
93
100
  {
94
- name: 'vapid_key_env',
95
- type: (prev, answers) => answers.support_push ? 'text' : null,
96
- message: 'Enter the VAPID KEY env id for push notification subscription',
101
+ name: 'exposed',
102
+ type: 'list',
103
+ message: 'Specify features exposed on capabilities.exposed',
104
+ initial: (config.exposed || []).join(', '),
97
105
  },
98
106
  {
99
- name: 'push_registration_url_env',
100
- type: (prev, answers) => answers.support_push ? 'text' : null,
101
- message: 'Enter the URL for push notification subscription',
107
+ name: 'vapid_public_key_variable',
108
+ type: (prev, answers) => !answers.webpush ? null : 'text',
109
+ message: 'Enter the environment variable name for APP_VAPID_PUBLIC_KEY if not as written',
102
110
  },
103
- ],
104
- },
105
- {
106
- name: 'bundle_public_env',
107
- type: 'toggle',
108
- message: '[bundle_public_env]: Bundle public ENV variables?',
109
- active: 'YES',
110
- inactive: 'NO',
111
- initial: config.bundle_public_env,
112
- validation: ['important'],
113
- },
111
+ {
112
+ name: 'generic_public_webhook_url_variable',
113
+ type: 'text',
114
+ message: 'Enter the environment variable name for GENERIC_PUBLIC_WEBHOOK_URL if not as written',
115
+ },
116
+ ]
117
+ }
114
118
  ];
115
119
  }
116
120
  }
@@ -29,7 +29,18 @@ export default class Server extends Dotfile {
29
29
  force: false,
30
30
  },
31
31
  force_www: '',
32
- oohtml_support: 'full',
32
+ session_key_variable: 'SESSION_KEY',
33
+ capabilities: {
34
+ database: false,
35
+ database_dialect: 'postgres',
36
+ database_url_variable: 'DATABASE_URL',
37
+ redis: false,
38
+ redis_url_variable: 'REDIS_URL',
39
+ webpush: false,
40
+ vapid_subject: 'mailto:foo@example.com',
41
+ vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
42
+ vapid_private_key_variable: 'VAPID_PRIVATE_KEY',
43
+ },
33
44
  }, config, 'patch');
34
45
  }
35
46
 
@@ -42,13 +53,6 @@ export default class Server extends Dotfile {
42
53
  {value: 'add',},
43
54
  {value: 'remove',},
44
55
  ],
45
- oohtml_support: [
46
- {value: 'full', title: 'full'},
47
- {value: 'namespacing', title: 'namespacing'},
48
- {value: 'scripting', title: 'scripting'},
49
- {value: 'templating', title: 'templating'},
50
- {value: 'none', title: 'none'},
51
- ],
52
56
  }, choices, 'patch');
53
57
  // Questions
54
58
  return [
@@ -63,6 +67,7 @@ export default class Server extends Dotfile {
63
67
  name: 'domains',
64
68
  type: 'list',
65
69
  message: '[domains]: Enter a list of allowed domains if necessary (comma-separated)',
70
+ initial: (config.domains || []).join(', '),
66
71
  validation: ['important'],
67
72
  },
68
73
  {
@@ -72,6 +77,12 @@ export default class Server extends Dotfile {
72
77
  choices: CHOICES.force_www,
73
78
  initial: this.indexOfInitial(CHOICES.force_www, config.force_www),
74
79
  },
80
+ {
81
+ name: 'session_key_variable',
82
+ type: 'text',
83
+ message: 'Enter the environment variable name for SESSION_KEY if not as written',
84
+ initial: config.session_key_variable,
85
+ },
75
86
  {
76
87
  name: 'https',
77
88
  controls: {
@@ -113,12 +124,64 @@ export default class Server extends Dotfile {
113
124
  ],
114
125
  },
115
126
  {
116
- name: 'oohtml_support',
117
- type: 'select',
118
- message: '[oohtml_support]: Specify OOHTML support level',
119
- choices: CHOICES.oohtml_support,
120
- initial: this.indexOfInitial(CHOICES.oohtml_support, config.oohtml_support),
121
- validation: ['important'],
127
+ name: 'capabilities',
128
+ controls: {
129
+ name: 'capabilities',
130
+ },
131
+ initial: config.capabilities,
132
+ schema: [
133
+ {
134
+ name: 'database',
135
+ type: 'toggle',
136
+ message: 'Add database integration?',
137
+ active: 'YES',
138
+ inactive: 'NO',
139
+ },
140
+ {
141
+ name: 'database_dialect',
142
+ type: (prev, answers) => !answers.database ? null : 'text',
143
+ message: 'Enter the database dialect (postgres for now)',
144
+ },
145
+ {
146
+ name: 'database_url_variable',
147
+ type: (prev, answers) => !answers.database ? null : 'text',
148
+ message: 'Enter the environment variable name for DATABASE_URL if not as written',
149
+ },
150
+ {
151
+ name: 'redis',
152
+ type: 'toggle',
153
+ message: 'Add redis integration?',
154
+ active: 'YES',
155
+ inactive: 'NO',
156
+ },
157
+ {
158
+ name: 'redis_url_variable',
159
+ type: (prev, answers) => !answers.redis ? null : 'text',
160
+ message: 'Enter the environment variable name for REDIS_URL if not as written',
161
+ },
162
+ {
163
+ name: 'webpush',
164
+ type: 'toggle',
165
+ message: 'Add webpush integration?',
166
+ active: 'YES',
167
+ inactive: 'NO',
168
+ },
169
+ {
170
+ name: 'vapid_subject',
171
+ type: (prev, answers) => !answers.webpush ? null : 'text',
172
+ message: 'Enter the vapid_subject URL',
173
+ },
174
+ {
175
+ name: 'vapid_public_key_variable',
176
+ type: (prev, answers) => !answers.webpush ? null : 'text',
177
+ message: 'Enter the environment variable name for VAPID_PUBLIC_KEY if not as written',
178
+ },
179
+ {
180
+ name: 'vapid_private_key_variable',
181
+ type: (prev, answers) => !answers.webpush ? null : 'text',
182
+ message: 'Enter the environment variable name for VAPID_PRIVATE_KEY if not as written',
183
+ },
184
+ ]
122
185
  },
123
186
  ];
124
187
  }
@@ -21,14 +21,15 @@ export default class Worker extends Dotfile {
21
21
  // Defaults merger
22
22
  withDefaults(config) {
23
23
  return this.merge({
24
+ filename: 'worker.js',
25
+ scope: '/',
26
+ skip_waiting: true,
24
27
  cache_name: 'cache_v0',
25
28
  default_fetching_strategy: 'network-first',
26
29
  network_first_urls: [],
27
30
  cache_first_urls: [],
28
31
  network_only_urls: [],
29
32
  cache_only_urls: [],
30
- skip_waiting: true,
31
- bundle_public_env: false,
32
33
  }, config, 'patch');
33
34
  }
34
35
 
@@ -49,6 +50,24 @@ export default class Worker extends Dotfile {
49
50
  }, choices, 'patch');
50
51
  // Questions
51
52
  return [
53
+ {
54
+ name: 'filename',
55
+ type: 'text',
56
+ message: 'Specify the Service Worker filename',
57
+ },
58
+ {
59
+ name: 'scope',
60
+ type: 'text',
61
+ message: 'Specify the Service Worker scope',
62
+ },
63
+ {
64
+ name: 'skip_waiting',
65
+ type: 'toggle',
66
+ message: 'Choose whether to skip the "waiting" state for updated Service Workers',
67
+ active: 'YES',
68
+ inactive: 'NO',
69
+ initial: config.skip_waiting,
70
+ },
52
71
  {
53
72
  name: 'cache_name',
54
73
  type: 'text',
@@ -86,24 +105,7 @@ export default class Worker extends Dotfile {
86
105
  type: (prev, answers) => answers.default_fetching_strategy === 'cache-only' ? null : 'list',
87
106
  message: 'Specify URLs for a "cache-only" fetching strategy (comma-separated, globe supported)',
88
107
  initial: (config.cache_only_urls || []).join(', '),
89
- },
90
- {
91
- name: 'skip_waiting',
92
- type: 'toggle',
93
- message: 'Choose whether to skip the "waiting" state for updated Service Workers',
94
- active: 'YES',
95
- inactive: 'NO',
96
- initial: config.skip_waiting,
97
- },
98
- {
99
- name: 'bundle_public_env',
100
- type: 'toggle',
101
- message: '[bundle_public_env]: Bundle public ENV variables?',
102
- active: 'YES',
103
- inactive: 'NO',
104
- initial: config.bundle_public_env,
105
- validation: ['important'],
106
- },
108
+ }
107
109
  ];
108
110
  }
109
111
  }
@@ -31,6 +31,8 @@ export class HttpEvent {
31
31
 
32
32
  get client() { return this.#init.client; }
33
33
 
34
+ get sdk() { return this.#init.sdk; }
35
+
34
36
  #requestCloneCallback;
35
37
  set onRequestClone(callback) {
36
38
  this.#requestCloneCallback = callback;
@@ -59,7 +61,7 @@ export class HttpEvent {
59
61
  }
60
62
  }
61
63
 
62
- #response = null;
64
+ #response = undefined;
63
65
  get response() { return this.#response; }
64
66
 
65
67
  async respondWith(response) {
@@ -92,7 +94,7 @@ export class HttpEvent {
92
94
  const messageID = (0 | Math.random() * 9e6).toString(36);
93
95
  const $url = new URL(url, this.request.url);
94
96
  $url.searchParams.set('redirect-message', messageID);
95
- this.session.set(`redirect-message:${messageID}`, data);
97
+ await this.session.set(`redirect-message:${messageID}`, data);
96
98
  await this.redirect($url, ...args);
97
99
  }
98
100
 
@@ -114,7 +116,7 @@ export class HttpEvent {
114
116
  if (endOfStream) {
115
117
  res(response);
116
118
  } else {
117
- await this.client.postMessage(response, { messageType: 'response' });
119
+ await this.respondWith(response);
118
120
  }
119
121
  });
120
122
  poll(typeof maxClock === 'number' && maxClock > 0 ? --maxClock : maxClock);
@@ -122,20 +124,32 @@ export class HttpEvent {
122
124
  poll(maxClock);
123
125
  };
124
126
  // Life cycle management
125
- this.client.on('connected', () => {
126
- state.connected = true;
127
- start();
128
- });
129
- this.client.on('empty', () => {
130
- state.connected = false;
131
- });
132
- this.client.handleMessages('navigation', (e) => {
133
- if (!crossNavigation
134
- || (crossNavigation === -1 && e.data.pathname === this.url.pathname)
135
- || (typeof crossNavigation === 'function' && !crossNavigation(e.data))) {
136
- state.navigatedAway = true;
137
- }
138
- });
127
+ if (this.#response === undefined) {
128
+ callback(async (response, endOfStream = false) => {
129
+ if (endOfStream) {
130
+ state.earlyTermination = true;
131
+ res(response);
132
+ } else {
133
+ await this.respondWith(response);
134
+ }
135
+ });
136
+ }
137
+ if (!state.earlyTermination) {
138
+ this.client.on('connected', () => {
139
+ state.connected = true;
140
+ start();
141
+ });
142
+ this.client.on('empty', () => {
143
+ state.connected = false;
144
+ });
145
+ this.client.handleMessages('navigation', (e) => {
146
+ if (!crossNavigation
147
+ || (crossNavigation === -1 && e.data.pathname === this.url.pathname)
148
+ || (typeof crossNavigation === 'function' && !crossNavigation(e.data))) {
149
+ state.navigatedAway = true;
150
+ }
151
+ });
152
+ }
139
153
  setTimeout(() => {
140
154
  if (!state.connected) {
141
155
  res();
@@ -154,8 +168,9 @@ export class HttpEvent {
154
168
  request = !_isEmpty(init) ? new Request(url, init) : url;
155
169
  } else {
156
170
  url = new xURL(url, this.#url.origin);
157
- init = await Request.copy(this.request, init);
158
- request = new Request(url, { ...init, referrer: this.request.url });
171
+ const { url: _, ...$init } = await Request.copy(this.request, init);
172
+ init = $init;
173
+ request = Request.create(url, { ...init, referrer: this.request.url });
159
174
  }
160
175
  return new HttpEvent(this, { ...this.#init, request });
161
176
  }
@@ -1,4 +1,3 @@
1
- import { _isObject } from '@webqit/util/js/index.js';
2
1
  import { WebfloStorage } from './WebfloStorage.js';
3
2
 
4
3
  export class HttpUser extends WebfloStorage {
@@ -7,79 +6,15 @@ export class HttpUser extends WebfloStorage {
7
6
  return new this(request, session, client);
8
7
  }
9
8
 
10
- #session;
11
9
  #client;
12
10
 
13
11
  constructor(request, session, client) {
14
- super(request, session);
15
- this.#session = session;
12
+ super(session, '#user', request, session);
16
13
  this.#client = client;
17
- // Trigger this
18
- this.#dict;
19
14
  }
20
15
 
21
- get #dict() {
22
- if (!this.#session.has('user')) {
23
- this.#session.set('user', {});
24
- }
25
- return this.#session.get('user');
26
- }
27
-
28
- [ Symbol.iterator ]() { return this.entries()[ Symbol.iterator ](); }
29
-
30
- get size() { return Object.keys(this.#dict).length; }
31
-
32
- set(key, value) {
33
- Reflect.set(this.#dict, key, value);
34
- return this;
35
- }
36
-
37
- get(key) {
38
- return Reflect.get(this.#dict, key);
39
- }
40
-
41
- has(key) {
42
- return Reflect.has(this.#dict, key);
43
- }
44
-
45
- delete(key) {
46
- return Reflect.deleteProperty(this.#dict, key);
47
- }
48
-
49
- keys() {
50
- return Object.keys(this.#dict);
51
- }
52
-
53
- values() {
54
- return Object.values(this.#dict);
55
- }
56
-
57
- entries() {
58
- return Object.entries(this.#dict);
59
- }
60
-
61
- clear() {
62
- for (const key of this.keys()) {
63
- Reflect.deleteProperty(this.#dict, key);
64
- }
65
- }
66
-
67
- forEach(callback) {
68
- this.entries().forEach(callback);
69
- }
70
-
71
- json(arg = null) {
72
- if (!arguments.length || typeof arg === 'boolean') {
73
- return {...this.#dict};
74
- }
75
- if (!_isObject(arg)) {
76
- throw new Error(`Argument must be a valid JSON object`);
77
- }
78
- Object.assign(this.#dict, arg);
79
- }
80
-
81
- isSignedIn() {
82
- return this.has('id');
16
+ async isSignedIn() {
17
+ return await this.has('id');
83
18
  }
84
19
 
85
20
  async signIn(...args) {
@@ -90,22 +25,11 @@ export class HttpUser extends WebfloStorage {
90
25
  }
91
26
 
92
27
  async signOut() {
93
- const handler = this.getReverseHandlers().get('id')?.[0];
94
- let response;
95
- if (typeof handler === 'string') {
96
- response = new Response(null, { status: 302, headers: {
97
- Location: url
98
- }});
99
- }
100
- if (typeof handler === 'function') {
101
- response = await handler(this);
102
- }
103
- this.clear();
104
- return response;
28
+ await this.clear();
105
29
  }
106
30
 
107
- confirm(data, callback, options = {}) {
108
- return new Promise((resolve) => {
31
+ async confirm(data, callback, options = {}) {
32
+ return await new Promise((resolve) => {
109
33
  this.#client.postRequest(
110
34
  data,
111
35
  (event) => resolve(callback ? callback(event) : event),
@@ -114,8 +38,8 @@ export class HttpUser extends WebfloStorage {
114
38
  });
115
39
  }
116
40
 
117
- prompt(data, callback, options = {}) {
118
- return new Promise((resolve) => {
41
+ async prompt(data, callback, options = {}) {
42
+ return await new Promise((resolve) => {
119
43
  this.#client.postRequest(
120
44
  data,
121
45
  (event) => resolve(callback ? callback(event) : event),
@@ -3,25 +3,41 @@ import { renderCookieObj } from './util-http.js';
3
3
  import { WebfloStorage } from './WebfloStorage.js';
4
4
 
5
5
  export class WebfloCookieStorage extends WebfloStorage {
6
+
7
+ #originals;
8
+
6
9
  constructor(request, iterable = []) {
7
10
  iterable = [...iterable].map(([key, value]) => [key, !_isObject(value) ? { name: key, value } : value]);
8
- super(request, null, iterable);
9
- this.saveOriginals();
11
+ iterable = Object.fromEntries(iterable);
12
+ super(iterable, null, request);
13
+ this.#originals = { ...iterable };
10
14
  }
11
15
 
12
- render() {
13
- return this.getAdded().map((key) => renderCookieObj({ name: key, ...this.get(key, true) })).concat(
14
- this.getDeleted().map((key) => renderCookieObj({ name: key, value: '', maxAge: 0 }))
15
- );
16
+ async set(key, value) {
17
+ if (!_isObject(value)) { value = { name: key, value }; }
18
+ return await super.set(key, value);
16
19
  }
17
20
 
18
- set(key, value) {
19
- if (!_isObject(value)) { value = { name: key, value }; }
20
- return super.set(key, value);
21
+ async get(key, withDetail = false) {
22
+ if (!withDetail) return (await super.get(key))?.value;
23
+ return await super.get(key);
21
24
  }
22
25
 
23
- get(key, withDetail = false) {
24
- if (!withDetail) return super.get(key)?.value;
25
- return super.get(key);
26
+ async render() {
27
+ const entries = await Promise.all((await this.keys()).concat(Object.keys(this.#originals)).map(async (key) => {
28
+ const a = Reflect.get(this.#originals, key);
29
+ const b = await this.get(key, true);
30
+ if (a === b || (_isObject(a) && _isObject(b) && _even(a, b))) {
31
+ // Same
32
+ return;
33
+ }
34
+ if ([undefined, null].includes(b)) {
35
+ // Deleted
36
+ return { name: key, value: '', maxAge: 0 };
37
+ }
38
+ // Added or modified
39
+ return { name: key, ...(await this.get(key, true)) };
40
+ })).then((entries) => entries.filter((e) => e));
41
+ return entries.map((e) => renderCookieObj(e));
26
42
  }
27
43
  }
@@ -8,7 +8,7 @@ export class WebfloRouter {
8
8
  this.path = _isArray(path) ? path : (path + '').split('/').filter(a => a);
9
9
  }
10
10
 
11
- async route(method, event, arg, _default, remoteFetch = null, requestLifecycle = null) {
11
+ async route(method, event, arg = null, _default = null, remoteFetch = null, requestLifecycle = null) {
12
12
 
13
13
  const $this = this;
14
14
  const $runtime = this.cx.runtime;
@@ -58,7 +58,7 @@ export class WebfloRouter {
58
58
  nextTick.event = await thisTick.event.with(newDestination, _request, requestInit);
59
59
  } else {
60
60
  nextTick.event = await thisTick.event.with(newDestination, requestInit);
61
- }
61
+ }
62
62
  nextTick.source = thisTick.destination.join('/');
63
63
  nextTick.destination = newDestination.split('?').shift().split('/').map(a => a.trim()).filter(a => a);
64
64
  nextTick.trail = _args[1].startsWith('/') ? [] : thisTick.trail.reduce((_commonRoot, _seg, i) => _commonRoot.length === i && _seg === nextTick.destination[i] ? _commonRoot.concat(_seg) : _commonRoot, []);