@entrinsik/vite-plugin-informer 2.11.0 → 2.12.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.
- package/README.md +5 -3
- package/bin/init.js +13 -2
- package/index.d.ts +14 -0
- package/package.json +1 -1
- package/src/agent-dev.js +8 -11
- package/src/dev-bag.js +75 -19
- package/src/dev-channel-handlers.js +23 -13
- package/src/dev-channel-shim.js +37 -11
- package/src/dev-channels.js +33 -15
- package/src/dev-dependencies.js +140 -4
- package/src/dev-platform.js +80 -7
- package/src/dev-streams.js +176 -27
- package/src/index.js +61 -8
- package/src/server-routes.js +4 -3
- package/src/streams-client.js +121 -8
package/src/index.js
CHANGED
|
@@ -39,6 +39,36 @@ function buildMock(options) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The app id dev surfaces address on the server (the opt-in dev `embed()`
|
|
44
|
+
* posts to its `_embed` route). `informer-init` generates it locally into
|
|
45
|
+
* package.json `informer.id` and the first deploy creates the app under it.
|
|
46
|
+
* Absent, the surface says so at call time rather than the dev server
|
|
47
|
+
* refusing to start.
|
|
48
|
+
*/
|
|
49
|
+
function readAppId(projectRoot) {
|
|
50
|
+
const pkgPath = resolve(projectRoot, 'package.json');
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = readFileSync(pkgPath, 'utf8');
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Unreadable is not the same as "no id set", and the two want
|
|
56
|
+
// different fixes. Saying which one it is beats telling someone to
|
|
57
|
+
// set a field in a file they are already looking at — or that the
|
|
58
|
+
// dev server never found.
|
|
59
|
+
if (err.code !== 'ENOENT') console.warn(`[informer] Could not read ${pkgPath}: ${err.message}`);
|
|
60
|
+
else console.warn(`[informer] No package.json at ${pkgPath}; dev surfaces that address the deployed app are unavailable.`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(raw);
|
|
65
|
+
return (pkg.informer && pkg.informer.id) || null;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.warn(`[informer] Could not parse ${pkgPath}: ${err.message}`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
/**
|
|
43
73
|
* Occupied-spot check for the dev /api mount: does informer.yaml's
|
|
44
74
|
* access.apis declare this method+path? Mirrors the server's matchWhitelist
|
|
@@ -103,8 +133,10 @@ async function writeAppDepTypes (projectRoot, dts) {
|
|
|
103
133
|
* - Sets base to './' so built assets use relative paths
|
|
104
134
|
*
|
|
105
135
|
* @param {Object} [options]
|
|
106
|
-
* @param {{ report?: object, theme?: 'light'|'dark', roles?: string[] }} [options.mock]
|
|
107
|
-
* window.__INFORMER__ mock injected in dev.
|
|
136
|
+
* @param {{ report?: object, theme?: 'light'|'dark', roles?: string[], user?: object, platform?: object }} [options.mock]
|
|
137
|
+
* window.__INFORMER__ mock injected in dev. `platform` merges into the dev
|
|
138
|
+
* descriptor, so `{ capabilities: { embeddings: true } }` opts one capability
|
|
139
|
+
* in and leaves the rest. See InformerPluginOptions in index.d.ts.
|
|
108
140
|
* @param {Object} [options.devBindings] - dev bindings for `target: app` and
|
|
109
141
|
* `target: pack` deps. App slots can't be defaultBound in the manifest; pack
|
|
110
142
|
* slots resolve their marketplace pin via installs dev doesn't have, so the
|
|
@@ -181,6 +213,15 @@ export default function informer(options = {}) {
|
|
|
181
213
|
const projectRoot = process.cwd();
|
|
182
214
|
const migrationsDir = resolve(projectRoot, 'migrations');
|
|
183
215
|
|
|
216
|
+
// One merged descriptor for the whole dev server. Built once
|
|
217
|
+
// rather than per consumer so "the channel hub, the handler bags
|
|
218
|
+
// and the browser all see the same mock" is structural — which is
|
|
219
|
+
// the property the embeddings opt-in depends on, since the flag
|
|
220
|
+
// the app reads and the flag that binds embed() must be the same
|
|
221
|
+
// flag. (transformIndexHtml is a separate hook and builds its own.)
|
|
222
|
+
const mock = buildMock(options);
|
|
223
|
+
const appId = readAppId(projectRoot);
|
|
224
|
+
|
|
184
225
|
// One API client for the whole dev-server setup — createClient just
|
|
185
226
|
// builds an auth header (no I/O), so it's hoisted out of the two
|
|
186
227
|
// branches that each used to rebuild an identical one.
|
|
@@ -279,7 +320,7 @@ export default function informer(options = {}) {
|
|
|
279
320
|
// Every page receives every frame; the subscribe loop mounted below
|
|
280
321
|
// (join / joined / leave / send against channels/ files) is what
|
|
281
322
|
// tells a page which channels it may dispatch.
|
|
282
|
-
const channels = createDevChannels({ appId:
|
|
323
|
+
const channels = createDevChannels({ appId: mock.report.id });
|
|
283
324
|
channels.emitter.on(BROADCAST_EVENT, (frame) => {
|
|
284
325
|
server.ws.send({ type: 'custom', event: DEV_CHANNEL_EVENT, data: frame });
|
|
285
326
|
});
|
|
@@ -291,8 +332,10 @@ export default function informer(options = {}) {
|
|
|
291
332
|
devBindings: options.devBindings || {},
|
|
292
333
|
appToken,
|
|
293
334
|
channels,
|
|
294
|
-
user:
|
|
295
|
-
roles: (options.mock && options.mock.roles) || []
|
|
335
|
+
user: mock.user,
|
|
336
|
+
roles: (options.mock && options.mock.roles) || [],
|
|
337
|
+
platform: mock.platform,
|
|
338
|
+
appId
|
|
296
339
|
}));
|
|
297
340
|
|
|
298
341
|
// Generate .d.ts types for bound `target: app` / `target: pack`
|
|
@@ -368,6 +411,11 @@ export default function informer(options = {}) {
|
|
|
368
411
|
server.middlewares.use('/_uploads', createUploadsMiddleware(streamStore));
|
|
369
412
|
server.middlewares.use('/_downloads', createDownloadsMiddleware(streamStore));
|
|
370
413
|
|
|
414
|
+
// The merged platform descriptor (dev defaults + mock.platform) is
|
|
415
|
+
// what the handler bags see too, not only the browser: an app that
|
|
416
|
+
// opts into `embeddings` there must find embed() bound, not throwing.
|
|
417
|
+
const platform = mock.platform;
|
|
418
|
+
|
|
371
419
|
// Mount server-side route handlers if a server/ directory exists
|
|
372
420
|
const serverDir = resolve(projectRoot, 'server');
|
|
373
421
|
|
|
@@ -378,7 +426,7 @@ export default function informer(options = {}) {
|
|
|
378
426
|
devWorkspaceId,
|
|
379
427
|
projectRoot,
|
|
380
428
|
roles: (options.mock && options.mock.roles) || [],
|
|
381
|
-
user:
|
|
429
|
+
user: mock.user,
|
|
382
430
|
// Dev-only bindings for `target: app` / `target: pack`
|
|
383
431
|
// slots (app: overrides the manifest defaultBinding; pack:
|
|
384
432
|
// names the locally-installed pack app). Shape:
|
|
@@ -386,7 +434,9 @@ export default function informer(options = {}) {
|
|
|
386
434
|
devBindings: options.devBindings || {},
|
|
387
435
|
appToken,
|
|
388
436
|
channels,
|
|
389
|
-
streamStore
|
|
437
|
+
streamStore,
|
|
438
|
+
platform,
|
|
439
|
+
appId
|
|
390
440
|
});
|
|
391
441
|
server.middlewares.use('/api/_server', serverRoutes);
|
|
392
442
|
// Occupied-spot precedence (App API v2, matches production
|
|
@@ -418,7 +468,9 @@ export default function informer(options = {}) {
|
|
|
418
468
|
projectRoot,
|
|
419
469
|
devBindings: options.devBindings || {},
|
|
420
470
|
appToken,
|
|
421
|
-
channels
|
|
471
|
+
channels,
|
|
472
|
+
platform,
|
|
473
|
+
appId
|
|
422
474
|
});
|
|
423
475
|
server.middlewares.use('/api/_agent', agentDev);
|
|
424
476
|
}
|
|
@@ -446,6 +498,7 @@ export default function informer(options = {}) {
|
|
|
446
498
|
var __streams = ${streamsClientSource()};
|
|
447
499
|
window.__INFORMER__.upload = __streams.upload;
|
|
448
500
|
window.__INFORMER__.downloadUrl = __streams.downloadUrl;
|
|
501
|
+
window.__INFORMER__.streams = __streams.streams;
|
|
449
502
|
})();
|
|
450
503
|
</script>
|
|
451
504
|
${renderDevChannelScript({ hub: Boolean(serverOrigin) })}`;
|
package/src/server-routes.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { parse as parseUrl } from 'node:url';
|
|
4
4
|
import { createDevBagBuilder, buildDevUser } from './dev-bag.js';
|
|
5
5
|
import { createDevChannels } from './dev-channels.js';
|
|
6
|
+
import { devPlatform } from './dev-platform.js';
|
|
6
7
|
import { createStreamStore, createStreamServices, isStreamRef, serveDownload as serveDevDownload } from './dev-streams.js';
|
|
7
8
|
|
|
8
9
|
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
@@ -224,13 +225,13 @@ function isDescriptor(value) {
|
|
|
224
225
|
* and /_downloads middleware (createStreamStore()); a private one when omitted
|
|
225
226
|
* @returns {Function} Connect middleware
|
|
226
227
|
*/
|
|
227
|
-
export function createMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, roles, user, devBindings, appToken, channels = createDevChannels(), streamStore }) {
|
|
228
|
+
export function createMiddleware(viteServer, { serverOrigin, authHeader, devWorkspaceId, projectRoot, roles, user, devBindings, appToken, channels = createDevChannels(), streamStore, platform = devPlatform(), appId = null }) {
|
|
228
229
|
// `@user/${request.user.username}` on the server names the channel the
|
|
229
230
|
// page subscribes to.
|
|
230
231
|
const devUser = buildDevUser(user);
|
|
231
232
|
const serverDir = join(projectRoot, 'server');
|
|
232
233
|
const streams = streamStore || createStreamStore();
|
|
233
|
-
const bagBuilder = createDevBagBuilder({ serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken, channels, logPrefix: '[app]' });
|
|
234
|
+
const bagBuilder = createDevBagBuilder({ serverOrigin, authHeader, devWorkspaceId, projectRoot, devBindings, appToken, channels, logPrefix: '[app]', platform, appId });
|
|
234
235
|
const { query } = bagBuilder;
|
|
235
236
|
|
|
236
237
|
return async function serverRoutesMiddleware(req, res, next) {
|
|
@@ -333,7 +334,7 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
333
334
|
res.end(JSON.stringify(earlyBody));
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
const { bag } = await bagBuilder.build();
|
|
337
|
+
const { bag } = await bagBuilder.build({ forwarding: streamServices.forwarding });
|
|
337
338
|
|
|
338
339
|
// Call handler — bag mirrors the prod sandbox (see app-sandbox.js).
|
|
339
340
|
const result = await handler({ ...bag, request, query: streamQuery, respond, uploads: streamServices.uploads, downloads: streamServices.downloads });
|
package/src/streams-client.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dev copy of the injected app streams client (I5-12979):
|
|
3
|
-
* `__INFORMER__.upload(file, opts)
|
|
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 () {
|
|
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
|
|
236
|
+
return seal(p.meta, false);
|
|
166
237
|
});
|
|
167
238
|
})
|
|
168
|
-
.then(function (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
|
-
|
|
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); }
|