lemon-model 1.2.4 → 1.5.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/dist/esm/upload/engine/engine.js +204 -0
- package/dist/esm/upload/engine/engine.js.map +1 -0
- package/dist/esm/upload/engine/executors.js +57 -0
- package/dist/esm/upload/engine/executors.js.map +1 -0
- package/dist/esm/upload/engine/index.js +13 -0
- package/dist/esm/upload/engine/index.js.map +1 -0
- package/dist/esm/upload/engine/progress.js +52 -0
- package/dist/esm/upload/engine/progress.js.map +1 -0
- package/dist/esm/upload/index.js +11 -0
- package/dist/esm/upload/index.js.map +1 -0
- package/dist/esm/upload/testing.js +80 -0
- package/dist/esm/upload/testing.js.map +1 -0
- package/dist/esm/upload/types.js +84 -0
- package/dist/esm/upload/types.js.map +1 -0
- package/dist/upload/engine/engine.d.ts +73 -0
- package/dist/upload/engine/engine.js +210 -0
- package/dist/upload/engine/engine.js.map +1 -0
- package/dist/upload/engine/executors.d.ts +44 -0
- package/dist/upload/engine/executors.js +62 -0
- package/dist/upload/engine/executors.js.map +1 -0
- package/dist/upload/engine/index.d.ts +12 -0
- package/dist/upload/engine/index.js +29 -0
- package/dist/upload/engine/index.js.map +1 -0
- package/dist/upload/engine/progress.d.ts +48 -0
- package/dist/upload/engine/progress.js +57 -0
- package/dist/upload/engine/progress.js.map +1 -0
- package/dist/upload/index.d.ts +10 -0
- package/dist/upload/index.js +27 -0
- package/dist/upload/index.js.map +1 -0
- package/dist/upload/testing.d.ts +23 -0
- package/dist/upload/testing.js +83 -0
- package/dist/upload/testing.js.map +1 -0
- package/dist/upload/types.d.ts +254 -0
- package/dist/upload/types.js +89 -0
- package/dist/upload/types.js.map +1 -0
- package/package.json +26 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.asStorageFailure = exports.asApiFailure = exports.UploadEngine = exports.UPLOAD_DEFAULT_CONCURRENCY = void 0;
|
|
13
|
+
const types_1 = require("../types");
|
|
14
|
+
const progress_1 = require("./progress");
|
|
15
|
+
/**
|
|
16
|
+
* transfers in flight at once.
|
|
17
|
+
* - a browser queues beyond ~6 per origin, and a queued presigned url burns its `expiresAt` while it waits.
|
|
18
|
+
* - override per engine when a shell measures something better.
|
|
19
|
+
*/
|
|
20
|
+
exports.UPLOAD_DEFAULT_CONCURRENCY = 3;
|
|
21
|
+
/** run `task` over 0..count-1 with at most `limit` in flight; results keep input order */
|
|
22
|
+
const runPooled = (count, limit, task) => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
+
const results = new Array(count);
|
|
24
|
+
let next = 0;
|
|
25
|
+
const worker = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const index = next++;
|
|
28
|
+
if (index >= count)
|
|
29
|
+
return;
|
|
30
|
+
results[index] = yield task(index);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const size = Math.max(1, Math.min(limit, count));
|
|
34
|
+
yield Promise.all(new Array(size).fill(0).map(() => worker()));
|
|
35
|
+
return results;
|
|
36
|
+
});
|
|
37
|
+
const asContent = (source) => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
+
return ({
|
|
39
|
+
contentType: source.contentType,
|
|
40
|
+
contentSize: source.contentSize,
|
|
41
|
+
width: source.width,
|
|
42
|
+
height: source.height,
|
|
43
|
+
hash: source.hash ? yield source.hash() : undefined,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
const asIntent = (source) => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
return (Object.assign(Object.assign({}, (yield asContent(source))), { name: source.name, thumbnail: source.thumbnail ? yield asContent(source.thumbnail) : undefined }));
|
|
48
|
+
});
|
|
49
|
+
/** bytes this source puts on the wire in total; the thumbnail rides along so the bar counts it */
|
|
50
|
+
const sizeOf = (source) => { var _a, _b; return source.contentSize + ((_b = (_a = source.thumbnail) === null || _a === void 0 ? void 0 : _a.contentSize) !== null && _b !== void 0 ? _b : 0); };
|
|
51
|
+
class UploadEngine {
|
|
52
|
+
constructor(service, executors, options) {
|
|
53
|
+
this.service = service;
|
|
54
|
+
this.executors = executors;
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.kinds = executors.map(executor => executor.kind);
|
|
57
|
+
}
|
|
58
|
+
/** returns one view per source, in order: `stored`, or `failed` with the reason. `onProgress` gets a snapshot per change */
|
|
59
|
+
upload(sources, onProgress) {
|
|
60
|
+
var _a, _b;
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const tracker = new progress_1.UploadProgressTracker(sources.map(sizeOf), onProgress);
|
|
63
|
+
tracker.publish();
|
|
64
|
+
const list = yield Promise.all(sources.map(asIntent));
|
|
65
|
+
const { list: tickets } = yield this.service.start({ list, transfers: this.kinds });
|
|
66
|
+
const limit = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.concurrency) !== null && _b !== void 0 ? _b : exports.UPLOAD_DEFAULT_CONCURRENCY;
|
|
67
|
+
const receipts = yield runPooled(tickets.length, limit, i => this.transferTracked(tickets[i], sources[i], tracker, i));
|
|
68
|
+
// a server that broke rule 1 (shorter list) must not leave the bar hanging
|
|
69
|
+
sources.forEach((_, i) => tracker.settle(i));
|
|
70
|
+
const pending = receipts.filter((item) => !!item);
|
|
71
|
+
const settled = pending.length ? (yield this.service.complete({ list: pending })).list : [];
|
|
72
|
+
const byId = new Map(settled.filter(upload => !!upload.id).map((upload) => [upload.id, upload]));
|
|
73
|
+
return tickets.map(ticket => (ticket.upload.id && byId.get(ticket.upload.id)) || ticket.upload);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
transferTracked(ticket, source, tracker, index) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
try {
|
|
79
|
+
return yield this.transfer(ticket, source, tracker.sink(index));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// an executor that throws (a CORS preflight failure surfaces as one) must not take the batch down
|
|
83
|
+
const id = ticket.upload.id;
|
|
84
|
+
if (!id)
|
|
85
|
+
return undefined;
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
failure: { source: types_1.UPLOAD_FAILURE_SOURCE.client, code: types_1.UPLOAD_FAILURE_CODE.unknown, message },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
tracker.settle(index);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
transfer(ticket, source, onProgress) {
|
|
98
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
99
|
+
const id = ticket.upload.id;
|
|
100
|
+
// rejected at start: nothing was created, nothing to complete
|
|
101
|
+
if (!id)
|
|
102
|
+
return undefined;
|
|
103
|
+
const receipt = yield this.transferOriginal(id, ticket.transfer, source, onProgress);
|
|
104
|
+
// a failed original has nothing to preview; otherwise the thumbnail rides after it (never with it)
|
|
105
|
+
if (!receipt.failure)
|
|
106
|
+
yield this.transferThumbnail(id, ticket.thumbnailTransfer, source, onProgress);
|
|
107
|
+
return receipt;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
transferOriginal(id, transfer, source, onProgress) {
|
|
111
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
112
|
+
// no instruction: already stored (dedup, or a stored upload taking only a thumbnail) — complete still confirms it
|
|
113
|
+
if (!transfer)
|
|
114
|
+
return { id };
|
|
115
|
+
const executor = this.executors.find(candidate => candidate.kind === transfer.kind);
|
|
116
|
+
if (!executor)
|
|
117
|
+
return { id, failure: { source: types_1.UPLOAD_FAILURE_SOURCE.client, code: types_1.UPLOAD_FAILURE_CODE.notAcceptable } };
|
|
118
|
+
// NOTE: `transfer` must not be logged or stored — it may carry a credential url
|
|
119
|
+
return executor.run(id, transfer, source, onProgress);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* the preview is subordinate: whatever happens here never reaches `complete`.
|
|
124
|
+
* - `UploadCompleteItem` is one per upload, so reporting a thumbnail failure would fail the original.
|
|
125
|
+
* - the server settles the thumbnail by looking at storage at `complete` time, so silence is enough.
|
|
126
|
+
*/
|
|
127
|
+
transferThumbnail(id, transfer, source, onProgress) {
|
|
128
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
const thumbnail = source.thumbnail;
|
|
130
|
+
if (!transfer || !thumbnail)
|
|
131
|
+
return;
|
|
132
|
+
const executor = this.executors.find(candidate => candidate.kind === transfer.kind);
|
|
133
|
+
if (!executor)
|
|
134
|
+
return;
|
|
135
|
+
// the original's bytes are already counted, so the thumbnail reports on top of them
|
|
136
|
+
const base = source.contentSize;
|
|
137
|
+
try {
|
|
138
|
+
yield executor.run(id, transfer, thumbnail, sent => onProgress(base + sent));
|
|
139
|
+
}
|
|
140
|
+
catch (_a) {
|
|
141
|
+
// swallowed on purpose: see the note above
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.UploadEngine = UploadEngine;
|
|
147
|
+
/** lemon-core error convention: "<status> <LABEL> - <detail>" (status 0 = no response, XHR convention) */
|
|
148
|
+
const API_ERROR = /^(\d{1,3}) ([A-Z][A-Z ]*?) - ([\s\S]*)$/;
|
|
149
|
+
const codeOfApiStatus = (status) => {
|
|
150
|
+
switch (status) {
|
|
151
|
+
case 0:
|
|
152
|
+
return types_1.UPLOAD_FAILURE_CODE.network;
|
|
153
|
+
case 400:
|
|
154
|
+
return types_1.UPLOAD_FAILURE_CODE.invalid;
|
|
155
|
+
case 404:
|
|
156
|
+
return types_1.UPLOAD_FAILURE_CODE.notFound;
|
|
157
|
+
case 406:
|
|
158
|
+
return types_1.UPLOAD_FAILURE_CODE.notAcceptable;
|
|
159
|
+
case 409:
|
|
160
|
+
return types_1.UPLOAD_FAILURE_CODE.conflict;
|
|
161
|
+
case 410:
|
|
162
|
+
return types_1.UPLOAD_FAILURE_CODE.expired;
|
|
163
|
+
case 413:
|
|
164
|
+
return types_1.UPLOAD_FAILURE_CODE.tooLarge;
|
|
165
|
+
case 415:
|
|
166
|
+
return types_1.UPLOAD_FAILURE_CODE.unsupported;
|
|
167
|
+
default:
|
|
168
|
+
return types_1.UPLOAD_FAILURE_CODE.unknown;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/** normalize an error thrown by the API adapter */
|
|
172
|
+
const asApiFailure = (error) => {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
const match = API_ERROR.exec(message);
|
|
175
|
+
if (!match)
|
|
176
|
+
return { source: types_1.UPLOAD_FAILURE_SOURCE.api, code: types_1.UPLOAD_FAILURE_CODE.unknown, message };
|
|
177
|
+
const status = Number(match[1]);
|
|
178
|
+
return {
|
|
179
|
+
source: types_1.UPLOAD_FAILURE_SOURCE.api,
|
|
180
|
+
code: codeOfApiStatus(status),
|
|
181
|
+
status,
|
|
182
|
+
reason: match[2],
|
|
183
|
+
message: match[3],
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
exports.asApiFailure = asApiFailure;
|
|
187
|
+
/** S3 error `<Code>` -> normalized code. unknown codes fall back by status */
|
|
188
|
+
const STORAGE_CODES = {
|
|
189
|
+
SignatureDoesNotMatch: types_1.UPLOAD_FAILURE_CODE.signature,
|
|
190
|
+
AccessDenied: types_1.UPLOAD_FAILURE_CODE.expired,
|
|
191
|
+
ExpiredToken: types_1.UPLOAD_FAILURE_CODE.expired,
|
|
192
|
+
BadDigest: types_1.UPLOAD_FAILURE_CODE.checksum,
|
|
193
|
+
XAmzContentSHA256Mismatch: types_1.UPLOAD_FAILURE_CODE.checksum,
|
|
194
|
+
EntityTooLarge: types_1.UPLOAD_FAILURE_CODE.tooLarge,
|
|
195
|
+
RequestTimeout: types_1.UPLOAD_FAILURE_CODE.network,
|
|
196
|
+
};
|
|
197
|
+
/** normalize a non-2xx answer from the storage endpoint (status 0 = no response) */
|
|
198
|
+
const asStorageFailure = (status, code) => {
|
|
199
|
+
const known = code ? STORAGE_CODES[code] : undefined;
|
|
200
|
+
const byStatus = status === 0
|
|
201
|
+
? types_1.UPLOAD_FAILURE_CODE.network
|
|
202
|
+
: status === 403
|
|
203
|
+
? types_1.UPLOAD_FAILURE_CODE.signature
|
|
204
|
+
: status === 413
|
|
205
|
+
? types_1.UPLOAD_FAILURE_CODE.tooLarge
|
|
206
|
+
: types_1.UPLOAD_FAILURE_CODE.unknown;
|
|
207
|
+
return { source: types_1.UPLOAD_FAILURE_SOURCE.storage, code: known !== null && known !== void 0 ? known : byStatus, status, reason: code };
|
|
208
|
+
};
|
|
209
|
+
exports.asStorageFailure = asStorageFailure;
|
|
210
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/upload/engine/engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload/engine/executors.ts`
|
|
3
|
+
* - one executor per `UploadTransfer` kind. **this is the only file that grows when the server
|
|
4
|
+
* changes how bytes travel** — `UploadEngine` never does.
|
|
5
|
+
* - each executor takes its HTTP primitive from the shell: a fetch-based adapter reports no
|
|
6
|
+
* progress, an `XMLHttpRequest`-based one forwards `upload.onprogress`.
|
|
7
|
+
*
|
|
8
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
9
|
+
*/
|
|
10
|
+
import type { UploadCompleteItem, UploadInlineTransfer, UploadPresignedPutTransfer, UploadSendRequest, Upload } from '../types';
|
|
11
|
+
import type { UploadContentSource, UploadTransferExecutor } from './engine';
|
|
12
|
+
import type { UploadProgressSink, UploadWireProgress } from './progress';
|
|
13
|
+
/**
|
|
14
|
+
* the `send` call as the shell provides it.
|
|
15
|
+
* - a fetch-based API adapter is `(id, body) => service.send(id, body)` and reports nothing;
|
|
16
|
+
* - an `XMLHttpRequest`-based one forwards `upload.onprogress` through `onProgress`.
|
|
17
|
+
*/
|
|
18
|
+
export declare type InlineSendCall = (id: string, body: UploadSendRequest, onProgress?: UploadWireProgress) => Promise<Upload>;
|
|
19
|
+
/** inline transfer — the only place that knows bytes go to the API as base64 */
|
|
20
|
+
export declare class InlineExecutor implements UploadTransferExecutor<UploadInlineTransfer> {
|
|
21
|
+
private readonly send;
|
|
22
|
+
/** shell-specific base64 encoder (btoa / Buffer / RN polyfill) */
|
|
23
|
+
private readonly toBase64;
|
|
24
|
+
readonly kind: "inline";
|
|
25
|
+
constructor(send: InlineSendCall,
|
|
26
|
+
/** shell-specific base64 encoder (btoa / Buffer / RN polyfill) */
|
|
27
|
+
toBase64: (bytes: Uint8Array) => string);
|
|
28
|
+
run(id: string, transfer: UploadInlineTransfer, source: UploadContentSource, onProgress: UploadProgressSink): Promise<UploadCompleteItem>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* raw HTTP PUT provided by the shell; `code` = parsed S3 `<Code>`.
|
|
32
|
+
* - fetch-based: never calls `onProgress`. `XMLHttpRequest`-based: forwards `upload.onprogress`.
|
|
33
|
+
*/
|
|
34
|
+
export declare type RawPut = (url: string, headers: Record<string, string>, body: Uint8Array, onProgress?: UploadWireProgress) => Promise<{
|
|
35
|
+
status: number;
|
|
36
|
+
code?: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** presigned PUT transfer — raw bytes straight to storage */
|
|
39
|
+
export declare class PresignedPutExecutor implements UploadTransferExecutor<UploadPresignedPutTransfer> {
|
|
40
|
+
private readonly put;
|
|
41
|
+
readonly kind: "presigned-put";
|
|
42
|
+
constructor(put: RawPut);
|
|
43
|
+
run(id: string, transfer: UploadPresignedPutTransfer, source: UploadContentSource, onProgress: UploadProgressSink): Promise<UploadCompleteItem>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PresignedPutExecutor = exports.InlineExecutor = void 0;
|
|
13
|
+
const types_1 = require("../types");
|
|
14
|
+
const engine_1 = require("./engine");
|
|
15
|
+
const progress_1 = require("./progress");
|
|
16
|
+
/** inline transfer — the only place that knows bytes go to the API as base64 */
|
|
17
|
+
class InlineExecutor {
|
|
18
|
+
constructor(send,
|
|
19
|
+
/** shell-specific base64 encoder (btoa / Buffer / RN polyfill) */
|
|
20
|
+
toBase64) {
|
|
21
|
+
this.send = send;
|
|
22
|
+
this.toBase64 = toBase64;
|
|
23
|
+
this.kind = types_1.UPLOAD_TRANSFER_KIND.inline;
|
|
24
|
+
}
|
|
25
|
+
run(id, transfer, source, onProgress) {
|
|
26
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
27
|
+
if (source.contentSize > transfer.maxBytes) {
|
|
28
|
+
return { id, failure: { source: types_1.UPLOAD_FAILURE_SOURCE.client, code: types_1.UPLOAD_FAILURE_CODE.tooLarge } };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const body = { content: this.toBase64(yield source.bytes()) };
|
|
32
|
+
// wire bytes are base64 JSON (~4/3 of the send): report in send bytes
|
|
33
|
+
yield this.send(id, body, (loaded, total) => onProgress((0, progress_1.asContentBytes)(loaded, total, source.contentSize)));
|
|
34
|
+
return { id };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { id, failure: (0, engine_1.asApiFailure)(error) };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.InlineExecutor = InlineExecutor;
|
|
43
|
+
/** presigned PUT transfer — raw bytes straight to storage */
|
|
44
|
+
class PresignedPutExecutor {
|
|
45
|
+
constructor(put) {
|
|
46
|
+
this.put = put;
|
|
47
|
+
this.kind = types_1.UPLOAD_TRANSFER_KIND.presignedPut;
|
|
48
|
+
}
|
|
49
|
+
run(id, transfer, source, onProgress) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
const res = yield this.put(transfer.url, transfer.headers, yield source.bytes(), (loaded, total) => onProgress((0, progress_1.asContentBytes)(loaded, total, source.contentSize)));
|
|
52
|
+
if (res.status >= 200 && res.status < 300)
|
|
53
|
+
return { id };
|
|
54
|
+
// 412 = object already exists under this key; the server decides at complete
|
|
55
|
+
if (res.status === 412)
|
|
56
|
+
return { id };
|
|
57
|
+
return { id, failure: (0, engine_1.asStorageFailure)(res.status, res.code) };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.PresignedPutExecutor = PresignedPutExecutor;
|
|
62
|
+
//# sourceMappingURL=executors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executors.js","sourceRoot":"","sources":["../../../src/upload/engine/executors.ts"],"names":[],"mappings":";;;;;;;;;;;;AAgBA,oCAA4F;AAE5F,qCAA0D;AAE1D,yCAA4C;AAS5C,gFAAgF;AAChF,MAAa,cAAc;IAGvB,YACqB,IAAoB;IACrC,kEAAkE;IACjD,QAAuC;QAFvC,SAAI,GAAJ,IAAI,CAAgB;QAEpB,aAAQ,GAAR,QAAQ,CAA+B;QAL5C,SAAI,GAAG,4BAAoB,CAAC,MAAM,CAAC;IAMhD,CAAC;IAES,GAAG,CACZ,EAAU,EACV,QAA8B,EAC9B,MAA2B,EAC3B,UAA8B;;YAE9B,IAAI,MAAM,CAAC,WAAW,GAAG,QAAQ,CAAC,QAAQ,EAAE;gBACxC,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,6BAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,2BAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC;aACxG;YACD,IAAI;gBACA,MAAM,IAAI,GAAsB,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC;gBACjF,sEAAsE;gBACtE,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,IAAA,yBAAc,EAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;gBAC5G,OAAO,EAAE,EAAE,EAAE,CAAC;aACjB;YAAC,OAAO,KAAK,EAAE;gBACZ,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAA,qBAAY,EAAC,KAAK,CAAC,EAAE,CAAC;aAC/C;QACL,CAAC;KAAA;CACJ;AA3BD,wCA2BC;AAaD,6DAA6D;AAC7D,MAAa,oBAAoB;IAG7B,YAAoC,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;QAF/B,SAAI,GAAG,4BAAoB,CAAC,YAAY,CAAC;IAEP,CAAC;IAEtC,GAAG,CACZ,EAAU,EACV,QAAoC,EACpC,MAA2B,EAC3B,UAA8B;;YAE9B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAC/F,UAAU,CAAC,IAAA,yBAAc,EAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAChE,CAAC;YACF,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG;gBAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACzD,6EAA6E;YAC7E,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;gBAAE,OAAO,EAAE,EAAE,EAAE,CAAC;YACtC,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAA,yBAAgB,EAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,CAAC;KAAA;CACJ;AAnBD,oDAmBC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload/engine/index.ts`
|
|
3
|
+
* - isolated entry for the client upload engine: `import { ... } from 'lemon-model/upload/engine'`.
|
|
4
|
+
* - kept OUT of both the package root barrel and `lemon-model/upload`: a server consumer needs the
|
|
5
|
+
* contract only, and must not pull the engine in with it.
|
|
6
|
+
* - shell adapters (XHR, RN bridge, Electron fs) are NOT here — they belong to the consuming app.
|
|
7
|
+
*
|
|
8
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
9
|
+
*/
|
|
10
|
+
export * from './progress';
|
|
11
|
+
export * from './engine';
|
|
12
|
+
export * from './executors';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* `upload/engine/index.ts`
|
|
19
|
+
* - isolated entry for the client upload engine: `import { ... } from 'lemon-model/upload/engine'`.
|
|
20
|
+
* - kept OUT of both the package root barrel and `lemon-model/upload`: a server consumer needs the
|
|
21
|
+
* contract only, and must not pull the engine in with it.
|
|
22
|
+
* - shell adapters (XHR, RN bridge, Electron fs) are NOT here — they belong to the consuming app.
|
|
23
|
+
*
|
|
24
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
25
|
+
*/
|
|
26
|
+
__exportStar(require("./progress"), exports);
|
|
27
|
+
__exportStar(require("./engine"), exports);
|
|
28
|
+
__exportStar(require("./executors"), exports);
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/upload/engine/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;;;;;;GAQG;AACH,6CAA2B;AAC3B,2CAAyB;AACzB,8CAA4B"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload/engine/progress.ts`
|
|
3
|
+
* - progress is a CLIENT-LOCAL signal: nothing here travels on the wire and no server ever sees it.
|
|
4
|
+
* the server's knowledge of progress differs per transfer kind (a presigned PUT it cannot see at all),
|
|
5
|
+
* so the contract deliberately carries none of it.
|
|
6
|
+
* - per-file percent + one batch percent. everything is measured in bytes of the ORIGINAL
|
|
7
|
+
* content, never in wire bytes, so the bar means the same thing whichever executor runs.
|
|
8
|
+
*
|
|
9
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
10
|
+
*/
|
|
11
|
+
/** bytes of the ORIGINAL send sent so far for one file. called 0..n times; never after run() settles */
|
|
12
|
+
export declare type UploadProgressSink = (sent: number) => void;
|
|
13
|
+
/** what an HTTP primitive can observe: request-body bytes (`XMLHttpRequest.upload.onprogress`). fetch never calls it */
|
|
14
|
+
export declare type UploadWireProgress = (loaded: number, total: number) => void;
|
|
15
|
+
/** scale request-body progress (base64 JSON or raw bytes) to original-send bytes */
|
|
16
|
+
export declare const asContentBytes: (loaded: number, total: number, contentSize: number) => number;
|
|
17
|
+
/** progress of one file in the batch; `index` matches the `sources` array (exists before any server id does) */
|
|
18
|
+
export interface UploadFileProgress {
|
|
19
|
+
index: number;
|
|
20
|
+
/** original send bytes — known before `start` */
|
|
21
|
+
total: number;
|
|
22
|
+
/** bytes reported by the executor; undefined until the first report = indeterminate */
|
|
23
|
+
sent?: number;
|
|
24
|
+
/** the slot is done for the bar's purpose: stored, failed, rejected at start, or dedup */
|
|
25
|
+
settled: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface UploadBatchProgress {
|
|
28
|
+
files: ReadonlyArray<UploadFileProgress>;
|
|
29
|
+
/** sum of `total` — fixed for the whole batch so the bar never moves backwards */
|
|
30
|
+
totalBytes: number;
|
|
31
|
+
/** sum of (settled ? total : sent ?? 0) */
|
|
32
|
+
sentBytes: number;
|
|
33
|
+
/** sentBytes / totalBytes in [0, 1]; when totalBytes is 0, settled files / all files */
|
|
34
|
+
ratio: number;
|
|
35
|
+
}
|
|
36
|
+
export declare type UploadBatchProgressSink = (progress: UploadBatchProgress) => void;
|
|
37
|
+
/** byte-weighted aggregation; one immutable snapshot per change so a UI store can replace state as-is */
|
|
38
|
+
export declare class UploadProgressTracker {
|
|
39
|
+
private readonly emit?;
|
|
40
|
+
private files;
|
|
41
|
+
private readonly totalBytes;
|
|
42
|
+
constructor(totals: ReadonlyArray<number>, emit?: UploadBatchProgressSink);
|
|
43
|
+
/** the per-file sink handed to an executor: clamps to [previous, total], ignores reports after settle */
|
|
44
|
+
sink(index: number): UploadProgressSink;
|
|
45
|
+
settle(index: number): void;
|
|
46
|
+
snapshot(): UploadBatchProgress;
|
|
47
|
+
publish(): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `upload/engine/progress.ts`
|
|
4
|
+
* - progress is a CLIENT-LOCAL signal: nothing here travels on the wire and no server ever sees it.
|
|
5
|
+
* the server's knowledge of progress differs per transfer kind (a presigned PUT it cannot see at all),
|
|
6
|
+
* so the contract deliberately carries none of it.
|
|
7
|
+
* - per-file percent + one batch percent. everything is measured in bytes of the ORIGINAL
|
|
8
|
+
* content, never in wire bytes, so the bar means the same thing whichever executor runs.
|
|
9
|
+
*
|
|
10
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.UploadProgressTracker = exports.asContentBytes = void 0;
|
|
14
|
+
/** scale request-body progress (base64 JSON or raw bytes) to original-send bytes */
|
|
15
|
+
const asContentBytes = (loaded, total, contentSize) => total > 0 ? Math.min(contentSize, Math.round((loaded / total) * contentSize)) : 0;
|
|
16
|
+
exports.asContentBytes = asContentBytes;
|
|
17
|
+
/** byte-weighted aggregation; one immutable snapshot per change so a UI store can replace state as-is */
|
|
18
|
+
class UploadProgressTracker {
|
|
19
|
+
constructor(totals, emit) {
|
|
20
|
+
this.emit = emit;
|
|
21
|
+
this.files = totals.map((total, index) => ({ index, total: Math.max(0, total), settled: false }));
|
|
22
|
+
this.totalBytes = this.files.reduce((sum, file) => sum + file.total, 0);
|
|
23
|
+
}
|
|
24
|
+
/** the per-file sink handed to an executor: clamps to [previous, total], ignores reports after settle */
|
|
25
|
+
sink(index) {
|
|
26
|
+
return sent => {
|
|
27
|
+
var _a;
|
|
28
|
+
const file = this.files[index];
|
|
29
|
+
if (!file || file.settled)
|
|
30
|
+
return;
|
|
31
|
+
const next = Math.min(file.total, Math.max(sent, (_a = file.sent) !== null && _a !== void 0 ? _a : 0));
|
|
32
|
+
if (next === file.sent)
|
|
33
|
+
return;
|
|
34
|
+
this.files = this.files.map(item => (item.index === index ? Object.assign(Object.assign({}, item), { sent: next }) : item));
|
|
35
|
+
this.publish();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
settle(index) {
|
|
39
|
+
const file = this.files[index];
|
|
40
|
+
if (!file || file.settled)
|
|
41
|
+
return;
|
|
42
|
+
this.files = this.files.map(item => (item.index === index ? Object.assign(Object.assign({}, item), { settled: true }) : item));
|
|
43
|
+
this.publish();
|
|
44
|
+
}
|
|
45
|
+
snapshot() {
|
|
46
|
+
const sentBytes = this.files.reduce((sum, file) => { var _a; return sum + (file.settled ? file.total : (_a = file.sent) !== null && _a !== void 0 ? _a : 0); }, 0);
|
|
47
|
+
const settled = this.files.filter(file => file.settled).length;
|
|
48
|
+
const ratio = this.totalBytes > 0 ? sentBytes / this.totalBytes : this.files.length > 0 ? settled / this.files.length : 1;
|
|
49
|
+
return { files: this.files, totalBytes: this.totalBytes, sentBytes, ratio };
|
|
50
|
+
}
|
|
51
|
+
publish() {
|
|
52
|
+
if (this.emit)
|
|
53
|
+
this.emit(this.snapshot());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.UploadProgressTracker = UploadProgressTracker;
|
|
57
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../../../src/upload/engine/progress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload/index.ts`
|
|
3
|
+
* - isolated entry for the upload contract: `import { ... } from 'lemon-model/upload'`.
|
|
4
|
+
* - kept OUT of the package root barrel: a consumer that only needs this contract must not pull
|
|
5
|
+
* the socket/genai runtime, and a subpath can be promoted to the root later without a breaking
|
|
6
|
+
* change (the reverse is not true).
|
|
7
|
+
*
|
|
8
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
9
|
+
*/
|
|
10
|
+
export * from './types';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* `upload/index.ts`
|
|
19
|
+
* - isolated entry for the upload contract: `import { ... } from 'lemon-model/upload'`.
|
|
20
|
+
* - kept OUT of the package root barrel: a consumer that only needs this contract must not pull
|
|
21
|
+
* the socket/genai runtime, and a subpath can be promoted to the root later without a breaking
|
|
22
|
+
* change (the reverse is not true).
|
|
23
|
+
*
|
|
24
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
25
|
+
*/
|
|
26
|
+
__exportStar(require("./types"), exports);
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/upload/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA;;;;;;;;GAQG;AACH,0CAAwB"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload/testing.ts`
|
|
3
|
+
* - shared fixtures so a server spec and a client spec assert against the same payloads.
|
|
4
|
+
* - kept OUT of the root barrel like the other `<module>/testing` entries: `lemon-model/upload/testing`.
|
|
5
|
+
* - fixtures never carry a real signature or a real host.
|
|
6
|
+
*
|
|
7
|
+
* @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
|
|
8
|
+
*/
|
|
9
|
+
import type { Upload, UploadTicket } from './types';
|
|
10
|
+
/** a settled image as the server returns it after `complete`, with the preview the client sent */
|
|
11
|
+
export declare const SAMPLE_UPLOAD_STORED: Upload;
|
|
12
|
+
/** start slot: inline ticket */
|
|
13
|
+
export declare const SAMPLE_UPLOAD_TICKET_INLINE: UploadTicket;
|
|
14
|
+
/** start slot: presigned PUT ticket */
|
|
15
|
+
export declare const SAMPLE_UPLOAD_TICKET_PRESIGNED: UploadTicket;
|
|
16
|
+
/**
|
|
17
|
+
* start slot for an upload that is already `stored` and is only taking a thumbnail.
|
|
18
|
+
* - `transfer` absent + `thumbnailTransfer` present: the original's bytes are immutable.
|
|
19
|
+
* - an engine that returns early on a missing `transfer` silently drops the preview.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SAMPLE_UPLOAD_TICKET_THUMBNAIL_ONLY: UploadTicket;
|
|
22
|
+
/** start slot rejected at validation: no id, nothing to transfer or complete */
|
|
23
|
+
export declare const SAMPLE_UPLOAD_REJECTED: UploadTicket;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SAMPLE_UPLOAD_REJECTED = exports.SAMPLE_UPLOAD_TICKET_THUMBNAIL_ONLY = exports.SAMPLE_UPLOAD_TICKET_PRESIGNED = exports.SAMPLE_UPLOAD_TICKET_INLINE = exports.SAMPLE_UPLOAD_STORED = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/** a settled image as the server returns it after `complete`, with the preview the client sent */
|
|
6
|
+
exports.SAMPLE_UPLOAD_STORED = {
|
|
7
|
+
id: 'up-001',
|
|
8
|
+
status: types_1.UPLOAD_STATUS.stored,
|
|
9
|
+
stereo: types_1.UPLOAD_STEREO.image,
|
|
10
|
+
name: 'photo.png',
|
|
11
|
+
contentType: 'image/png',
|
|
12
|
+
contentSize: 123456,
|
|
13
|
+
url: 'https://cdn.example.com/up-001.png',
|
|
14
|
+
width: 1024,
|
|
15
|
+
height: 768,
|
|
16
|
+
thumbnail: { url: 'https://cdn.example.com/up-001-thumb.jpg', contentType: 'image/jpeg' },
|
|
17
|
+
};
|
|
18
|
+
/** start slot: inline ticket */
|
|
19
|
+
exports.SAMPLE_UPLOAD_TICKET_INLINE = {
|
|
20
|
+
upload: {
|
|
21
|
+
id: 'up-002',
|
|
22
|
+
status: types_1.UPLOAD_STATUS.pending,
|
|
23
|
+
stereo: types_1.UPLOAD_STEREO.image,
|
|
24
|
+
name: 'photo.png',
|
|
25
|
+
contentType: 'image/png',
|
|
26
|
+
contentSize: 123456,
|
|
27
|
+
},
|
|
28
|
+
transfer: { kind: types_1.UPLOAD_TRANSFER_KIND.inline, maxBytes: 4000000 },
|
|
29
|
+
};
|
|
30
|
+
/** start slot: presigned PUT ticket */
|
|
31
|
+
exports.SAMPLE_UPLOAD_TICKET_PRESIGNED = {
|
|
32
|
+
upload: {
|
|
33
|
+
id: 'up-003',
|
|
34
|
+
status: types_1.UPLOAD_STATUS.pending,
|
|
35
|
+
stereo: types_1.UPLOAD_STEREO.image,
|
|
36
|
+
name: 'photo.png',
|
|
37
|
+
contentType: 'image/png',
|
|
38
|
+
contentSize: 123456,
|
|
39
|
+
},
|
|
40
|
+
transfer: {
|
|
41
|
+
kind: types_1.UPLOAD_TRANSFER_KIND.presignedPut,
|
|
42
|
+
method: 'PUT',
|
|
43
|
+
url: 'https://storage.example.com/up-003?X-Amz-Signature=masked',
|
|
44
|
+
headers: { 'content-type': 'image/png', 'content-length': '123456' },
|
|
45
|
+
maxBytes: 50000000,
|
|
46
|
+
expiresAt: 1757895300000,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* start slot for an upload that is already `stored` and is only taking a thumbnail.
|
|
51
|
+
* - `transfer` absent + `thumbnailTransfer` present: the original's bytes are immutable.
|
|
52
|
+
* - an engine that returns early on a missing `transfer` silently drops the preview.
|
|
53
|
+
*/
|
|
54
|
+
exports.SAMPLE_UPLOAD_TICKET_THUMBNAIL_ONLY = {
|
|
55
|
+
upload: {
|
|
56
|
+
id: 'up-001',
|
|
57
|
+
status: types_1.UPLOAD_STATUS.stored,
|
|
58
|
+
stereo: types_1.UPLOAD_STEREO.image,
|
|
59
|
+
name: 'photo.png',
|
|
60
|
+
contentType: 'image/png',
|
|
61
|
+
contentSize: 123456,
|
|
62
|
+
url: 'https://cdn.example.com/up-001.png',
|
|
63
|
+
},
|
|
64
|
+
thumbnailTransfer: {
|
|
65
|
+
kind: types_1.UPLOAD_TRANSFER_KIND.presignedPut,
|
|
66
|
+
method: 'PUT',
|
|
67
|
+
url: 'https://storage.example.com/up-001-thumb?X-Amz-Signature=masked',
|
|
68
|
+
headers: { 'content-type': 'image/jpeg' },
|
|
69
|
+
maxBytes: 200000,
|
|
70
|
+
expiresAt: 1757895300000,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
/** start slot rejected at validation: no id, nothing to transfer or complete */
|
|
74
|
+
exports.SAMPLE_UPLOAD_REJECTED = {
|
|
75
|
+
upload: {
|
|
76
|
+
status: types_1.UPLOAD_STATUS.failed,
|
|
77
|
+
name: 'big.png',
|
|
78
|
+
contentType: 'image/png',
|
|
79
|
+
contentSize: 6291456,
|
|
80
|
+
error: '413 TOO LARGE - 6291456 > 4000000',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=testing.js.map
|