@webqit/webflo 1.0.20 → 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.
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.20",
15
+ "version": "1.0.21",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -35,15 +35,17 @@
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
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
51
  "urlpattern-polyfill": "^4.0.3",
@@ -28,8 +28,8 @@ export default class Client extends Dotfile {
28
28
  webpush: false,
29
29
  custom_install: false,
30
30
  exposed: ['display-mode', 'notifications'],
31
- app_vapid_public_key_variable: 'APP_VAPID_PUBLIC_KEY',
32
- app_public_webhook_url_variable: 'APP_PUBLIC_WEBHOOK_URL',
31
+ vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
32
+ generic_public_webhook_url_variable: 'GENERIC_PUBLIC_WEBHOOK_URL',
33
33
  },
34
34
  }, config, 'patch');
35
35
  }
@@ -104,14 +104,14 @@ export default class Client extends Dotfile {
104
104
  initial: (config.exposed || []).join(', '),
105
105
  },
106
106
  {
107
- name: 'app_vapid_public_key_variable',
107
+ name: 'vapid_public_key_variable',
108
108
  type: (prev, answers) => !answers.webpush ? null : 'text',
109
109
  message: 'Enter the environment variable name for APP_VAPID_PUBLIC_KEY if not as written',
110
110
  },
111
111
  {
112
- name: 'app_public_webhook_url_variable',
112
+ name: 'generic_public_webhook_url_variable',
113
113
  type: 'text',
114
- message: 'Enter the environment variable name for APP_PUBLIC_WEBHOOK_URL if not as written',
114
+ message: 'Enter the environment variable name for GENERIC_PUBLIC_WEBHOOK_URL if not as written',
115
115
  },
116
116
  ]
117
117
  }
@@ -29,11 +29,17 @@ export default class Server extends Dotfile {
29
29
  force: false,
30
30
  },
31
31
  force_www: '',
32
- session_key_variable: 'APP_SESSION_KEY',
32
+ session_key_variable: 'SESSION_KEY',
33
33
  capabilities: {
34
+ database: false,
35
+ database_dialect: 'postgres',
36
+ database_url_variable: 'DATABASE_URL',
37
+ redis: false,
38
+ redis_url_variable: 'REDIS_URL',
34
39
  webpush: false,
35
- app_vapid_public_key_variable: 'APP_VAPID_PUBLIC_KEY',
36
- app_vapid_private_key_variable: 'APP_VAPID_PRIVATE_KEY'
40
+ vapid_subject: 'mailto:foo@example.com',
41
+ vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
42
+ vapid_private_key_variable: 'VAPID_PRIVATE_KEY',
37
43
  },
38
44
  }, config, 'patch');
39
45
  }
@@ -74,7 +80,7 @@ export default class Server extends Dotfile {
74
80
  {
75
81
  name: 'session_key_variable',
76
82
  type: 'text',
77
- message: 'Enter the environment variable name for APP_SESSION_KEY if not as written',
83
+ message: 'Enter the environment variable name for SESSION_KEY if not as written',
78
84
  initial: config.session_key_variable,
79
85
  },
80
86
  {
@@ -124,22 +130,56 @@ export default class Server extends Dotfile {
124
130
  },
125
131
  initial: config.capabilities,
126
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
+ },
127
162
  {
128
163
  name: 'webpush',
129
164
  type: 'toggle',
130
- message: 'Support push-notifications?',
165
+ message: 'Add webpush integration?',
131
166
  active: 'YES',
132
167
  inactive: 'NO',
133
168
  },
134
169
  {
135
- name: 'app_vapid_public_key_variable',
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',
136
176
  type: (prev, answers) => !answers.webpush ? null : 'text',
137
- message: 'Enter the environment variable name for APP_VAPID_PUBLIC_KEY if not as written',
177
+ message: 'Enter the environment variable name for VAPID_PUBLIC_KEY if not as written',
138
178
  },
139
179
  {
140
- name: 'app_vapid_private_key_variable',
180
+ name: 'vapid_private_key_variable',
141
181
  type: (prev, answers) => !answers.webpush ? null : 'text',
142
- message: 'Enter the environment variable name for APP_VAPID_PRIVATE_KEY if not as written',
182
+ message: 'Enter the environment variable name for VAPID_PRIVATE_KEY if not as written',
143
183
  },
144
184
  ]
145
185
  },
@@ -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,89 +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
- return this.#session.get('user') || {};
23
- }
24
-
25
- [ Symbol.iterator ]() { return this.entries()[ Symbol.iterator ](); }
26
-
27
- get size() { return Object.keys(this.#dict).length; }
28
-
29
- get(key) {
30
- return Reflect.get(this.#dict, key);
31
- }
32
-
33
- has(key) {
34
- return Reflect.has(this.#dict, key);
35
- }
36
-
37
- keys() {
38
- return Object.keys(this.#dict);
39
- }
40
-
41
- values() {
42
- return Object.values(this.#dict);
43
- }
44
-
45
- entries() {
46
- return Object.entries(this.#dict);
47
- }
48
-
49
- forEach(callback) {
50
- this.entries().forEach(callback);
51
- }
52
-
53
- async set(key, value) {
54
- if (!this.#session.has('user')) {
55
- await this.#session.set('user', {});
56
- }
57
- Reflect.set(this.#dict, key, value);
58
- await this.emit(key, value);
59
- return this;
60
- }
61
-
62
- async delete(key) {
63
- if (!this.#session.has('user')) {
64
- await this.#session.set('user', {});
65
- }
66
- Reflect.deleteProperty(this.#dict, key);
67
- await this.emit(key);
68
- return this;
69
- }
70
-
71
- async clear() {
72
- for (const key of this.keys()) {
73
- Reflect.deleteProperty(this.#dict, key);
74
- }
75
- await this.emit();
76
- return this;
77
- }
78
-
79
- async json(arg = null) {
80
- if (!arguments.length || typeof arg === 'boolean') {
81
- return {...this.#dict};
82
- }
83
- if (!_isObject(arg)) {
84
- throw new Error(`Argument must be a valid JSON object`);
85
- }
86
- return await Promise.all(Object.entries(arg).map(([key, value]) => {
87
- return this.set(key, value);
88
- }));
89
- }
90
-
91
- isSignedIn() {
92
- return this.has('id');
16
+ async isSignedIn() {
17
+ return await this.has('id');
93
18
  }
94
19
 
95
20
  async signIn(...args) {
@@ -3,10 +3,14 @@ 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
16
  async set(key, value) {
@@ -14,14 +18,26 @@ export class WebfloCookieStorage extends WebfloStorage {
14
18
  return await super.set(key, value);
15
19
  }
16
20
 
17
- get(key, withDetail = false) {
18
- if (!withDetail) return super.get(key)?.value;
19
- return super.get(key);
21
+ async get(key, withDetail = false) {
22
+ if (!withDetail) return (await super.get(key))?.value;
23
+ return await super.get(key);
20
24
  }
21
25
 
22
- render() {
23
- return this.getAdded().map((key) => renderCookieObj({ name: key, ...this.get(key, true) })).concat(
24
- this.getDeleted().map((key) => renderCookieObj({ name: key, value: '', maxAge: 0 }))
25
- );
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
  }
@@ -37,8 +37,8 @@ export class WebfloRuntime {
37
37
  ? new Response(null, { status: 404 })
38
38
  : Response.create(response);
39
39
  }
40
- // Commit data
41
- for (const storage of [httpEvent.cookies, httpEvent.session, httpEvent.storage]) {
40
+ // Commit data in the exact order. Reason: in how they depend on each other
41
+ for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
42
42
  await storage?.commit?.(response);
43
43
  }
44
44
  return response;
@@ -1,39 +1,83 @@
1
1
  import { _isObject } from '@webqit/util/js/index.js';
2
2
  import { _even } from '@webqit/util/obj/index.js';
3
3
 
4
- export class WebfloStorage extends Map {
4
+ export class WebfloStorage {
5
5
 
6
6
  #request;
7
7
  #session;
8
+ #registry;
9
+ #key;
10
+ #store;
8
11
 
9
- constructor(request, session, iterable = []) {
10
- super();
12
+ constructor(registry, key, request, session = null) {
13
+ this.#registry = registry;
14
+ this.#key = key;
11
15
  this.#request = request;
12
16
  this.#session = session === true ? this : session;
13
- for (const [k, v] of iterable) {
14
- this.set(k, v);
15
- }
16
17
  }
17
18
 
18
- #originals;
19
- saveOriginals() { this.#originals = new Map(this); }
20
-
21
- getDeleted() {
22
- if (!this.#originals) return [];
23
- return [...this.#originals.keys()].filter((k) => {
24
- return !this.has(k);
25
- });
19
+ async store() {
20
+ if (!this.#key) {
21
+ return this.#registry;
22
+ }
23
+ if (!this.#store && !(this.#store = await this.#registry.get(this.#key))) {
24
+ this.#store = {};
25
+ await this.#registry.set(this.#key, this.#store);
26
+ }
27
+ return this.#store;
26
28
  }
27
29
 
28
- getAdded() {
29
- if (!this.#originals) return [...this.keys()];
30
- return [...new Set([...this.keys(), ...this.#originals.keys()])].filter((k) => {
31
- return !this.#originals.has(k) || (this.has(k) && ((a, b) => _isObject(a) && _isObject(b) ? !_even(a, b) : a !== b)(this.get(k, true), this.#originals.get(k)));
32
- });
30
+ async commit() {
31
+ if (!this.#store || !this.#key) return;
32
+ await this.#registry.set(this.#key, this.#store);
33
33
  }
34
34
 
35
- async commit() {
36
- this.saveOriginals();
35
+ get size() { return this.store().then((store) => Object.keys(store).length); }
36
+
37
+ [ Symbol.iterator ]() { return this.entries().then((entries) => entries[ Symbol.iterator ]()); }
38
+
39
+ async json(arg = null) {
40
+ if (!arguments.length || typeof arg === 'boolean') {
41
+ return { ...(await this.store()) };
42
+ }
43
+ if (!_isObject(arg)) {
44
+ throw new Error(`Argument must be a valid JSON object`);
45
+ }
46
+ return await Promise.all(Object.entries(arg).map(([key, value]) => {
47
+ return this.set(key, value);
48
+ }));
49
+ }
50
+
51
+ async get(key) { return Reflect.get(await this.store(), key); }
52
+
53
+ async has(key) { return Reflect.has(await this.store(), key); }
54
+
55
+ async keys() { return Object.keys(await this.store()); }
56
+
57
+ async values() { return Object.values(await this.store()); }
58
+
59
+ async entries() { return Object.entries(await this.store()); }
60
+
61
+ async forEach(callback) { (await this.entries()).forEach(callback); }
62
+
63
+ async set(key, value) {
64
+ Reflect.set(await this.store(), key, value);
65
+ await this.emit(key, value);
66
+ return this;
67
+ }
68
+
69
+ async delete(key) {
70
+ Reflect.deleteProperty(await this.store(), key);
71
+ await this.emit(key);
72
+ return this;
73
+ }
74
+
75
+ async clear() {
76
+ for (const key of await this.keys()) {
77
+ Reflect.deleteProperty(await this.store(), key);
78
+ }
79
+ await this.emit();
80
+ return this;
37
81
  }
38
82
 
39
83
  #listeners = new Set;
@@ -58,24 +102,6 @@ export class WebfloStorage extends Map {
58
102
  return Promise.all(returnValues);
59
103
  }
60
104
 
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
-
79
105
  #handlers = new Map;
80
106
  defineHandler(attr, ...handlers) {
81
107
  const $handlers = [];
@@ -97,7 +123,7 @@ export class WebfloStorage extends Map {
97
123
  async require(attrs, callback = null, noNulls = false) {
98
124
  const entries = [];
99
125
  main: for await (const attr of [].concat(attrs)) {
100
- if (!this.has(attr) || (noNulls && [undefined, null].includes(this.get(attr)))) {
126
+ if (!(await this.has(attr)) || (noNulls && [undefined, null].includes(await this.get(attr)))) {
101
127
  const handlers = this.#handlers.get(attr);
102
128
  if (!handlers) {
103
129
  throw new Error(`No handler defined for the user attribute: ${attr}`);
@@ -132,7 +158,7 @@ export class WebfloStorage extends Map {
132
158
  }});
133
159
  }
134
160
  }
135
- entries.push(this.get(attr));
161
+ entries.push(await this.get(attr));
136
162
  }
137
163
  if (callback) return await callback(...entries);
138
164
  return entries;
@@ -12,8 +12,8 @@ export class Capabilities {
12
12
  static async initialize(params) {
13
13
  const instance = new this;
14
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];
15
+ instance.#params.generic_public_webhook_url = instance.#params.generic_public_webhook_url_variable && instance.#params.env[instance.#params.generic_public_webhook_url_variable];
16
+ instance.#params.vapid_public_key = instance.#params.vapid_public_key_variable && instance.#params.env[instance.#params.vapid_public_key_variable];
17
17
  // --------
18
18
  // Custom install
19
19
  const onbeforeinstallprompt = (e) => {
@@ -26,11 +26,11 @@ export class Capabilities {
26
26
  instance.#cleanups.push(() => window.removeEventListener('beforeinstallprompt', onbeforeinstallprompt));
27
27
  // --------
28
28
  // Webhooks
29
- if (instance.#params.app_public_webhook_url) {
29
+ if (instance.#params.generic_public_webhook_url) {
30
30
  // --------
31
31
  // app.installed
32
32
  const onappinstalled = () => {
33
- fetch(instance.#params.app_public_webhook_url, {
33
+ fetch(instance.#params.generic_public_webhook_url, {
34
34
  method: 'POST',
35
35
  headers: { 'Content-Type': 'application/json' },
36
36
  body: JSON.stringify({ type: 'app.installed', data: true })
@@ -51,7 +51,7 @@ export class Capabilities {
51
51
  if (eventPayload.type === 'push.subscribe' && !eventPayload.data) {
52
52
  return window.queueMicrotask(pushPermissionStatusHandler);
53
53
  }
54
- fetch(instance.#params.app_public_webhook_url, {
54
+ fetch(instance.#params.generic_public_webhook_url, {
55
55
  method: 'POST',
56
56
  headers: { 'Content-Type': 'application/json' },
57
57
  body: JSON.stringify(eventPayload)
@@ -184,8 +184,8 @@ export class Capabilities {
184
184
  if (!params.userVisibleOnly) {
185
185
  params = { ...params, userVisibleOnly: true };
186
186
  }
187
- if (!params.applicationServerKey && this.#params.app_vapid_public_key) {
188
- params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.app_vapid_public_key) };
187
+ if (!params.applicationServerKey && this.#params.vapid_public_key) {
188
+ params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.vapid_public_key) };
189
189
  }
190
190
  }
191
191
  return params;
@@ -9,7 +9,7 @@ export class CookieStorage extends WebfloCookieStorage {
9
9
  }
10
10
 
11
11
  async commit(response) {
12
- for (const cookieStr of this.render()) {
12
+ for (const cookieStr of await this.render()) {
13
13
  document.cookie = cookieStr;
14
14
  }
15
15
  await super.commit();
@@ -1,33 +1,16 @@
1
1
  import { WebfloStorage } from '../WebfloStorage.js';
2
2
 
3
3
  export class SessionStorage extends WebfloStorage {
4
- static get type() { return 'session'; }
5
-
6
4
  static create(request) {
7
- const keys = [];
8
- const storeType = this.type === 'user' ? 'localStorage' : 'sessionStorage';
9
- for(let i = 0; i < window[storeType].length; i ++){
10
- keys.push(window[storeType].key(i));
11
- };
12
- const instance = new this(
5
+ const registry = {
6
+ async get(key) { return localStorage.getItem(key) },
7
+ async set(key, value) { return localStorage.setItem(key, value) },
8
+ };
9
+ return new this(
10
+ registry,
11
+ 'session',
13
12
  request,
14
- keys.map((key) => [key, window[storeType].getItem(key)])
13
+ true
15
14
  );
16
- return instance;
17
- }
18
-
19
- constructor(request, iterable) {
20
- super(request, true, iterable);
21
- }
22
-
23
- async commit() {
24
- const storeType = this.constructor.type === 'user' ? 'localStorage' : 'sessionStorage';
25
- for (const key of this.getAdded()) {
26
- window[storeType].setItem(key, this.get(key));
27
- }
28
- for (const key of this.getDeleted()) {
29
- window[storeType].removeItem(key);
30
- }
31
- await super.commit();
32
15
  }
33
16
  }
@@ -8,9 +8,11 @@ export class CookieStorage extends WebfloCookieStorage {
8
8
  );
9
9
  }
10
10
 
11
- async commit(response) {
12
- for (const cookieStr of this.render()) {
13
- response.headers.append('Set-Cookie', cookieStr);
11
+ async commit(response = null) {
12
+ if (response) {
13
+ for (const cookieStr of await this.render()) {
14
+ response.headers.append('Set-Cookie', cookieStr);
15
+ }
14
16
  }
15
17
  await super.commit();
16
18
  }
@@ -2,14 +2,10 @@ import { WebfloStorage } from '../../WebfloStorage.js';
2
2
 
3
3
  export class SessionStorage extends WebfloStorage {
4
4
  static create(request) {
5
- return new this(request);
5
+ return new this({}, null, request);
6
6
  }
7
7
 
8
- constructor(request) {
9
- super(request, true);
10
- }
11
-
12
- async commit(response) {
8
+ async commit(response = null) {
13
9
  await super.commit();
14
10
  }
15
11
  }
@@ -184,7 +184,7 @@ export class WebfloWorker extends WebfloRuntime {
184
184
  // Restore session before dispatching
185
185
  if (scope.request.method === 'GET'
186
186
  && (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
187
- && (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
187
+ && (scope.redirectMessage = await scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
188
188
  await scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
189
189
  }
190
190
  // Dispatch for response
@@ -8,9 +8,11 @@ export class CookieStorage extends WebfloCookieStorage {
8
8
  );
9
9
  }
10
10
 
11
- async commit(response) {
12
- for (const cookieStr of this.render()) {
13
- response.headers.append('Set-Cookie', cookieStr);
11
+ async commit(response = null) {
12
+ if (response) {
13
+ for (const cookieStr of await this.render()) {
14
+ response.headers.append('Set-Cookie', cookieStr);
15
+ }
14
16
  }
15
17
  await super.commit();
16
18
  }
@@ -1,7 +1,7 @@
1
1
  import { WebfloStorage } from '../WebfloStorage.js';
2
2
  import crypto from 'crypto';
3
3
 
4
- const sessionStorage = new Map;
4
+ const inmemSessionRegistry = new Map;
5
5
  export class SessionStorage extends WebfloStorage {
6
6
 
7
7
  static create(request, params = {}) {
@@ -26,28 +26,26 @@ export class SessionStorage extends WebfloStorage {
26
26
  sessionID = crypto.randomUUID();
27
27
  }
28
28
  }
29
- if (sessionStorage.has(sessionID)) {
30
- return sessionStorage.get(sessionID);
31
- }
32
- const instance = new this(request, sessionID);
33
- sessionStorage.set(sessionID, instance);
34
- return instance;
35
- }
36
-
37
- constructor(request, sessionID) {
38
- super(request, true);
39
- this.#sessionID = sessionID;
29
+ return new this(params.registry || inmemSessionRegistry, sessionID, request);
40
30
  }
41
31
 
42
32
  #sessionID;
43
- get sessionID() {
44
- return this.#sessionID;
33
+ get sessionID() { return this.#sessionID; }
34
+
35
+ constructor(reqistry, sessionID, request) {
36
+ super(
37
+ reqistry,
38
+ `session:${sessionID}`,
39
+ request,
40
+ true
41
+ );
42
+ this.#sessionID = sessionID;
45
43
  }
46
44
 
47
- async commit(response, force = false) {
48
- if (response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) return;
49
- //if (!force && !this.getAdded().length && !this.getDeleted().length) return;
50
- response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=31536000`);
45
+ async commit(response = null) {
46
+ if (response && !response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
47
+ response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=31536000`);
48
+ }
51
49
  await super.commit();
52
50
  }
53
51
  }
@@ -4,7 +4,6 @@ import Path from 'path';
4
4
  import Http from 'http';
5
5
  import Https from 'https';
6
6
  import WebSocket from 'ws';
7
- import webpush from 'web-push';
8
7
  import Mime from 'mime-types';
9
8
  import QueryString from 'querystring';
10
9
  import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
@@ -55,6 +54,9 @@ export class WebfloServer extends WebfloRuntime {
55
54
  // Typically for access by Router
56
55
  get cx() { return this.#cx; }
57
56
 
57
+ #sdk = {};
58
+ get sdk() { return this.#sdk; }
59
+
58
60
  constructor(cx) {
59
61
  super();
60
62
  if (!(cx instanceof this.constructor.Context)) {
@@ -100,6 +102,46 @@ export class WebfloServer extends WebfloRuntime {
100
102
  }));
101
103
  }
102
104
  // ---------------
105
+ if (this.#cx.server.capabilities?.database) {
106
+ if (this.#cx.server.capabilities.database_dialect !== 'postgres') {
107
+ throw new Error(`Only postgres supported for now for database dialect`);
108
+ }
109
+ if (this.#cx.env.entries[this.#cx.server.capabilities.database_url_variable]) {
110
+ const { SQLClient } = await import('@linked-db/linked-ql/sql');
111
+ const { default: pg } = await import('pg');
112
+ // Obtain pg client
113
+ const pgClient = new pg.Pool({
114
+ connectionString: this.#cx.env.entries[this.#cx.server.capabilities.database_url_variable],
115
+ database: 'postgres',
116
+ });
117
+ // Connect
118
+ await pgClient.connect();
119
+ this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
120
+ } else {
121
+ //const { ODBClient } = await import('@linked-db/linked-ql/odb');
122
+ //this.#sdk.db = new ODBClient({ dialect: 'postgres' });
123
+ }
124
+ }
125
+ if (this.#cx.server.capabilities?.redis && this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable]) {
126
+ const { Redis } = await import('ioredis');
127
+ this.#sdk.redis = !this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable]
128
+ ? new Redis : new Redis(this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable], {
129
+ tls: { rejectUnauthorized: false }, // Required for Upstash
130
+ });
131
+ }
132
+ if (this.#cx.server.capabilities?.webpush) {
133
+ const { default: webpush } = await import('web-push');
134
+ this.#sdk.webpush = webpush;
135
+ if (this.#cx.env.entries[this.#cx.server.capabilities.vapid_public_key_variable]
136
+ && this.#cx.env.entries[this.#cx.server.capabilities.vapid_private_key_variable]) {
137
+ webpush.setVapidDetails(
138
+ this.#cx.server.capabilities.vapid_subject,
139
+ this.#cx.env.entries[this.#cx.server.capabilities.vapid_public_key_variable],
140
+ this.#cx.env.entries[this.#cx.server.capabilities.vapid_private_key_variable]
141
+ );
142
+ }
143
+ }
144
+ // ---------------
103
145
  this.control();
104
146
  if (this.#cx.logger) {
105
147
  if (this.#servers.size) {
@@ -507,7 +549,13 @@ export class WebfloServer extends WebfloRuntime {
507
549
  : [];
508
550
  scope.request = this.createRequest(scope.url.href, scope.init, scope.autoHeaders.filter((header) => header.type === 'request'));
509
551
  scope.cookies = this.constructor.CookieStorage.create(scope.request);
510
- scope.session = this.constructor.SessionStorage.create(scope.request, { secret: this.#cx.env.entries[this.#cx.server.session_key_variable] });
552
+ scope.session = this.constructor.SessionStorage.create(scope.request, {
553
+ secret: this.#cx.env.entries[this.#cx.server.session_key_variable],
554
+ registry: this.#sdk.redis && {
555
+ get: async (key) => { return await this.#sdk.redis.hgetall(key) },
556
+ set: async (key, value) => { return await this.#sdk.redis.hset(key, value) },
557
+ },
558
+ });
511
559
  const sessionID = scope.session.sessionID;
512
560
  if (!this.#globalMessagingRegistry.has(sessionID)) {
513
561
  this.#globalMessagingRegistry.set(sessionID, new ClientMessagingRegistry(this, sessionID));
@@ -526,22 +574,13 @@ export class WebfloServer extends WebfloRuntime {
526
574
  session: scope.session,
527
575
  user: scope.user,
528
576
  client: scope.clientMessaging,
529
- sdk: { ...(this.#cx.server.capabilities?.webpush ? { webpush } : {}) }
577
+ sdk: this.#sdk
530
578
  });
531
- if (this.#cx.server.capabilities?.webpush
532
- && this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_public_key_variable]
533
- && this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_private_key_variable]) {
534
- webpush.setVapidDetails(
535
- scope.url.origin.replace(/^http:/i, 'https:'),
536
- this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_public_key_variable],
537
- this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_private_key_variable]
538
- );
539
- }
540
579
  await this.setup(scope.httpEvent);
541
580
  // Restore session before dispatching
542
581
  if (scope.request.method === 'GET'
543
582
  && (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
544
- && (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
583
+ && (scope.redirectMessage = await scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
545
584
  await scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
546
585
  }
547
586
  // Dispatch for response