@webqit/fetch-plus 0.1.2 → 0.1.3

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.
@@ -1,13 +1,13 @@
1
1
  import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
2
- import { BroadcastChannelPlus, WebSocketPort, MessagePortPlus, Observer } from '@webqit/port-plus';
3
- import { isTypeStream, _meta, _wq } from './core.js';
2
+ import { Observer, ListenerRegistry, Descriptor } from '@webqit/observer';
3
+ import { BroadcastChannelPlus, WebSocketPort, MessagePortPlus } from '@webqit/port-plus';
4
+ import { isTypeStream, _meta, _wq } from './messageParserMixin.js';
4
5
  import { ResponsePlus } from './ResponsePlus.js';
5
- import { HeadersPlus } from './HeadersPlus.js';
6
6
 
7
7
  export class LiveResponse extends EventTarget {
8
8
 
9
9
  static get xHeaderName() {
10
- return 'X-Background-Messaging-Port';
10
+ return 'X-Message-Port';
11
11
  }
12
12
 
13
13
  static test(unknown) {
@@ -27,33 +27,41 @@ export class LiveResponse extends EventTarget {
27
27
  return 'Default';
28
28
  }
29
29
 
30
- static hasBackgroundPort(respone) {
30
+ static hasPort(respone) {
31
31
  return !!respone.headers?.get?.(this.xHeaderName)?.trim();
32
32
  }
33
33
 
34
- static getBackgroundPort(respone) {
34
+ static getPort(respone, autoStart = false) {
35
35
  if (!/Response/.test(this.test(respone))) {
36
36
  return;
37
37
  }
38
38
  const responseMeta = _meta(respone);
39
39
 
40
- if (!responseMeta.has('background_port')) {
40
+ if (!responseMeta.has('port')) {
41
41
  const value = respone.headers.get(this.xHeaderName)?.trim();
42
42
  if (!value) return;
43
43
 
44
- const [proto, portID] = value.split(':');
45
- if (!['ws', 'br'].includes(proto)) {
46
- throw new Error(`Unknown background messaging protocol: ${value}`);
44
+ const [, scheme, portID] = /^(socket|channel):\/\/(.*)$/.exec(value) || [];
45
+ if (!scheme || !portID) {
46
+ throw new Error(`Unknown port messaging protocol: ${value}`);
47
47
  }
48
48
 
49
- const backgroundPort = proto === 'br'
50
- ? new BroadcastChannelPlus(portID, { autoStart: false, postAwaitsOpen: true, clientServerMode: 'client' })
51
- : new WebSocketPort(portID, { autoStart: false, naturalOpen: false, postAwaitsOpen: true });
49
+ const port = scheme === 'channel'
50
+ ? new BroadcastChannelPlus(portID, { autoStart, postAwaitsOpen: true, clientServerMode: 'client' })
51
+ : new WebSocketPort(portID, { autoStart, naturalOpen: false, postAwaitsOpen: true });
52
52
 
53
- responseMeta.set('background_port', backgroundPort);
53
+ responseMeta.set('port', port);
54
54
  }
55
55
 
56
- return responseMeta.get('background_port');
56
+ return responseMeta.get('port');
57
+ }
58
+
59
+ static attachPort(respone, port) {
60
+ if (!(port instanceof MessagePortPlus)) {
61
+ throw new Error('Client must be a MessagePortPlus interface');
62
+ }
63
+ const responseMeta = _meta(respone);
64
+ responseMeta.set('port', port);
57
65
  }
58
66
 
59
67
  static from(data, ...args) {
@@ -67,9 +75,18 @@ export class LiveResponse extends EventTarget {
67
75
 
68
76
  [Symbol.toStringTag] = 'LiveResponse';
69
77
 
78
+ #listenersRegistry;
79
+
70
80
  constructor(body, ...args) {
71
81
  super();
72
- this.#replaceWith(body, ...args);
82
+ this.#listenersRegistry = ListenerRegistry.getInstance(this, true);
83
+
84
+ const readyStateInternals = getReadyStateInternals.call(this);
85
+ const frame = readyStateInternals.now;
86
+
87
+ this.#replaceWith(frame, body, ...args).catch((e) => {
88
+ frame.reject(e);
89
+ });
73
90
  }
74
91
 
75
92
  /* Level 1 props */
@@ -77,9 +94,12 @@ export class LiveResponse extends EventTarget {
77
94
  #body = null;
78
95
  get body() { return this.#body; }
79
96
 
80
- get bodyUsed() { return false; }
97
+ #concurrent = false;
98
+ get concurrent() { return this.#concurrent; }
81
99
 
82
- #headers = new HeadersPlus;
100
+ get bodyUsed() { return true; }
101
+
102
+ #headers = new Headers;
83
103
  get headers() { return this.#headers; }
84
104
 
85
105
  #status = 200;
@@ -99,28 +119,21 @@ export class LiveResponse extends EventTarget {
99
119
  #url = null;
100
120
  get url() { return this.#url; }
101
121
 
102
- get ok() { return this.#status >= 200 && this.#status < 299; }
103
-
104
- async arrayBuffer() { throw new Error(`LiveResponse does not support the arrayBuffer() method.`); }
105
-
106
- async formData() { throw new Error(`LiveResponse does not support the formData() method.`); }
107
-
108
- async json() { throw new Error(`LiveResponse does not support the json() method.`); }
109
-
110
- async text() { throw new Error(`LiveResponse does not support the text() method.`); }
111
-
112
- async blob() { throw new Error(`LiveResponse does not support the blob() method.`); }
122
+ get ok() { return !!(this.#status >= 200 && this.#status < 299); }
113
123
 
114
- async bytes() { throw new Error(`LiveResponse does not support the bytes() method.`); }
124
+ async now() {
125
+ const readyStateInternals = getReadyStateInternals.call(this);
126
+ return readyStateInternals.now.promise;
127
+ }
115
128
 
116
129
  /* Level 3 props */
117
130
 
118
- get background() { return this.constructor.getBackgroundPort(this); }
131
+ get port() { return this.constructor.getPort(this, true); }
119
132
 
120
133
  // Lifecycle
121
134
 
122
135
  #abortController = new AbortController;
123
- get signal() { return this.#abortController.signal; }
136
+ #concurrencyAbortController = new AbortController;
124
137
 
125
138
  get readyState() {
126
139
  const readyStateInternals = getReadyStateInternals.call(this);
@@ -129,16 +142,20 @@ export class LiveResponse extends EventTarget {
129
142
  }
130
143
 
131
144
  readyStateChange(query) {
132
- if (!['live', 'done'].includes(query)) {
145
+ if (!['live', 'now', 'done'].includes(query)) {
133
146
  throw new Error(`Invalid readyState query "${query}"`);
134
147
  }
135
148
  const readyStateInternals = getReadyStateInternals.call(this);
136
149
  return readyStateInternals[query].promise;
137
150
  }
138
151
 
139
- disconnect() {
152
+ disconnect(dispose = false) {
140
153
  this.#abortController.abort();
141
154
  this.#abortController = new AbortController;
155
+ if (dispose) {
156
+ this.#concurrencyAbortController.abort();
157
+ this.#concurrencyAbortController = new AbortController;
158
+ }
142
159
  }
143
160
 
144
161
  #currentFramePromise;
@@ -161,6 +178,7 @@ export class LiveResponse extends EventTarget {
161
178
  readyStateInternals.done.reject(e);
162
179
  }
163
180
  });
181
+ return promise;
164
182
  }
165
183
 
166
184
  async replaceWith(body, ...args) {
@@ -168,65 +186,101 @@ export class LiveResponse extends EventTarget {
168
186
  throw new Error('Response already done.');
169
187
  }
170
188
  this.disconnect(); // Disconnect from existing source if any
171
- await this.#replaceWith(body, ...args);
189
+ await this.#replaceWith(null, body, ...args);
172
190
  }
173
191
 
174
- async #replaceWith(body, ...args) {
192
+ async #replaceWith(__frame, body, ...args) {
193
+ const readyStateInternals = getReadyStateInternals.call(this);
194
+ const frame = __frame || readyStateInternals.now.refresh();
195
+
196
+ // ----------- Promise input
197
+
175
198
  if (body instanceof Promise) {
176
- this.#extendLifecycle(body);
177
- return await new Promise((resolve, reject) => {
178
- let aborted = false;
199
+ return this.#extendLifecycle(new Promise((resolve, reject) => {
179
200
  this.#abortController.signal.addEventListener('abort', () => {
180
- aborted = true
201
+ frame.aborted = true;
181
202
  resolve();
182
203
  });
204
+
183
205
  body.then(async (resolveData) => {
184
- if (aborted) return;
185
- await this.#replaceWith(resolveData, ...args);
206
+ await this.#replaceWith(frame, resolveData, ...args);
186
207
  resolve();
187
- });
188
- body.catch((e) => reject(e));
189
- });
208
+ }).catch((e) => reject(e));
209
+ }));
190
210
  }
191
211
 
192
212
  // ----------- Formatters
193
213
 
194
- const directReplaceWith = (responseLike) => {
195
- const $body = responseLike.body;
214
+ const directReplaceWith = (__frame, responseFrame) => {
215
+ responseFrame = Object.freeze({
216
+ ...responseFrame,
217
+ ok: !!(responseFrame.status >= 200 && responseFrame.status < 299),
218
+ bodyUsed: true,
219
+ });
220
+
221
+ if (__frame?.aborted) {
222
+ __frame.resolve(responseFrame);
223
+ return;
224
+ }
196
225
 
197
- this.#status = responseLike.status;
198
- this.#statusText = responseLike.statusText;
226
+ const frame = __frame || readyStateInternals.now.refresh();
227
+
228
+ const $body = responseFrame.body;
229
+
230
+ this.#status = responseFrame.status;
231
+ this.#statusText = responseFrame.statusText;
199
232
 
200
233
  for (const [name] of [/*IMPORTANT*/...this.#headers.entries()]) { // for some reason, some entries not produced when not spread
201
234
  this.#headers.delete(name);
202
235
  }
203
- for (const [name, value] of responseLike.headers.entries()) {
236
+ for (const [name, value] of responseFrame.headers.entries()) {
204
237
  this.#headers.append(name, value);
205
238
  }
206
239
 
207
- this.#type = responseLike.type;
208
- this.#redirected = responseLike.redirected;
209
- this.#url = responseLike.url;
240
+ this.#type = responseFrame.type;
241
+ this.#redirected = responseFrame.redirected;
242
+ this.#url = responseFrame.url;
210
243
 
244
+ const bodyOld = this.#body;
211
245
  this.#body = $body;
246
+ this.#concurrent = !!responseFrame.concurrent;
212
247
 
213
- // Must come after all property assignments above because it fires events
214
- Observer.defineProperty(this, 'body', { get: () => this.#body, enumerable: false, configurable: true });
248
+ if (!this.#concurrent) {
249
+ this.#concurrencyAbortController.abort();
250
+ this.#concurrencyAbortController = new AbortController;
251
+ }
252
+
253
+ const descriptor = new Descriptor(this, {
254
+ type: 'set',
255
+ key: 'body',
256
+ value: $body,
257
+ oldValue: bodyOld,
258
+ isUpdate: true,
259
+ related: [],
260
+ operation: 'set',
261
+ detail: null,
262
+ });
263
+
264
+ // Must come first so that observers below here see this state
215
265
 
216
- const readyStateInternals = getReadyStateInternals.call(this);
217
266
  readyStateInternals.live.state = true;
218
- readyStateInternals.live.resolve();
267
+ readyStateInternals.live.resolve(this);
219
268
 
220
- this.dispatchEvent(new Event('replace'));
269
+ // May trigger "done" ready state
270
+ frame.resolve(responseFrame);
271
+
272
+ // Must come after all property assignments above because it fires events
273
+ this.#listenersRegistry.emit([descriptor]);
274
+ this.dispatchEvent(new ReplaceEvent(responseFrame));
221
275
  };
222
276
 
223
- const wrapReplaceWith = async (body, options) => {
224
- directReplaceWith({
277
+ const wrapReplaceWith = (frame, body, options) => {
278
+ directReplaceWith(frame, {
225
279
  body,
226
280
  status: 200,
227
281
  statusText: '',
228
- headers: new Headers,
229
282
  ...options,
283
+ headers: options.headers instanceof Headers ? options.headers : new Headers(options.headers || {}),
230
284
  type: 'basic',
231
285
  redirected: false,
232
286
  url: null
@@ -235,21 +289,22 @@ export class LiveResponse extends EventTarget {
235
289
 
236
290
  // ----------- "Response" handler
237
291
 
238
- const execReplaceWithResponse = async (response, options) => {
292
+ const execReplaceWithResponse = async (frame, response, options) => {
239
293
  let body, jsonSuccess = false;
240
294
  try {
241
295
  body = response instanceof Response
242
- ? await ResponsePlus.prototype.parse.call(response, { to: 'json' })
243
- : response.body;
296
+ ? await ResponsePlus.prototype.any.call(response, { to: 'json' })
297
+ : (await response.readyStateChange('live')).body;
244
298
  jsonSuccess = true;
245
299
  } catch (e) {
246
- body = response.body;
300
+ body = await ResponsePlus.prototype.any.call(response);
247
301
  }
248
- directReplaceWith({
302
+ directReplaceWith(frame, {
249
303
  body,
250
304
  status: response.status,
251
305
  statusText: response.statusText,
252
306
  headers: response.headers,
307
+ concurrent: response.concurrent, // for response === LiveResponse
253
308
  ...options,
254
309
  type: response.type,
255
310
  redirected: response.redirected,
@@ -257,54 +312,61 @@ export class LiveResponse extends EventTarget {
257
312
  });
258
313
 
259
314
  if (this.constructor.test(response) === 'LiveResponse') {
260
- response.addEventListener('replace', () => {
261
- directReplaceWith(response)
262
- }, { signal: this.#abortController.signal });
263
- return await response.readyStateChange('done');
315
+ const replaceHandler = () => {
316
+ wrapReplaceWith(null, response.body, response);
317
+ };
318
+ response.addEventListener('replace', replaceHandler, { signal: this.#abortController.signal });
319
+ await response.readyStateChange('done');
320
+ response.removeEventListener('replace', replaceHandler);
321
+ return response;
264
322
  }
265
323
 
266
- if (this.hasBackgroundPort(response)) {
267
- const backgroundPort = this.constructor.getBackgroundPort(response);
324
+ if (this.constructor.hasPort(response)) {
325
+ const port = this.constructor.getPort(response);
326
+ port.start();
327
+
268
328
  // Bind to upstream mutations
269
- let undoInitialProjectMutations;
270
329
  if (jsonSuccess) {
271
- undoInitialProjectMutations = donePromise.projectMutations({
330
+ port.projectMutations({
272
331
  from: 'initial_response',
273
332
  to: body,
274
- signal: this.#abortController.signal
333
+ signal: this.#concurrencyAbortController.signal
275
334
  });
276
335
  }
336
+
277
337
  // Bind to replacements
278
- backgroundPort.addEventListener('response.replace', (e) => {
279
- undoInitialProjectMutations?.();
280
- undoInitialProjectMutations = null;
281
-
282
- directReplaceWith(e.data);
283
- }, { signal: this.#abortController.signal });
284
- // Wait until done
285
- return await backgroundPort.readyStateChange('close');
338
+ return new Promise((resolve) => {
339
+ const replaceHandler = (e) => {
340
+ const { body, ...options } = e.data;
341
+ wrapReplaceWith(null, body, { ...options });
342
+ };
343
+ port.addEventListener('response.replace', replaceHandler, { signal: this.#abortController.signal });
344
+ port.addEventListener('response.done', () => {
345
+ port.removeEventListener('response.replace', replaceHandler);
346
+ resolve(this);
347
+ }, { once: true });
348
+ port.readyStateChange('close').then(resolve);
349
+ });
286
350
  }
287
-
288
- return Promise.resolve();
289
351
  };
290
352
 
291
353
  // ----------- "Generator" handler
292
354
 
293
- const execReplaceWithGenerator = async (gen, options) => {
355
+ const execReplaceWithGenerator = async (frame, gen, options) => {
294
356
  const firstFrame = await gen.next();
295
357
  const firstValue = await firstFrame.value;
296
358
 
297
- await this.#replaceWith(firstValue, { done: firstFrame.done, ...options });
359
+ await this.#replaceWith(frame, firstValue, { done: firstFrame.done, ...options });
298
360
  // this is the first time options has a chance to be applied
299
361
 
300
- let frame = firstFrame;
362
+ let generatorFrame = firstFrame;
301
363
  let value = firstValue;
302
364
 
303
- while (!frame.done && !this.#abortController.signal.aborted) {
304
- frame = await gen.next();
305
- value = await frame.value;
365
+ while (!generatorFrame.done && !this.#abortController.signal.aborted) {
366
+ generatorFrame = await gen.next();
367
+ value = await generatorFrame.value;
306
368
  if (!this.#abortController.signal.aborted) {
307
- await this.#replaceWith(value, { done: options.done === false ? false : frame.done });
369
+ await this.#replaceWith(null, value, { concurrent: options.concurrent, done: options.done === false ? false : generatorFrame.done });
308
370
  // top-level false need to be respected: means keep instance alive even when done
309
371
  }
310
372
  }
@@ -312,14 +374,14 @@ export class LiveResponse extends EventTarget {
312
374
 
313
375
  // ----------- "LiveProgramHandle" handler
314
376
 
315
- const execReplaceWithLiveProgramHandle = async (liveProgramHandle, options) => {
316
- await this.#replaceWith(liveProgramHandle.value, options);
377
+ const execReplaceWithLiveProgramHandle = async (frame, liveProgramHandle, options) => {
378
+ await this.#replaceWith(frame, liveProgramHandle.value, options);
317
379
  // this is the first time options has a chance to be applied
318
380
 
319
381
  Observer.observe(
320
382
  liveProgramHandle,
321
383
  'value',
322
- (e) => this.#replaceWith(e.value, { done: false }),
384
+ (e) => this.#replaceWith(null, e.value, { concurrent: options.concurrent, done: false }),
323
385
  // we're never done unless explicitly aborted
324
386
  { signal: this.#abortController.signal }
325
387
  );
@@ -329,110 +391,109 @@ export class LiveResponse extends EventTarget {
329
391
 
330
392
  // ----------- Procesing time
331
393
 
332
- const options = _isObject(args[0]/* !ORDER 1 */) ? { ...args.shift() } : {};
333
- const frameClosure = typeof args[0]/* !ORDER 2 */ === 'function' ? args.shift() : null;
394
+ const frameClosure = typeof args[0]/* !ORDER 1 */ === 'function' ? args.shift() : null;
395
+ const frameOptions = _isObject(args[0]/* !ORDER 2 */) ? { ...args.shift() } : {};
334
396
 
335
- if ('status' in options) {
336
- options.status = parseInt(options.status);
337
- if (options.status < 200 || options.status > 599) {
338
- throw new Error(`The status provided (${options.status}) is outside the range [200, 599].`);
397
+ if ('status' in frameOptions) {
398
+ frameOptions.status = parseInt(frameOptions.status);
399
+ if (frameOptions.status < 200 || frameOptions.status > 599) {
400
+ throw new Error(`The status provided (${frameOptions.status}) is outside the range [200, 599].`);
339
401
  }
340
402
  }
341
- if ('statusText' in options) {
342
- options.statusText = String(options.statusText);
403
+ if ('statusText' in frameOptions) {
404
+ frameOptions.statusText = String(frameOptions.statusText);
343
405
  }
344
- if (options.headers && !(options.headers instanceof Headers)) {
345
- options.headers = new Headers(options.headers);
406
+ if (frameOptions.headers && !(frameOptions.headers instanceof Headers)) {
407
+ frameOptions.headers = new Headers(frameOptions.headers);
408
+ }
409
+ if ('concurrent' in frameOptions) {
410
+ frameOptions.concurrent = Boolean(frameOptions.concurrent);
346
411
  }
347
412
 
348
413
  // ----------- Dispatch time
349
414
 
350
- let donePromise;
351
-
352
415
  if (/Response/.test(this.constructor.test(body))) {
353
416
  if (frameClosure) {
354
417
  throw new Error(`frameClosure is not supported for responses.`);
355
418
  }
356
- donePromise = await execReplaceWithResponse(body, options);
419
+ frame.donePromise = execReplaceWithResponse(frame, body, frameOptions);
357
420
  } else if (this.constructor.test(body) === 'Generator') {
358
421
  if (frameClosure) {
359
422
  throw new Error(`frameClosure is not supported for generators.`);
360
423
  }
361
- donePromise = await execReplaceWithGenerator(body, options);
424
+ frame.donePromise = execReplaceWithGenerator(frame, body, frameOptions);
362
425
  } else if (this.constructor.test(body) === 'LiveProgramHandle') {
363
426
  if (frameClosure) {
364
427
  throw new Error(`frameClosure is not supported for live program handles.`);
365
428
  }
366
- donePromise = await execReplaceWithLiveProgramHandle(body, options);
429
+ frame.donePromise = execReplaceWithLiveProgramHandle(frame, body, frameOptions);
367
430
  } else {
368
- donePromise = wrapReplaceWith(body, options);
431
+ frame.donePromise = Promise.resolve(wrapReplaceWith(frame, body, frameOptions));
369
432
  if (frameClosure) {
370
433
  const reactiveProxy = _isTypeObject(body) && !isTypeStream(body)
371
434
  ? Observer.proxy(body, { chainable: true, membrane: body })
372
435
  : body;
373
- donePromise = Promise.resolve(frameClosure.call(this, reactiveProxy));
436
+ frame.donePromise = Promise.resolve(frameClosure.call(this, reactiveProxy, this.#concurrencyAbortController.signal));
374
437
  }
375
438
  }
376
439
 
377
440
  // Lifecycle time
378
441
 
379
- this.#extendLifecycle(options.done === false ? new Promise(() => { }) : donePromise);
380
-
442
+ this.#extendLifecycle(frameOptions.done === false ? new Promise(() => { }) : frame.donePromise);
443
+
381
444
  return await new Promise((resolve, reject) => {
382
- this.#abortController.signal.addEventListener('abort', resolve);
383
- donePromise.then(() => resolve());
384
- donePromise.catch((e) => reject(e));
445
+ this.#abortController.signal.addEventListener('abort', () => resolve(false));
446
+ frame.donePromise.then(() => resolve(true)).catch(reject);
385
447
  });
386
448
  }
387
449
 
388
450
  // ----------- Conversions
389
451
 
390
- toResponse({ client: clientPort, signal: abortSignal } = {}) {
452
+ toResponse({ port: clientPort, signal: abortSignal } = {}) {
391
453
  if (clientPort && !(clientPort instanceof MessagePortPlus)) {
392
454
  throw new Error('Client must be a MessagePortPlus interface');
393
455
  }
394
456
 
395
457
  const response = ResponsePlus.from(this.body, {
396
- status: this.status,
397
- statusText: this.statusText,
398
- headers: this.headers,
458
+ status: this.#status,
459
+ statusText: this.#statusText,
460
+ headers: this.#headers,
399
461
  });
400
462
 
401
463
  const responseMeta = _meta(this);
402
464
  _wq(response).set('meta', responseMeta);
403
465
 
404
- if (clientPort && this.readyState === 'live') {
405
- let undoInitialProjectMutations;
406
- if (_isTypeObject(this.body) && !isTypeStream(this.body)) {
407
- undoInitialProjectMutations = clientPort.projectMutations({
408
- from: this.body,
409
- to: 'initial_response',
410
- signal: abortSignal/* stop observing mutations on body when we abort */
411
- });
412
- }
466
+ if (!clientPort) return response;
413
467
 
414
- const replaceHandler = () => {
415
- undoInitialProjectMutations?.();
416
- undoInitialProjectMutations = null;
468
+ if (_isTypeObject(this.#body) && !isTypeStream(this.#body)) {
469
+ clientPort.projectMutations({
470
+ from: this.#body,
471
+ to: 'initial_response',
472
+ signal: AbortSignal.any([this.#concurrencyAbortController.signal].concat(abortSignal || []))/* stop observing mutations on body when we abort */
473
+ });
474
+ }
417
475
 
418
- const headers = Object.fromEntries([...this.headers.entries()]);
476
+ const replaceHandler = () => {
477
+ const headers = Object.fromEntries([...this.headers.entries()]);
419
478
 
420
- if (headers?.['set-cookie']) {
421
- delete headers['set-cookie'];
422
- console.warn('Warning: The "set-cookie" header is not supported for security reasons and has been removed from the response.');
423
- }
479
+ if (headers?.['set-cookie']) {
480
+ delete headers['set-cookie'];
481
+ console.warn('Warning: The "set-cookie" header is not supported for security reasons and has been removed from the response.');
482
+ }
424
483
 
425
- clientPort.postMessage({
426
- body: this.body,
427
- status: this.status,
428
- statusText: this.statusText,
429
- headers,
430
- done: this.readyState === 'done',
431
- }, { type: 'response.replace', live: true/*gracefully ignored if not an object*/, signal: this.#abortController.signal/* stop observing mutations on body a new body takes effect */ });
432
- };
484
+ clientPort.postMessage({
485
+ body: this.#body,
486
+ status: this.#status,
487
+ statusText: this.#statusText,
488
+ headers,
489
+ concurrent: this.#concurrent,
490
+ }, { type: 'response.replace', live: true/*gracefully ignored if not an object*/, signal: AbortSignal.any([this.#concurrencyAbortController.signal].concat(abortSignal || []))/* stop observing mutations on body a new body takes effect */ });
491
+ };
433
492
 
434
- this.addEventListener('replace', replaceHandler, { signal: abortSignal/* stop listening when we abort */ });
435
- }
493
+ this.addEventListener('replace', replaceHandler, { signal: abortSignal/* stop listening when we abort */ });
494
+ this.readyStateChange('done').then(() => {
495
+ clientPort.postMessage(null, { type: 'response.done' });
496
+ });
436
497
 
437
498
  return response;
438
499
  }
@@ -448,8 +509,8 @@ export class LiveResponse extends EventTarget {
448
509
 
449
510
  toLiveProgramHandle({ signal: abortSignal } = {}) {
450
511
  const handle = new LiveProgramHandleX;
451
-
452
- const replaceHandler = () => Observer.defineProperty(handle, 'value', { value: this.body, enumerable: true, configurable: true });
512
+
513
+ const replaceHandler = () => Observer.defineProperty(handle, 'value', { value: this.body, enumerable: false, configurable: true });
453
514
  this.addEventListener('replace', replaceHandler, { signal: abortSignal });
454
515
  replaceHandler();
455
516
 
@@ -460,7 +521,7 @@ export class LiveResponse extends EventTarget {
460
521
  const clone = new this.constructor();
461
522
 
462
523
  const responseMeta = _meta(this);
463
- _wq(clone).set('meta', responseMeta);
524
+ _wq(clone).set('meta', new Map(responseMeta));
464
525
 
465
526
  clone.replaceWith(this, init);
466
527
  return clone;
@@ -477,18 +538,35 @@ export function getReadyStateInternals() {
477
538
  const portPlusMeta = _meta(this);
478
539
  if (!portPlusMeta.has('readystate_registry')) {
479
540
  const $ref = (o) => {
480
- o.promise = new Promise((res, rej) => (o.resolve = () => res(this), o.reject = rej));
541
+ o.promise = new Promise((res, rej) => (o.resolve = res, o.reject = rej));
481
542
  return o;
482
543
  };
483
- portPlusMeta.set('readystate_registry', {
544
+ const states = {
484
545
  live: $ref({}),
485
546
  done: $ref({}),
486
- });
547
+ };
548
+ (function refresh() {
549
+ states.now = $ref({});
550
+ states.now.refresh = refresh;
551
+ return states.now;
552
+ })();
553
+ portPlusMeta.set('readystate_registry', states);
487
554
  }
488
555
  return portPlusMeta.get('readystate_registry');
489
556
  }
490
557
 
491
- class LiveProgramHandleX {
558
+ export class ReplaceEvent extends Event {
559
+
560
+ #data;
561
+ get data() { return this.#data; }
562
+
563
+ constructor(responseFrame) {
564
+ super('replace');
565
+ this.#data = responseFrame;
566
+ }
567
+ }
568
+
569
+ export class LiveProgramHandleX {
492
570
  [Symbol.toStringTag] = 'LiveProgramHandle';
493
571
  abort() { }
494
572
  }