@webqit/webflo 0.20.23 → 0.20.25
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 +1 -1
- package/src/runtime-pi/WebfloRuntime.js +13 -4
- package/src/runtime-pi/webflo-routing/HttpState.js +27 -10
- package/src/runtime-pi/webflo-routing/HttpThread.js +18 -4
- package/src/runtime-pi/webflo-routing/HttpUser.js +9 -2
- package/src/runtime-pi/webflo-server/WebfloServer.js +157 -20
- package/src/runtime-pi/webflo-server/messaging/Client.js +3 -3
package/package.json
CHANGED
|
@@ -156,6 +156,17 @@ export class WebfloRuntime {
|
|
|
156
156
|
for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
|
|
157
157
|
await storage?.commit?.(response, FLAGS['dev']);
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
// End-of-life cleanup
|
|
161
|
+
const cleanup = async () => {
|
|
162
|
+
for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
|
|
163
|
+
storage.cleanup();
|
|
164
|
+
}
|
|
165
|
+
if (!httpEvent.thread.extended) {
|
|
166
|
+
await httpEvent.thread.clear();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
159
170
|
// Wait for any whileLive promises to resolve
|
|
160
171
|
if (LiveResponse.test(response) === 'LiveResponse' && response.whileLive()) {
|
|
161
172
|
httpEvent.waitUntil(response.whileLive(true));
|
|
@@ -194,11 +205,9 @@ export class WebfloRuntime {
|
|
|
194
205
|
// Close httpEvent.client
|
|
195
206
|
httpEvent.lifeCycleComplete(true).then(async () => {
|
|
196
207
|
httpEvent.client.close();
|
|
197
|
-
|
|
198
|
-
await httpEvent.thread.clear();
|
|
199
|
-
}
|
|
208
|
+
cleanup();
|
|
200
209
|
});
|
|
201
|
-
}
|
|
210
|
+
} else cleanup();
|
|
202
211
|
|
|
203
212
|
if (!this.isClientSide && LiveResponse.test(response) === 'LiveResponse') {
|
|
204
213
|
// Must convert to Response on the server-side before returning
|
|
@@ -69,25 +69,42 @@ export class HttpState {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
#listeners = new Set;
|
|
72
|
-
observe(attr, handler) {
|
|
73
|
-
const args = { attr, handler };
|
|
74
|
-
this.#listeners.add(args);
|
|
75
|
-
return () => {
|
|
76
|
-
this.#listeners.delete(args);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
async emit(attr, value) {
|
|
81
73
|
const returnValues = [];
|
|
82
|
-
for (const
|
|
74
|
+
for (const subscription of this.#listeners) {
|
|
75
|
+
const { attr: $attr, handler, options } = subscription;
|
|
83
76
|
if (arguments.length && $attr !== attr) continue;
|
|
84
77
|
if (arguments.length > 1) {
|
|
85
78
|
returnValues.push(handler(value));
|
|
86
79
|
} else {
|
|
87
80
|
returnValues.push(handler());
|
|
88
81
|
}
|
|
82
|
+
if (options.once) this.#listeners.delete(subscription);
|
|
83
|
+
}
|
|
84
|
+
await Promise.all(returnValues);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
observe(attr, handler, options = {}) {
|
|
88
|
+
if (typeof this.#store.observe === 'function') {
|
|
89
|
+
return this.#store.observe(attr, handler, options);
|
|
90
|
+
}
|
|
91
|
+
const subscription = { attr, handler, options };
|
|
92
|
+
this.#listeners.add(subscription);
|
|
93
|
+
if (options.signal) {
|
|
94
|
+
options.signal.addEventListener('abort', () => {
|
|
95
|
+
this.#listeners.delete(subscription);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return () => {
|
|
99
|
+
this.#listeners.delete(subscription);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
cleanup() {
|
|
104
|
+
if (typeof this.#store.cleanup === 'function') {
|
|
105
|
+
this.#store.cleanup();
|
|
89
106
|
}
|
|
90
|
-
|
|
107
|
+
this.#listeners.clear();
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
#handlers = new Map;
|
|
@@ -20,6 +20,10 @@ export class HttpThread {
|
|
|
20
20
|
this.#realm = realm;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
get extended() { return this.#extended; }
|
|
24
|
+
|
|
25
|
+
extend(set = true) { this.#extended = !!set; }
|
|
26
|
+
|
|
23
27
|
spawn(_threadID = null) {
|
|
24
28
|
return this.constructor.create({
|
|
25
29
|
store: this.#store,
|
|
@@ -48,6 +52,20 @@ export class HttpThread {
|
|
|
48
52
|
return this;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
async get(key, filter = null) {
|
|
56
|
+
const thread = await this.#store.get(this.#threadID) || {};
|
|
57
|
+
const values = [].concat(thread[key] ?? []);
|
|
58
|
+
|
|
59
|
+
let value;
|
|
60
|
+
if (filter === true) {
|
|
61
|
+
value = values;
|
|
62
|
+
} else if (filter) {
|
|
63
|
+
value = values.find(filter);
|
|
64
|
+
} else { value = values[values.length - 1]; }
|
|
65
|
+
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
async consume(key, filter = null) {
|
|
52
70
|
const thread = await this.#store.get(this.#threadID) || {};
|
|
53
71
|
const values = [].concat(thread[key] ?? []);
|
|
@@ -79,8 +97,4 @@ export class HttpThread {
|
|
|
79
97
|
await this.#store.delete(this.#threadID);
|
|
80
98
|
return this;
|
|
81
99
|
}
|
|
82
|
-
|
|
83
|
-
get extended() { return this.#extended; }
|
|
84
|
-
|
|
85
|
-
extend(set = true) { this.#extended = !!set; }
|
|
86
100
|
}
|
|
@@ -17,8 +17,15 @@ export class HttpUser extends HttpState {
|
|
|
17
17
|
this.#client = client;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async isSignedIn() {
|
|
21
|
-
|
|
20
|
+
async isSignedIn(callback = null, options = {}) {
|
|
21
|
+
const isSignedIn = await this.get('id');
|
|
22
|
+
if (callback) {
|
|
23
|
+
await callback(isSignedIn);
|
|
24
|
+
return options.once
|
|
25
|
+
? undefined
|
|
26
|
+
: this.observe('id', callback, options);
|
|
27
|
+
}
|
|
28
|
+
return !!isSignedIn;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
async signIn(...args) {
|
|
@@ -152,28 +152,165 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
152
152
|
|| !this.bootstrap.init.redis) {
|
|
153
153
|
return super.initCreateStorage();
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
// ns -> field -> { value, subscriptions: Set<fn> }
|
|
157
|
+
const local = new Map();
|
|
158
|
+
const fire = (state, value, key, namespace, scopeReference = false) => {
|
|
159
|
+
const returnValues = [];
|
|
160
|
+
for (const subscription of state.subscriptions) {
|
|
161
|
+
const { callback, options } = subscription;
|
|
162
|
+
|
|
163
|
+
const scope = subscription.scopeReference === scopeReference ? 0 : scopeReference ? 1 : 2;
|
|
164
|
+
// For options.scope === 0, only include same-request cycle mutations
|
|
165
|
+
// For options.scope === 1, only include local mutations
|
|
166
|
+
// For options.scope === 2, include remote mutations
|
|
167
|
+
if ((options.scope || 0) !== scope) continue;
|
|
168
|
+
|
|
169
|
+
returnValues.push(callback(value, scope));
|
|
170
|
+
if (options.once) state.subscriptions.delete(subscription);
|
|
171
|
+
const ns = local.get(namespace);
|
|
172
|
+
if (!state.subscriptions.size) ns.delete(key);
|
|
173
|
+
if (!ns.size) local.delete(namespace);
|
|
174
|
+
}
|
|
175
|
+
return Promise.all(returnValues);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Initialize storage
|
|
155
179
|
const redis = this.bootstrap.init.redis;
|
|
156
|
-
this.bootstrap.init.createStorage = (namespace, ttl = null) =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
this.bootstrap.init.createStorage = (namespace, ttl = null, scopeReference = {}) => {
|
|
181
|
+
const cleanups = [];
|
|
182
|
+
return {
|
|
183
|
+
async has(key) { return await redis.hexists(namespace, key); },
|
|
184
|
+
async get(key) {
|
|
185
|
+
const jsonValue = await redis.hget(namespace, key);
|
|
186
|
+
return jsonValue === null ? undefined : JSON.parse(jsonValue);
|
|
187
|
+
},
|
|
188
|
+
async set(key, value) {
|
|
189
|
+
const jsonValue = JSON.stringify(value);
|
|
190
|
+
const returnValue = await redis.hset(namespace, key, jsonValue);
|
|
191
|
+
if (!this.ttlApplied && ttl) {
|
|
192
|
+
await redis.expire(namespace, ttl);
|
|
193
|
+
this.ttlApplied = true;
|
|
194
|
+
}
|
|
195
|
+
const state = local.get(namespace)?.get(key);
|
|
196
|
+
if (state) {
|
|
197
|
+
state.jsonValue = jsonValue;
|
|
198
|
+
state.initialized = true;
|
|
199
|
+
await fire(state, value, key, namespace, scopeReference);
|
|
200
|
+
}
|
|
201
|
+
return returnValue;
|
|
202
|
+
},
|
|
203
|
+
async delete(key) {
|
|
204
|
+
const returnValue = await redis.hdel(namespace, key);
|
|
205
|
+
const state = local.get(namespace)?.get(key);
|
|
206
|
+
if (state) {
|
|
207
|
+
state.jsonValue = null;
|
|
208
|
+
state.initialized = true;
|
|
209
|
+
await fire(state, undefined, key, namespace, scopeReference);
|
|
210
|
+
}
|
|
211
|
+
return returnValue;
|
|
212
|
+
},
|
|
213
|
+
async clear() {
|
|
214
|
+
const returnValue = await redis.del(namespace);
|
|
215
|
+
const nsLocal = local.get(namespace);
|
|
216
|
+
if (nsLocal) {
|
|
217
|
+
for (const [key, state] of nsLocal.entries()) {
|
|
218
|
+
state.jsonValue = null;
|
|
219
|
+
state.initialized = true;
|
|
220
|
+
await fire(state, undefined, key, namespace, scopeReference);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return returnValue;
|
|
224
|
+
},
|
|
225
|
+
async keys() { return await redis.hkeys(namespace); },
|
|
226
|
+
async values() { return (await redis.hvals(namespace) || []).map((value) => value === null ? undefined : JSON.parse(value)); },
|
|
227
|
+
async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, value === null ? undefined : JSON.parse(value)]); },
|
|
228
|
+
get size() { return redis.hlen(namespace); },
|
|
229
|
+
observe(key, callback, options = {}) {
|
|
230
|
+
// Prepare local data structure
|
|
231
|
+
let ns = local.get(namespace);
|
|
232
|
+
if (!ns) {
|
|
233
|
+
ns = new Map();
|
|
234
|
+
local.set(namespace, ns);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let state = ns.get(key);
|
|
238
|
+
if (!state) {
|
|
239
|
+
state = { jsonValue: null, initialized: false, subscriptions: new Set() };
|
|
240
|
+
ns.set(key, state);
|
|
241
|
+
// Prime initial value only once
|
|
242
|
+
redis.hget(namespace, key).then((jsonValue) => {
|
|
243
|
+
if (state.initialized) return;
|
|
244
|
+
state.jsonValue = jsonValue;
|
|
245
|
+
state.initialized = true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const subscription = { callback, options, scopeReference };
|
|
250
|
+
state.subscriptions.add(subscription);
|
|
251
|
+
if (options.signal) {
|
|
252
|
+
options.signal.addEventListener('abort', () => {
|
|
253
|
+
state.subscriptions.delete(subscription);
|
|
254
|
+
if (state.subscriptions.size === 0) ns.delete(key);
|
|
255
|
+
if (ns.size === 0) local.delete(namespace);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Unsubscribe logic
|
|
260
|
+
const cleanup = () => {
|
|
261
|
+
state.subscriptions.delete(subscription);
|
|
262
|
+
if (state.subscriptions.size === 0) ns.delete(key);
|
|
263
|
+
if (ns.size === 0) local.delete(namespace);
|
|
264
|
+
};
|
|
265
|
+
cleanups.push(cleanup);
|
|
266
|
+
return cleanup;
|
|
267
|
+
},
|
|
268
|
+
cleanup() {
|
|
269
|
+
for (const cleanup of cleanups) cleanup();
|
|
270
|
+
cleanups.length = 0;
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Watch for changes
|
|
276
|
+
const redisWatch = this.bootstrap.init.redisWatch;
|
|
277
|
+
if (redisWatch) {
|
|
278
|
+
// Subscribe to all events
|
|
279
|
+
redisWatch.psubscribe('__keyevent@0__:*');
|
|
280
|
+
redisWatch.on('pmessage', async (pattern, channel, redisKey) => {
|
|
281
|
+
const [, , event] = channel.split(':'); // -> "hset", "hdel", "expire", "del"
|
|
282
|
+
const namespace = redisKey;
|
|
283
|
+
|
|
284
|
+
const nsLocal = local.get(namespace);
|
|
285
|
+
if (!nsLocal) return;
|
|
286
|
+
|
|
287
|
+
const emitDiff = async () => {
|
|
288
|
+
for (const [field, state] of nsLocal) {
|
|
289
|
+
const jsonValue = await redis.hget(namespace, field);
|
|
290
|
+
if (jsonValue !== state.jsonValue) {
|
|
291
|
+
state.jsonValue = jsonValue;
|
|
292
|
+
const value = jsonValue === null ? undefined : JSON.parse(jsonValue);
|
|
293
|
+
fire(state, value, field, namespace);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
167
296
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
297
|
+
|
|
298
|
+
// Field updates
|
|
299
|
+
if (event === 'hset') await emitDiff();
|
|
300
|
+
if (event === 'hdel') await emitDiff();
|
|
301
|
+
|
|
302
|
+
// Namespace removal (expire or del)
|
|
303
|
+
if (event === 'expire' || event === 'del') {
|
|
304
|
+
for (const [key, state] of nsLocal) {
|
|
305
|
+
if (state.jsonValue !== null) {
|
|
306
|
+
state.jsonValue = null;
|
|
307
|
+
fire(state, undefined, key, namespace);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
local.delete(namespace);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
177
314
|
}
|
|
178
315
|
|
|
179
316
|
control() {
|
|
@@ -17,11 +17,11 @@ export class Client extends WQStarPort {
|
|
|
17
17
|
|
|
18
18
|
createRequestRealtime(portID, url = null) {
|
|
19
19
|
const requestPort = new ClientRequestRealtime(portID, url);
|
|
20
|
-
|
|
20
|
+
this.addPort(requestPort);
|
|
21
21
|
setTimeout(() => {
|
|
22
22
|
if (requestPort.length || !this.findPort((port) => port === requestPort)) return;
|
|
23
|
-
|
|
24
|
-
},
|
|
23
|
+
requestPort.close(true);
|
|
24
|
+
}, 15000/*15sec*/);
|
|
25
25
|
return requestPort;
|
|
26
26
|
}
|
|
27
27
|
}
|