@entrinsik/vite-plugin-informer 2.11.0 → 2.13.0

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,6 +1,8 @@
1
1
  /**
2
2
  * Dev copy of the injected app streams client (I5-12979):
3
- * `__INFORMER__.upload(file, opts)` and `__INFORMER__.downloadUrl(id, filename)`.
3
+ * `__INFORMER__.upload(file, opts)`, `__INFORMER__.downloadUrl(id, filename)` and
4
+ * `__INFORMER__.streams` (I5-13030: status / list / discard, plus upload()'s onEvent
5
+ * and task.created).
4
6
  *
5
7
  * VERBATIM copy of modules/app/routes/lib/html-utils.js#generateStreamsHelper
6
8
  * in its origin-mode form (base "", plain window.fetch / XHR transport — the
@@ -11,7 +13,7 @@
11
13
  * that test says exactly what to paste here. The dev page must upload the way
12
14
  * a deployed one does.
13
15
  *
14
- * @returns {string} JavaScript expression evaluating to { upload, downloadUrl }
16
+ * @returns {string} JavaScript expression evaluating to { upload, downloadUrl, streams }
15
17
  */
16
18
  export function streamsClientSource() {
17
19
  return `(function (base, transport) {
@@ -80,6 +82,24 @@ export function streamsClientSource() {
80
82
  return [opts.filename || file.name || '', file.size, file.lastModified || 0].join(':');
81
83
  }
82
84
 
85
+ // The transfer's event stream for the page (I5-13030): what the helper
86
+ // already knows at each step, handed out rather than inferred from a
87
+ // percentage. A listener's own throw must not fail the transfer it is
88
+ // watching, so it is rethrown on a fresh tick — still reaching
89
+ // window.onerror and the console, but not the chunk promise chain.
90
+ function notify (opts, event) {
91
+ if (typeof opts.onEvent !== 'function') return;
92
+ try {
93
+ opts.onEvent(event);
94
+ } catch (err) {
95
+ // Say so where it happened: the deferred rethrow below reaches
96
+ // window.onerror and nothing else, by which point the event it
97
+ // came from is long gone.
98
+ try { console.error('[informer] upload onEvent listener threw for ' + event.type + '; the transfer continues', err); } catch (e) {}
99
+ setTimeout(function () { throw err; }, 0);
100
+ }
101
+ }
102
+
83
103
  function upload (file, opts) {
84
104
  opts = opts || {};
85
105
  if (!file || typeof file.slice !== 'function' || typeof file.size !== 'number') {
@@ -97,6 +117,20 @@ export function streamsClientSource() {
97
117
  var task = { id: opts.resume || null, abort: function () { controller.abort(); } };
98
118
  var loaded = {};
99
119
 
120
+ // Settles with the upload's geometry the moment the store has reserved
121
+ // it — before a chunk moves — or with the create-time refusal (a 413
122
+ // over the cap, a resume that does not match). Nobody has to await
123
+ // it: a create failure still surfaces on the task itself.
124
+ var settleCreated;
125
+ var createdSettled = false;
126
+ task.created = new Promise(function (resolve, reject) {
127
+ // failed is its own argument rather than inferred from err:
128
+ // code that throws a falsy value would otherwise settle this
129
+ // as a resolve(undefined) — a failure read as success.
130
+ settleCreated = function (failed, err, value) { createdSettled = true; if (failed) reject(err); else resolve(value); };
131
+ });
132
+ task.created.catch(function () {});
133
+
100
134
  function progress () {
101
135
  if (typeof opts.onProgress !== 'function') return;
102
136
  var sum = 0;
@@ -105,16 +139,24 @@ export function streamsClientSource() {
105
139
  }
106
140
  function abortError () { var err = new Error('Upload aborted'); err.code = 'aborted'; return err; }
107
141
 
142
+ // 'attempt' is 1-based in events: the first try is attempt 1, and a
143
+ // 'retry' names the attempt that just failed.
108
144
  function sendChunk (meta, n, attempt) {
109
145
  if (signal.aborted) return Promise.reject(abortError());
110
146
  var start = (n - 1) * meta.chunkSize;
111
147
  var blob = file.slice(start, Math.min(file.size, start + meta.chunkSize));
148
+ notify(opts, { type: 'chunk', n: n, state: 'sent', attempt: attempt + 1 });
112
149
  return putChunk(base + '/_uploads/' + meta.id + '/' + n, blob, signal, function (sent) { loaded[n] = sent; progress(); })
113
- .then(function () { loaded[n] = blob.size; progress(); })
150
+ .then(function () {
151
+ loaded[n] = blob.size;
152
+ progress();
153
+ notify(opts, { type: 'chunk', n: n, state: 'landed', attempt: attempt + 1 });
154
+ })
114
155
  .catch(function (err) {
115
156
  if (err.code === 'aborted' || signal.aborted) throw err;
116
157
  if (err.status === 404) { var gone = new Error('Upload expired before it completed'); gone.code = 'upload_expired'; gone.status = 404; throw gone; }
117
158
  if (!retryable(err.status) || attempt >= retries) throw err;
159
+ notify(opts, { type: 'chunk', n: n, state: 'retry', attempt: attempt + 1, status: err.status });
118
160
  return delay(backoff(attempt)).then(function () { return sendChunk(meta, n, attempt + 1); });
119
161
  });
120
162
  }
@@ -131,6 +173,24 @@ export function streamsClientSource() {
131
173
  return Promise.all(workers);
132
174
  }
133
175
 
176
+ // Seal. A 412 means the store is missing chunks a PUT reported as
177
+ // landed (a proxy that answered before the bytes were durable, an
178
+ // expiry racing the last write): send exactly those once more and
179
+ // seal again. The server names at most 50 (app-streams.js
180
+ // completeUpload), so a gap wider than that cannot be closed this
181
+ // way — a second 412 is a failure the caller resumes from.
182
+ function seal (meta, resent) {
183
+ return json('POST', base + '/_uploads/' + meta.id + '/_complete').catch(function (err) {
184
+ var missing = err.status === 412 && err.data && err.data.missing;
185
+ if (resent || !missing || !missing.length) throw err;
186
+ for (var i = 0; i < missing.length; i++) notify(opts, { type: 'chunk', n: missing[i], state: 'resend' });
187
+ return sendAll(meta, missing).then(function () {
188
+ if (signal.aborted) throw abortError();
189
+ return seal(meta, true);
190
+ });
191
+ });
192
+ }
193
+
134
194
  var plan = signal.aborted ? Promise.reject(abortError()) : opts.resume
135
195
  ? json('GET', base + '/_uploads/' + encodeURIComponent(opts.resume)).then(function (meta) {
136
196
  // Size alone is not identity: two same-length files would be
@@ -159,26 +219,45 @@ export function streamsClientSource() {
159
219
  var promise = plan
160
220
  .then(function (p) {
161
221
  task.id = p.meta.id;
222
+ var created = {
223
+ id: p.meta.id,
224
+ size: p.meta.size,
225
+ chunks: p.meta.chunks,
226
+ chunkSize: p.meta.chunkSize,
227
+ resumed: !!opts.resume,
228
+ // Chunks a resumed upload already holds; empty for a new one.
229
+ received: opts.resume ? (p.meta.received || []) : []
230
+ };
231
+ settleCreated(false, null, created);
232
+ notify(opts, { type: 'created', id: created.id, size: created.size, chunks: created.chunks, chunkSize: created.chunkSize, resumed: created.resumed, received: created.received });
162
233
  progress();
163
234
  return sendAll(p.meta, p.chunks).then(function () {
164
235
  if (signal.aborted) throw abortError();
165
- return json('POST', base + '/_uploads/' + p.meta.id + '/_complete');
236
+ return seal(p.meta, false);
166
237
  });
167
238
  })
168
- .then(function (handle) { progress(); return handle; })
239
+ .then(function (handle) {
240
+ progress();
241
+ notify(opts, { type: 'sealed', handle: handle });
242
+ return handle;
243
+ })
169
244
  .catch(function (err) {
170
245
  var aborted = err.code === 'aborted' || signal.aborted;
171
246
  // Promise.all rejects on the first failure but leaves the other
172
247
  // workers retrying with backoff; stop them before returning.
173
248
  controller.abort();
249
+ if (!createdSettled) settleCreated(true, err);
174
250
  if (task.id) {
175
251
  // Discard only on an explicit abort — the caller is done
176
252
  // with it. Any other failure keeps the staged chunks so
177
253
  // upload({ resume: err.uploadId }) can finish the job; the
178
- // TTL reclaims them if it never does.
254
+ // TTL reclaims them if it never does. An expired upload is
255
+ // the exception: the server already reclaimed it, so
256
+ // offering a resume would send the page into a 404 loop.
179
257
  if (aborted) transport.fetch(base + '/_uploads/' + task.id, { method: 'DELETE', credentials: 'same-origin' }).catch(function () {});
180
- else err.uploadId = task.id;
258
+ else if (err.code !== 'upload_expired') err.uploadId = task.id;
181
259
  }
260
+ notify(opts, { type: 'failed', error: err, aborted: aborted, resumable: !!err.uploadId });
182
261
  throw err;
183
262
  });
184
263
 
@@ -192,7 +271,41 @@ export function streamsClientSource() {
192
271
  return base + '/_downloads/' + encodeURIComponent(id) + (filename ? '/' + encodeURIComponent(filename) : '');
193
272
  }
194
273
 
195
- return { upload: upload, downloadUrl: downloadUrl };
274
+ // What the store holds for this user in this app (I5-13030). The helper
275
+ // owns the base URL, so these are the sanctioned way to ask — a page
276
+ // must not derive the prefix from downloadUrl() and hand-build the calls.
277
+ var streams = {
278
+ // Resume state for an upload there is no task for: which chunks
279
+ // landed, which are missing, whether it is sealed.
280
+ status: function (id) {
281
+ return json('GET', base + '/_uploads/' + encodeURIComponent(id));
282
+ },
283
+ // Every upload and download this user has staged for the app,
284
+ // oldest first. What a truthful "n of maxStreamsPerUser" reads.
285
+ list: function () {
286
+ return Promise.all([json('GET', base + '/_uploads'), json('GET', base + '/_downloads')])
287
+ .then(function (r) {
288
+ // A truthy non-array (an auth-bounce page body) would
289
+ // otherwise pass through as an empty-looking listing.
290
+ if (!Array.isArray(r[0]) || !Array.isArray(r[1])) {
291
+ throw new Error('streams.list(): the staging routes did not answer with listings');
292
+ }
293
+ return { uploads: r[0], downloads: r[1] };
294
+ });
295
+ },
296
+ // Free a staged stream now rather than at expiry. Takes a handle or
297
+ // a bare id; a handle already carries its kind, so only a bare id
298
+ // needs the second argument (which still defaults to 'upload').
299
+ discard: function (idOrHandle, type) {
300
+ var handle = idOrHandle && typeof idOrHandle === 'object' ? idOrHandle : null;
301
+ var id = handle ? handle.id : idOrHandle;
302
+ var kind = type || (handle && handle.__appStream) || 'upload';
303
+ var prefix = kind === 'download' ? '/_downloads/' : '/_uploads/';
304
+ return json('DELETE', base + prefix + encodeURIComponent(id)).then(function () { return true; });
305
+ }
306
+ };
307
+
308
+ return { upload: upload, downloadUrl: downloadUrl, streams: streams };
196
309
  })("", {
197
310
  fetch: function (url, opts) { return window.fetch(url, opts); },
198
311
  open: function (xhr, method, url) { xhr.open(method, url); }