@webqit/fetch-plus 0.1.2 → 0.1.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.
@@ -0,0 +1,459 @@
1
+ import { expect } from 'chai';
2
+ import { BroadcastChannelPlus, Observer } from '@webqit/port-plus';
3
+ import { LiveResponse } from '../src/LiveResponse.js';
4
+
5
+ describe('LiveResponse Integration Tests (Background Ports)', function () {
6
+
7
+ describe('Simulation via direct Port Manipulation', function () {
8
+ async function setupWire() {
9
+ const portID = 'test-channel-' + Math.random().toString(36).substring(7);
10
+
11
+ // 1. Setup port
12
+ const serverSideClientPort = new BroadcastChannelPlus(portID, {
13
+ clientServerMode: 'server',
14
+ postAwaitsOpen: true,
15
+ autoStart: true // Ensure it's ready to accept connections
16
+ });
17
+
18
+ // ------------
19
+
20
+ // 2. Transport layer response
21
+ const response = new Response('initial content', {
22
+ headers: { 'X-Message-Port': `channel://${portID}` }
23
+ });
24
+
25
+ // ------------
26
+
27
+ // 3. Client-side response
28
+ const liveResponseB = LiveResponse.from(response);
29
+ await liveResponseB.readyStateChange('live');
30
+
31
+ // 4. Take records
32
+ const clientSideResult = [];
33
+ liveResponseB.addEventListener('replace', (e) => {
34
+ clientSideResult.push(liveResponseB.body);
35
+ });
36
+
37
+ return [serverSideClientPort, liveResponseB, clientSideResult];
38
+ }
39
+
40
+ it('should simulate response.replace via a background port', async function () {
41
+ const [serverSideClientPort, liveResponseB, clientSideResult] = await setupWire();
42
+
43
+ // ----- Server-side -----
44
+ serverSideClientPort.postMessage({
45
+ body: 'pushed content 1',
46
+ }, { type: 'response.replace' });
47
+ serverSideClientPort.postMessage({
48
+ body: 'pushed content 2',
49
+ }, { type: 'response.replace' });
50
+ // ----- End: Server-side
51
+
52
+ // ----- Transport latency -----
53
+ await new Promise((r) => setTimeout(r, 10));
54
+ // ----- End: Transport latency
55
+
56
+ // ----- Client-side -----
57
+ expect(clientSideResult[0]).to.equal('pushed content 1');
58
+ expect(clientSideResult[1]).to.equal('pushed content 2');
59
+
60
+ await new Promise((r) => setTimeout(r, 0));
61
+
62
+ // Server did not explicitly post a response.done message
63
+ expect(liveResponseB.readyState).to.equal('live');
64
+ // ----- End: Client-side
65
+ });
66
+
67
+ it('should be done when server specifies done in last message', async function () {
68
+ const [serverSideClientPort, liveResponseB, clientSideResult] = await setupWire();
69
+
70
+ // ----- Server-side -----
71
+ serverSideClientPort.postMessage({
72
+ body: 'pushed content 1',
73
+ }, { type: 'response.replace' });
74
+ serverSideClientPort.postMessage({
75
+ body: 'pushed content 2'
76
+ }, { type: 'response.replace' });
77
+ serverSideClientPort.postMessage(null, { type: 'response.done' });
78
+ // ----- End: Server-side
79
+
80
+ // ----- Transport latency -----
81
+ await new Promise((r) => setTimeout(r, 10));
82
+ // ----- End: Transport latency
83
+
84
+ // ----- Client-side -----
85
+ expect(clientSideResult[0]).to.equal('pushed content 1');
86
+ expect(clientSideResult[1]).to.equal('pushed content 2');
87
+
88
+ await new Promise((r) => setTimeout(r, 0));
89
+
90
+ // Server did explicitly post a response.done message
91
+ expect(liveResponseB.readyState).to.equal('done');
92
+ // ----- End: Client-side
93
+ });
94
+
95
+ it('should project Live Objects', async function () {
96
+ const [serverSideClientPort, liveResponseB, clientSideResult] = await setupWire();
97
+
98
+ const obj1 = { a: 1, b: 2 };
99
+ const obj2 = { a: 2, b: 3 };
100
+ const obj3 = { a: 3, b: 4 };
101
+ const concurrencyController = new AbortController;
102
+
103
+ // ---- First Live Object ----
104
+
105
+ // Replacement + live-projection on the server side
106
+ serverSideClientPort.postMessage({
107
+ body: obj1,
108
+ concurrent: true,
109
+ }, { type: 'response.replace', live: true, signal: concurrencyController.signal });
110
+
111
+ // Inspect on the client side
112
+ await new Promise((r) => setTimeout(r, 20));
113
+ expect(clientSideResult[0]).to.eql(obj1);
114
+ expect(liveResponseB.concurrent).to.true;
115
+
116
+ // Mutate on the server side
117
+ Observer.set(obj1, 'c', 3);
118
+
119
+ // Inspect on the client side
120
+ await new Promise((r) => setTimeout(r, 20));
121
+ expect(clientSideResult[0]).to.eql(obj1);
122
+
123
+ // ---- Second Live Object ----
124
+
125
+ // Replacement + live-projection on the server side
126
+ serverSideClientPort.postMessage({
127
+ body: obj2,
128
+ concurrent: true,
129
+ }, { type: 'response.replace', live: true, signal: concurrencyController.signal });
130
+
131
+ // Inspect on the client side
132
+ await new Promise((r) => setTimeout(r, 20));
133
+ expect(clientSideResult[1]).to.eql(obj2);
134
+ expect(liveResponseB.concurrent).to.true;
135
+
136
+ // Mutate on the server side
137
+ Observer.set(obj2, 'c', 6);
138
+
139
+ // Inspect on the client side
140
+ await new Promise((r) => setTimeout(r, 20));
141
+ expect(clientSideResult[1]).to.eql(obj2);
142
+
143
+ // ---- Concurrency ----
144
+
145
+ // Mutate on the server side
146
+ Observer.set(obj1, 'd', 10); // NOTE: 1
147
+ Observer.set(obj2, 'd', 20);
148
+
149
+ // Inspect on the client side
150
+ await new Promise((r) => setTimeout(r, 20));
151
+ expect(clientSideResult[0]).to.eql(obj1); // NOTE: 1
152
+ expect(clientSideResult[1]).to.eql(obj2);
153
+
154
+ // NOTE: 1 – Second replacement+live projection being concurrent means that the previous live object projection has not been terminated by us on the server.
155
+
156
+ // Server did not explicitly post a response.done message
157
+ expect(liveResponseB.readyState).to.equal('live');
158
+
159
+ // ---- Third Live Object ----
160
+
161
+ concurrencyController.abort();
162
+
163
+ // Replacement + live-projection on the server side
164
+ serverSideClientPort.postMessage({
165
+ body: obj3,
166
+ concurrent: false,
167
+ }, { type: 'response.replace', live: true, signal: null });
168
+ serverSideClientPort.postMessage(null, { type: 'response.done' });
169
+
170
+ // Inspect on the client side
171
+ await new Promise((r) => setTimeout(r, 20));
172
+ expect(clientSideResult[2]).to.eql(obj3);
173
+ expect(liveResponseB.concurrent).to.false;
174
+
175
+ // Mutate on the server side
176
+ Observer.set(obj2, 'c', 6);
177
+
178
+ // Inspect on the client side
179
+ await new Promise((r) => setTimeout(r, 20));
180
+ expect(clientSideResult[1]).to.eql(obj2);
181
+
182
+ // Server did explicitly post a response.done message
183
+ expect(liveResponseB.readyState).to.equal('done');
184
+
185
+ // ---- Concurrency & Continuity ----
186
+
187
+ // Mutate on the server side
188
+ Observer.set(obj1, 'd', 11); // NOTE: 1
189
+ Observer.set(obj2, 'd', 21); // NOTE: 1
190
+ Observer.set(obj3, 'd', 22); // NOTE: 2
191
+
192
+ // Inspect on the client side
193
+ await new Promise((r) => setTimeout(r, 20));
194
+ expect(clientSideResult[0]).to.not.eql(obj1); // NOTE: 1
195
+ expect(clientSideResult[1]).to.not.eql(obj2); // NOTE: 1
196
+ expect(clientSideResult[2]).to.eql(obj3); // NOTE: 2
197
+
198
+ // NOTE: 1 – Third replacement+live projection being non-concurrent means that the previous live object projection has been terminated by us on the server and sp should be discontinued on the client.
199
+ // NOTE: 2 – Third replacement+live projection remains live even tho it's last in the series of "responses" – denoted by the server's "response.done" message
200
+ // – until the server terminates the live projection or until port closes.
201
+ });
202
+
203
+ });
204
+
205
+ describe('LiveResponse End-to-End Tests', function () {
206
+ async function setupWire() {
207
+ const portID = 'test-channel-' + Math.random().toString(36).substring(7);
208
+
209
+ // 1. Setup port
210
+ const serverSideClientPort = new BroadcastChannelPlus(portID, {
211
+ clientServerMode: 'server',
212
+ postAwaitsOpen: true,
213
+ autoStart: true // Ensure it's ready to accept connections
214
+ });
215
+
216
+ // 2. Server-side LiveResponse
217
+ const liveResponseA = new LiveResponse('initial content', { done: false });
218
+
219
+ // ------------
220
+
221
+ // 3. Transport layer Response
222
+ const response = liveResponseA.toResponse({ port: serverSideClientPort, signal: undefined });
223
+ response.headers.set('X-Message-Port', 'channel://' + portID);
224
+
225
+ // ------------
226
+
227
+ // 4. Client-side LiveResponse
228
+ const liveResponseB = new LiveResponse(response);
229
+ await liveResponseB.readyStateChange('live');
230
+
231
+ // 5. Take records
232
+ const clientSideResult = [];
233
+ liveResponseB.addEventListener('replace', (e) => {
234
+ clientSideResult.push(liveResponseB.body);
235
+ });
236
+
237
+ return [liveResponseA, liveResponseB, clientSideResult];
238
+ }
239
+
240
+ it('should simulate response.replace via a background port', async function () {
241
+ const [liveResponseA, liveResponseB, clientSideResult] = await setupWire();
242
+
243
+ // ----- Server-side -----
244
+ await liveResponseA.replaceWith('pushed content 1', { done: false });
245
+ await liveResponseA.replaceWith('pushed content 2', { done: false });
246
+ // ----- End: Server-side -----
247
+
248
+ // ----- Transport latency -----
249
+ await new Promise((r) => setTimeout(r, 20));
250
+ // ----- End: Transport latency -----
251
+
252
+ // ----- Client-side -----
253
+ expect(clientSideResult[0]).to.equal('pushed content 1');
254
+ expect(clientSideResult[1]).to.equal('pushed content 2');
255
+
256
+ await new Promise((r) => setTimeout(r, 20));
257
+
258
+ // Server did specify "done: false" in last message
259
+ expect(liveResponseB.readyState).to.equal('live');
260
+ // ----- End: Client-side -----
261
+ });
262
+
263
+ it('should be done when server specifies done in last message', async function () {
264
+ const [liveResponseA, liveResponseB, clientSideResult] = await setupWire();
265
+
266
+ // ----- Server-side -----
267
+ liveResponseA.replaceWith('pushed content 1', { done: false });
268
+ liveResponseA.replaceWith('pushed content 2');
269
+ // ----- End: Server-side -----
270
+
271
+ // ----- Transport latency -----
272
+ await new Promise((r) => setTimeout(r, 10));
273
+ // ----- End: Transport latency -----
274
+
275
+ // ----- Client-side -----
276
+ expect(clientSideResult[0]).to.equal('pushed content 1');
277
+ expect(clientSideResult[1]).to.equal('pushed content 2');
278
+
279
+ await new Promise((r) => setTimeout(r, 20));
280
+
281
+ // Server did not specify "done: false" in last message
282
+ expect(liveResponseB.readyState).to.equal('done');
283
+ // ----- End: Client-side -----
284
+ });
285
+
286
+ it('should project Live Objects', async function () {
287
+ const [liveResponseA, liveResponseB, clientSideResult] = await setupWire();
288
+
289
+ const obj1 = { a: 1, b: 2 };
290
+ const obj2 = { a: 2, b: 3 };
291
+ const obj3 = { a: 3, b: 4 };
292
+
293
+ // ---- First Live Object ----
294
+
295
+ // Replacement + live-projection on the server side
296
+ await liveResponseA.replaceWith(obj1, { concurrent: true, done: false });
297
+
298
+ // Inspect on the client side
299
+ await new Promise((r) => setTimeout(r, 20));
300
+ expect(clientSideResult[0]).to.eql(obj1);
301
+ expect(liveResponseB.concurrent).to.true;
302
+
303
+ // Mutate on the server side
304
+ Observer.set(obj1, 'c', 3);
305
+
306
+ // Inspect on the client side
307
+ await new Promise((r) => setTimeout(r, 20));
308
+ expect(clientSideResult[0]).to.eql(obj1);
309
+
310
+ // ---- Second Live Object ----
311
+
312
+ // Replacement + live-projection on the server side
313
+ await liveResponseA.replaceWith(obj2, { concurrent: true, done: false });
314
+
315
+ // Inspect on the client side
316
+ await new Promise((r) => setTimeout(r, 20));
317
+ expect(clientSideResult[1]).to.eql(obj2);
318
+ expect(liveResponseB.concurrent).to.true;
319
+
320
+ // Mutate on the server side
321
+ Observer.set(obj2, 'c', 6);
322
+
323
+ // Inspect on the client side
324
+ await new Promise((r) => setTimeout(r, 20));
325
+ expect(clientSideResult[1]).to.eql(obj2);
326
+
327
+ // ---- Concurrency ----
328
+
329
+ // Mutate on the server side
330
+ Observer.set(obj1, 'd', 10); // NOTE: 1
331
+ Observer.set(obj2, 'd', 20);
332
+
333
+ // Inspect on the client side
334
+ await new Promise((r) => setTimeout(r, 20));
335
+ expect(clientSideResult[0]).to.eql(obj1); // NOTE: 1
336
+ expect(clientSideResult[1]).to.eql(obj2);
337
+
338
+ // NOTE: 1 – Second replacement+live projection being concurrent means that the previous live object projection has not been terminated by us on the server.
339
+
340
+ // Server did specify "done: false" in last message
341
+ expect(liveResponseB.readyState).to.equal('live');
342
+
343
+ // ---- Third Live Object ----
344
+
345
+ // Replacement + live-projection on the server side
346
+ await liveResponseA.replaceWith(obj3, { concurrent: false });
347
+
348
+ // Inspect on the client side
349
+ await new Promise((r) => setTimeout(r, 20));
350
+ expect(clientSideResult[2]).to.eql(obj3);
351
+ expect(liveResponseB.concurrent).to.false;
352
+
353
+ // Mutate on the server side
354
+ Observer.set(obj2, 'c', 6);
355
+
356
+ // Inspect on the client side
357
+ await new Promise((r) => setTimeout(r, 20));
358
+ expect(clientSideResult[1]).to.eql(obj2);
359
+
360
+ // Server did not specify "done: false" in last message
361
+ expect(liveResponseB.readyState).to.equal('done');
362
+
363
+ // ---- Concurrency & Continuity ----
364
+
365
+ // Mutate on the server side
366
+ Observer.set(obj1, 'd', 11); // NOTE: 1
367
+ Observer.set(obj2, 'd', 21); // NOTE: 1
368
+ Observer.set(obj3, 'd', 22); // NOTE: 2
369
+
370
+ // Inspect on the client side
371
+ await new Promise((r) => setTimeout(r, 20));
372
+ expect(clientSideResult[0]).to.not.eql(obj1); // NOTE: 1
373
+ expect(clientSideResult[1]).to.not.eql(obj2); // NOTE: 1
374
+ expect(clientSideResult[2]).to.eql(obj3); // NOTE: 2
375
+
376
+ // NOTE: 1 – Third replacement+live projection being non-concurrent means that the previous live object projection has been terminated by us on the server and sp should be discontinued on the client.
377
+ // NOTE: 2 – Third replacement+live projection remains live even tho it's last in the series of "responses" – denoted by the server's "response.done" message
378
+ // – until the server terminates the live projection or until port closes.
379
+ });
380
+
381
+ it('should project Live Objects – advanced pipelines', async function () {
382
+ const [liveResponseA, liveResponseB, clientSideResult] = await setupWire();
383
+
384
+ const obj1 = { a: 1, b: 2 };
385
+ const obj2 = { a: 2, b: 3 };
386
+ const obj3 = { a: 3, b: 4 };
387
+
388
+ // ---- First Live Object ----
389
+
390
+ // Replacement + live-projection on the server side
391
+ await liveResponseA.replaceWith(obj1, async ($obj1) => {
392
+ await new Promise((r) => setTimeout(r, 0));
393
+ $obj1.c = 3;
394
+ }, { concurrent: true, done: false });
395
+
396
+ // Inspect on the client side
397
+ await new Promise((r) => setTimeout(r, 20));
398
+ expect(clientSideResult[0]).to.eql(obj1);
399
+ expect(liveResponseB.concurrent).to.true;
400
+
401
+ // ---- Second Live Object ----
402
+
403
+ // Replacement + live-projection on the server side
404
+ await liveResponseA.replaceWith((async function* gen() {
405
+ yield obj2;
406
+
407
+ await new Promise((r) => setTimeout(r, 2));
408
+ // Mutate obj2 even after having returned it
409
+ Observer.set(obj2, 'c', 6);
410
+ await new Promise((r) => setTimeout(r, 2));
411
+
412
+ (new Promise((r) => setTimeout(r, 2))).then(() => {
413
+ // Mutate obj3 even after having returned it
414
+ Observer.set(obj3, 'c', 60);
415
+ });
416
+
417
+ return obj3;
418
+ })(), { concurrent: true, done: false });
419
+
420
+ // Inspect on the client side
421
+ await new Promise((r) => setTimeout(r, 30));
422
+ expect(clientSideResult[1]).to.eql(obj2);
423
+ expect(clientSideResult[2]).to.eql(obj3);
424
+
425
+ // Replace with a LiveResponse instance itself
426
+
427
+ const obj4 = { a: 2, b: 3 };
428
+ const obj5 = { a: 3, b: 4 };
429
+
430
+ await liveResponseA.replaceWith(LiveResponse.from((async function* gen() {
431
+ yield obj4;
432
+
433
+ await new Promise((r) => setTimeout(r, 2));
434
+ // Mutate obj2 even after having returned it
435
+ Observer.set(obj4, 'c', 6);
436
+ await new Promise((r) => setTimeout(r, 2));
437
+
438
+ (new Promise((r) => setTimeout(r, 2))).then(() => {
439
+ // Mutate obj3 even after having returned it
440
+ Observer.set(obj5, 'c', 60);
441
+ });
442
+
443
+ return obj5;
444
+ })()), { concurrent: true });
445
+
446
+ // Inspect on the client side
447
+ await new Promise((r) => setTimeout(r, 30));
448
+ expect(clientSideResult[3]).to.eql(obj4);
449
+ expect(clientSideResult[4]).to.eql(obj5);
450
+
451
+ // The nested LiveResponse is essentially flattened out
452
+
453
+ // We should be done at this point
454
+ expect(liveResponseB.readyState).to.equal('done');
455
+ });
456
+ });
457
+
458
+ });
459
+
@@ -1,80 +0,0 @@
1
- import { _isString, _isNumeric, _isTypeObject } from '@webqit/util/js/index.js';
2
-
3
- export class URLSearchParamsPlus extends URLSearchParams {
4
-
5
- // Parse a search params string into an object
6
- static eval(targetObject, str, delim = '&') {
7
- str = str || '';
8
- (str.startsWith('?') ? str.substr(1) : str)
9
- .split(delim).filter(q => q).map(q => q.split('=').map(q => q.trim()))
10
- .forEach(q => this.set(targetObject, q[0], decodeURIComponent(q[1])));
11
- return targetObject;
12
- }
13
-
14
- // Stringify an object into a search params string
15
- static stringify(targetObject, delim = '&') {
16
- const q = [];
17
- Object.keys(targetObject).forEach(key => {
18
- this.reduceValue(targetObject[key], key, (_value, _pathNotation, suggestedKeys = undefined) => {
19
- if (suggestedKeys) return suggestedKeys;
20
- q.push(`${_pathNotation}=${encodeURIComponent(_value)}`);
21
- });
22
- });
23
- return q.join(delim);
24
- }
25
-
26
- // Get value by path notation
27
- static get(targetObject, pathNotation) {
28
- return this.reducePath(pathNotation, targetObject, (key, _targetObject) => {
29
- if (!_targetObject && _targetObject !== 0) return;
30
- return _targetObject[key];
31
- });
32
- }
33
-
34
- // Set value by path notation
35
- static set(targetObject, pathNotation, value) {
36
- this.reducePath(pathNotation, targetObject, function(_key, _targetObject, suggestedBranch = undefined) {
37
- let _value = value;
38
- if (suggestedBranch) { _value = suggestedBranch; }
39
- if (_key === '' && Array.isArray(_targetObject)) {
40
- _targetObject.push(_value);
41
- } else {
42
- _targetObject[_key] = _value;
43
- }
44
- return _value;
45
- });
46
- }
47
-
48
- // Resolve a value to its leaf nodes
49
- static reduceValue(value, contextPath, callback) {
50
- if (_isTypeObject(value)) {
51
- let suggestedKeys = Object.keys(value);
52
- let keys = callback(value, contextPath, suggestedKeys);
53
- if (Array.isArray(keys)) {
54
- return keys.forEach(key => {
55
- this.reduceValue(value[key], contextPath ? `${contextPath}[${key}]` : key, callback);
56
- });
57
- }
58
- }
59
- callback(value, contextPath);
60
- }
61
-
62
- // Resolve a path to its leaf index
63
- static reducePath(pathNotation, contextObject, callback) {
64
- if (_isString(pathNotation) && pathNotation.endsWith(']') && _isTypeObject(contextObject)) {
65
- let [ key, ...rest ] = pathNotation.split('[');
66
- if (_isNumeric(key)) { key = parseInt(key); }
67
- rest = rest.join('[').replace(']', '');
68
- let branch;
69
- if (key in contextObject) {
70
- branch = contextObject[key];
71
- } else {
72
- let suggestedBranch = rest === '' || _isNumeric(rest.split('[')[0]) ? [] : {};
73
- branch = callback(key, contextObject, suggestedBranch);
74
- }
75
- return this.reducePath(rest, branch, callback);
76
- }
77
- if (_isNumeric(pathNotation)) { pathNotation = parseInt(pathNotation); }
78
- return callback(pathNotation, contextObject);
79
- }
80
- }