@entrinsik/vite-plugin-informer 2.7.0 → 2.11.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.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Dev copy of the injected app streams client (I5-12979):
3
+ * `__INFORMER__.upload(file, opts)` and `__INFORMER__.downloadUrl(id, filename)`.
4
+ *
5
+ * VERBATIM copy of modules/app/routes/lib/html-utils.js#generateStreamsHelper
6
+ * in its origin-mode form (base "", plain window.fetch / XHR transport — the
7
+ * dev server IS the app's origin, so the same-origin /_uploads and /_downloads
8
+ * middleware serves it). The plugin cannot import the server module (different
9
+ * runtime, no dependency), so the copy is pinned byte-for-byte by
10
+ * test/streams-parity.test.js: change the protocol or retry policy there, and
11
+ * that test says exactly what to paste here. The dev page must upload the way
12
+ * a deployed one does.
13
+ *
14
+ * @returns {string} JavaScript expression evaluating to { upload, downloadUrl }
15
+ */
16
+ export function streamsClientSource() {
17
+ return `(function (base, transport) {
18
+ var DEFAULTS = { chunkSize: 4 * 1024 * 1024, concurrency: 3, retries: 5 };
19
+
20
+ function delay (ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
21
+ function backoff (attempt) { return Math.min(8000, 500 * Math.pow(2, attempt)) * (0.5 + Math.random()); }
22
+ function retryable (status) { return status === 0 || status === 408 || status === 429 || status >= 500; }
23
+ function httpError (status, data, fallback) {
24
+ var err = new Error((data && (data.message || data.error)) || fallback || ('HTTP ' + status));
25
+ err.status = status;
26
+ err.data = data && data.data;
27
+ return err;
28
+ }
29
+
30
+ function json (method, url, body) {
31
+ var opts = { method: method, credentials: 'same-origin', headers: {} };
32
+ if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
33
+ return transport.fetch(url, opts).then(function (res) {
34
+ return res.text().then(function (text) {
35
+ var data = null;
36
+ try { data = text ? JSON.parse(text) : null; } catch (e) { data = text; }
37
+ if (!res.ok) throw httpError(res.status, data);
38
+ return data;
39
+ });
40
+ });
41
+ }
42
+
43
+ // XHR rather than fetch for the chunk body: byte-level upload progress
44
+ // events and a synchronous abort.
45
+ function putChunk (url, blob, signal, onProgress) {
46
+ return new Promise(function (resolve, reject) {
47
+ var xhr = new XMLHttpRequest();
48
+ transport.open(xhr, 'PUT', url);
49
+ xhr.setRequestHeader('Content-Type', 'application/octet-stream');
50
+ xhr.withCredentials = true;
51
+ // Without this, ontimeout can never fire and a half-open socket
52
+ // (VPN drop, laptop sleep, an idle reap with no RST) leaves the
53
+ // promise pending forever. Allows ~8 KB/s before giving up, so a
54
+ // genuinely slow link retries rather than stalls.
55
+ xhr.timeout = Math.max(60000, Math.ceil(blob.size / 8));
56
+ if (xhr.upload && onProgress) {
57
+ xhr.upload.onprogress = function (e) { if (e.lengthComputable) onProgress(e.loaded); };
58
+ }
59
+ xhr.onload = function () {
60
+ if (xhr.status >= 200 && xhr.status < 300) return resolve();
61
+ var data = null;
62
+ try { data = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch (e) { data = null; }
63
+ reject(httpError(xhr.status, data, 'Chunk upload failed: HTTP ' + xhr.status));
64
+ };
65
+ xhr.onerror = function () { reject(httpError(0, null, 'Network error during chunk upload')); };
66
+ xhr.ontimeout = function () { reject(httpError(408, null, 'Chunk upload timed out')); };
67
+ xhr.onabort = function () { var err = new Error('Upload aborted'); err.code = 'aborted'; reject(err); };
68
+ if (signal) {
69
+ if (signal.aborted) { xhr.abort(); return; }
70
+ signal.addEventListener('abort', function () { xhr.abort(); });
71
+ }
72
+ xhr.send(blob);
73
+ });
74
+ }
75
+
76
+ // Name + length + mtime. Enough to stop a different file being spliced
77
+ // into a resumed upload; not a checksum, which would mean reading the
78
+ // whole file in the page before the first byte goes out.
79
+ function fingerprintOf (file, opts) {
80
+ return [opts.filename || file.name || '', file.size, file.lastModified || 0].join(':');
81
+ }
82
+
83
+ function upload (file, opts) {
84
+ opts = opts || {};
85
+ if (!file || typeof file.slice !== 'function' || typeof file.size !== 'number') {
86
+ return Promise.reject(new Error('upload(file): a File or Blob is required'));
87
+ }
88
+ var controller = new AbortController();
89
+ if (opts.signal) {
90
+ // An already-aborted signal never fires 'abort' — mirror its state now.
91
+ if (opts.signal.aborted) controller.abort();
92
+ else opts.signal.addEventListener('abort', function () { controller.abort(); });
93
+ }
94
+ var signal = controller.signal;
95
+ var retries = opts.retries != null ? opts.retries : DEFAULTS.retries;
96
+ var concurrency = Math.max(1, opts.concurrency || DEFAULTS.concurrency);
97
+ var task = { id: opts.resume || null, abort: function () { controller.abort(); } };
98
+ var loaded = {};
99
+
100
+ function progress () {
101
+ if (typeof opts.onProgress !== 'function') return;
102
+ var sum = 0;
103
+ for (var k in loaded) sum += loaded[k];
104
+ opts.onProgress({ loaded: sum, total: file.size, percent: file.size ? Math.min(100, Math.floor(sum * 100 / file.size)) : 100 });
105
+ }
106
+ function abortError () { var err = new Error('Upload aborted'); err.code = 'aborted'; return err; }
107
+
108
+ function sendChunk (meta, n, attempt) {
109
+ if (signal.aborted) return Promise.reject(abortError());
110
+ var start = (n - 1) * meta.chunkSize;
111
+ var blob = file.slice(start, Math.min(file.size, start + meta.chunkSize));
112
+ return putChunk(base + '/_uploads/' + meta.id + '/' + n, blob, signal, function (sent) { loaded[n] = sent; progress(); })
113
+ .then(function () { loaded[n] = blob.size; progress(); })
114
+ .catch(function (err) {
115
+ if (err.code === 'aborted' || signal.aborted) throw err;
116
+ if (err.status === 404) { var gone = new Error('Upload expired before it completed'); gone.code = 'upload_expired'; gone.status = 404; throw gone; }
117
+ if (!retryable(err.status) || attempt >= retries) throw err;
118
+ return delay(backoff(attempt)).then(function () { return sendChunk(meta, n, attempt + 1); });
119
+ });
120
+ }
121
+
122
+ function sendAll (meta, chunks) {
123
+ var next = 0;
124
+ function worker () {
125
+ if (next >= chunks.length || signal.aborted) return Promise.resolve();
126
+ var n = chunks[next++];
127
+ return sendChunk(meta, n, 0).then(worker);
128
+ }
129
+ var workers = [];
130
+ for (var i = 0; i < Math.min(concurrency, chunks.length); i++) workers.push(worker());
131
+ return Promise.all(workers);
132
+ }
133
+
134
+ var plan = signal.aborted ? Promise.reject(abortError()) : opts.resume
135
+ ? json('GET', base + '/_uploads/' + encodeURIComponent(opts.resume)).then(function (meta) {
136
+ // Size alone is not identity: two same-length files would be
137
+ // spliced into one upload that completes without complaint.
138
+ var fp = fingerprintOf(file, opts);
139
+ if (meta.size !== file.size || (meta.fingerprint && meta.fingerprint !== fp)) {
140
+ throw new Error('upload({ resume }): the file does not match the upload being resumed');
141
+ }
142
+ (meta.received || []).forEach(function (n) {
143
+ loaded[n] = n < meta.chunks ? meta.chunkSize : meta.size - meta.chunkSize * (meta.chunks - 1);
144
+ });
145
+ return { meta: meta, chunks: meta.complete ? [] : (meta.missing || []) };
146
+ })
147
+ : json('POST', base + '/_uploads', {
148
+ filename: opts.filename || file.name || 'upload',
149
+ contentType: opts.contentType || file.type || undefined,
150
+ size: file.size,
151
+ fingerprint: fingerprintOf(file, opts),
152
+ chunkSize: opts.chunkSize || DEFAULTS.chunkSize
153
+ }).then(function (meta) {
154
+ var chunks = [];
155
+ for (var n = 1; n <= meta.chunks; n++) chunks.push(n);
156
+ return { meta: meta, chunks: chunks };
157
+ });
158
+
159
+ var promise = plan
160
+ .then(function (p) {
161
+ task.id = p.meta.id;
162
+ progress();
163
+ return sendAll(p.meta, p.chunks).then(function () {
164
+ if (signal.aborted) throw abortError();
165
+ return json('POST', base + '/_uploads/' + p.meta.id + '/_complete');
166
+ });
167
+ })
168
+ .then(function (handle) { progress(); return handle; })
169
+ .catch(function (err) {
170
+ var aborted = err.code === 'aborted' || signal.aborted;
171
+ // Promise.all rejects on the first failure but leaves the other
172
+ // workers retrying with backoff; stop them before returning.
173
+ controller.abort();
174
+ if (task.id) {
175
+ // Discard only on an explicit abort — the caller is done
176
+ // with it. Any other failure keeps the staged chunks so
177
+ // upload({ resume: err.uploadId }) can finish the job; the
178
+ // TTL reclaims them if it never does.
179
+ if (aborted) transport.fetch(base + '/_uploads/' + task.id, { method: 'DELETE', credentials: 'same-origin' }).catch(function () {});
180
+ else err.uploadId = task.id;
181
+ }
182
+ throw err;
183
+ });
184
+
185
+ task.then = function (onFulfilled, onRejected) { return promise.then(onFulfilled, onRejected); };
186
+ task['catch'] = function (onRejected) { return promise['catch'](onRejected); };
187
+ task['finally'] = function (onFinally) { return promise['finally'](onFinally); };
188
+ return task;
189
+ }
190
+
191
+ function downloadUrl (id, filename) {
192
+ return base + '/_downloads/' + encodeURIComponent(id) + (filename ? '/' + encodeURIComponent(filename) : '');
193
+ }
194
+
195
+ return { upload: upload, downloadUrl: downloadUrl };
196
+ })("", {
197
+ fetch: function (url, opts) { return window.fetch(url, opts); },
198
+ open: function (xhr, method, url) { xhr.open(method, url); }
199
+ })`;
200
+ }