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.
Files changed (36) hide show
  1. package/dist/esm/upload/engine/engine.js +204 -0
  2. package/dist/esm/upload/engine/engine.js.map +1 -0
  3. package/dist/esm/upload/engine/executors.js +57 -0
  4. package/dist/esm/upload/engine/executors.js.map +1 -0
  5. package/dist/esm/upload/engine/index.js +13 -0
  6. package/dist/esm/upload/engine/index.js.map +1 -0
  7. package/dist/esm/upload/engine/progress.js +52 -0
  8. package/dist/esm/upload/engine/progress.js.map +1 -0
  9. package/dist/esm/upload/index.js +11 -0
  10. package/dist/esm/upload/index.js.map +1 -0
  11. package/dist/esm/upload/testing.js +80 -0
  12. package/dist/esm/upload/testing.js.map +1 -0
  13. package/dist/esm/upload/types.js +84 -0
  14. package/dist/esm/upload/types.js.map +1 -0
  15. package/dist/upload/engine/engine.d.ts +73 -0
  16. package/dist/upload/engine/engine.js +210 -0
  17. package/dist/upload/engine/engine.js.map +1 -0
  18. package/dist/upload/engine/executors.d.ts +44 -0
  19. package/dist/upload/engine/executors.js +62 -0
  20. package/dist/upload/engine/executors.js.map +1 -0
  21. package/dist/upload/engine/index.d.ts +12 -0
  22. package/dist/upload/engine/index.js +29 -0
  23. package/dist/upload/engine/index.js.map +1 -0
  24. package/dist/upload/engine/progress.d.ts +48 -0
  25. package/dist/upload/engine/progress.js +57 -0
  26. package/dist/upload/engine/progress.js.map +1 -0
  27. package/dist/upload/index.d.ts +10 -0
  28. package/dist/upload/index.js +27 -0
  29. package/dist/upload/index.js.map +1 -0
  30. package/dist/upload/testing.d.ts +23 -0
  31. package/dist/upload/testing.js +83 -0
  32. package/dist/upload/testing.js.map +1 -0
  33. package/dist/upload/types.d.ts +254 -0
  34. package/dist/upload/types.js +89 -0
  35. package/dist/upload/types.js.map +1 -0
  36. package/package.json +26 -1
@@ -0,0 +1,204 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { UPLOAD_FAILURE_CODE, UPLOAD_FAILURE_SOURCE } from '../types.js';
11
+ import { UploadProgressTracker } from './progress.js';
12
+ /**
13
+ * transfers in flight at once.
14
+ * - a browser queues beyond ~6 per origin, and a queued presigned url burns its `expiresAt` while it waits.
15
+ * - override per engine when a shell measures something better.
16
+ */
17
+ export const UPLOAD_DEFAULT_CONCURRENCY = 3;
18
+ /** run `task` over 0..count-1 with at most `limit` in flight; results keep input order */
19
+ const runPooled = (count, limit, task) => __awaiter(void 0, void 0, void 0, function* () {
20
+ const results = new Array(count);
21
+ let next = 0;
22
+ const worker = () => __awaiter(void 0, void 0, void 0, function* () {
23
+ for (;;) {
24
+ const index = next++;
25
+ if (index >= count)
26
+ return;
27
+ results[index] = yield task(index);
28
+ }
29
+ });
30
+ const size = Math.max(1, Math.min(limit, count));
31
+ yield Promise.all(new Array(size).fill(0).map(() => worker()));
32
+ return results;
33
+ });
34
+ const asContent = (source) => __awaiter(void 0, void 0, void 0, function* () {
35
+ return ({
36
+ contentType: source.contentType,
37
+ contentSize: source.contentSize,
38
+ width: source.width,
39
+ height: source.height,
40
+ hash: source.hash ? yield source.hash() : undefined,
41
+ });
42
+ });
43
+ const asIntent = (source) => __awaiter(void 0, void 0, void 0, function* () {
44
+ return (Object.assign(Object.assign({}, (yield asContent(source))), { name: source.name, thumbnail: source.thumbnail ? yield asContent(source.thumbnail) : undefined }));
45
+ });
46
+ /** bytes this source puts on the wire in total; the thumbnail rides along so the bar counts it */
47
+ 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); };
48
+ export class UploadEngine {
49
+ constructor(service, executors, options) {
50
+ this.service = service;
51
+ this.executors = executors;
52
+ this.options = options;
53
+ this.kinds = executors.map(executor => executor.kind);
54
+ }
55
+ /** returns one view per source, in order: `stored`, or `failed` with the reason. `onProgress` gets a snapshot per change */
56
+ upload(sources, onProgress) {
57
+ var _a, _b;
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ const tracker = new UploadProgressTracker(sources.map(sizeOf), onProgress);
60
+ tracker.publish();
61
+ const list = yield Promise.all(sources.map(asIntent));
62
+ const { list: tickets } = yield this.service.start({ list, transfers: this.kinds });
63
+ const limit = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.concurrency) !== null && _b !== void 0 ? _b : UPLOAD_DEFAULT_CONCURRENCY;
64
+ const receipts = yield runPooled(tickets.length, limit, i => this.transferTracked(tickets[i], sources[i], tracker, i));
65
+ // a server that broke rule 1 (shorter list) must not leave the bar hanging
66
+ sources.forEach((_, i) => tracker.settle(i));
67
+ const pending = receipts.filter((item) => !!item);
68
+ const settled = pending.length ? (yield this.service.complete({ list: pending })).list : [];
69
+ const byId = new Map(settled.filter(upload => !!upload.id).map((upload) => [upload.id, upload]));
70
+ return tickets.map(ticket => (ticket.upload.id && byId.get(ticket.upload.id)) || ticket.upload);
71
+ });
72
+ }
73
+ transferTracked(ticket, source, tracker, index) {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ try {
76
+ return yield this.transfer(ticket, source, tracker.sink(index));
77
+ }
78
+ catch (error) {
79
+ // an executor that throws (a CORS preflight failure surfaces as one) must not take the batch down
80
+ const id = ticket.upload.id;
81
+ if (!id)
82
+ return undefined;
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ return {
85
+ id,
86
+ failure: { source: UPLOAD_FAILURE_SOURCE.client, code: UPLOAD_FAILURE_CODE.unknown, message },
87
+ };
88
+ }
89
+ finally {
90
+ tracker.settle(index);
91
+ }
92
+ });
93
+ }
94
+ transfer(ticket, source, onProgress) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ const id = ticket.upload.id;
97
+ // rejected at start: nothing was created, nothing to complete
98
+ if (!id)
99
+ return undefined;
100
+ const receipt = yield this.transferOriginal(id, ticket.transfer, source, onProgress);
101
+ // a failed original has nothing to preview; otherwise the thumbnail rides after it (never with it)
102
+ if (!receipt.failure)
103
+ yield this.transferThumbnail(id, ticket.thumbnailTransfer, source, onProgress);
104
+ return receipt;
105
+ });
106
+ }
107
+ transferOriginal(id, transfer, source, onProgress) {
108
+ return __awaiter(this, void 0, void 0, function* () {
109
+ // no instruction: already stored (dedup, or a stored upload taking only a thumbnail) — complete still confirms it
110
+ if (!transfer)
111
+ return { id };
112
+ const executor = this.executors.find(candidate => candidate.kind === transfer.kind);
113
+ if (!executor)
114
+ return { id, failure: { source: UPLOAD_FAILURE_SOURCE.client, code: UPLOAD_FAILURE_CODE.notAcceptable } };
115
+ // NOTE: `transfer` must not be logged or stored — it may carry a credential url
116
+ return executor.run(id, transfer, source, onProgress);
117
+ });
118
+ }
119
+ /**
120
+ * the preview is subordinate: whatever happens here never reaches `complete`.
121
+ * - `UploadCompleteItem` is one per upload, so reporting a thumbnail failure would fail the original.
122
+ * - the server settles the thumbnail by looking at storage at `complete` time, so silence is enough.
123
+ */
124
+ transferThumbnail(id, transfer, source, onProgress) {
125
+ return __awaiter(this, void 0, void 0, function* () {
126
+ const thumbnail = source.thumbnail;
127
+ if (!transfer || !thumbnail)
128
+ return;
129
+ const executor = this.executors.find(candidate => candidate.kind === transfer.kind);
130
+ if (!executor)
131
+ return;
132
+ // the original's bytes are already counted, so the thumbnail reports on top of them
133
+ const base = source.contentSize;
134
+ try {
135
+ yield executor.run(id, transfer, thumbnail, sent => onProgress(base + sent));
136
+ }
137
+ catch (_a) {
138
+ // swallowed on purpose: see the note above
139
+ }
140
+ });
141
+ }
142
+ }
143
+ /** lemon-core error convention: "<status> <LABEL> - <detail>" (status 0 = no response, XHR convention) */
144
+ const API_ERROR = /^(\d{1,3}) ([A-Z][A-Z ]*?) - ([\s\S]*)$/;
145
+ const codeOfApiStatus = (status) => {
146
+ switch (status) {
147
+ case 0:
148
+ return UPLOAD_FAILURE_CODE.network;
149
+ case 400:
150
+ return UPLOAD_FAILURE_CODE.invalid;
151
+ case 404:
152
+ return UPLOAD_FAILURE_CODE.notFound;
153
+ case 406:
154
+ return UPLOAD_FAILURE_CODE.notAcceptable;
155
+ case 409:
156
+ return UPLOAD_FAILURE_CODE.conflict;
157
+ case 410:
158
+ return UPLOAD_FAILURE_CODE.expired;
159
+ case 413:
160
+ return UPLOAD_FAILURE_CODE.tooLarge;
161
+ case 415:
162
+ return UPLOAD_FAILURE_CODE.unsupported;
163
+ default:
164
+ return UPLOAD_FAILURE_CODE.unknown;
165
+ }
166
+ };
167
+ /** normalize an error thrown by the API adapter */
168
+ export const asApiFailure = (error) => {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ const match = API_ERROR.exec(message);
171
+ if (!match)
172
+ return { source: UPLOAD_FAILURE_SOURCE.api, code: UPLOAD_FAILURE_CODE.unknown, message };
173
+ const status = Number(match[1]);
174
+ return {
175
+ source: UPLOAD_FAILURE_SOURCE.api,
176
+ code: codeOfApiStatus(status),
177
+ status,
178
+ reason: match[2],
179
+ message: match[3],
180
+ };
181
+ };
182
+ /** S3 error `<Code>` -> normalized code. unknown codes fall back by status */
183
+ const STORAGE_CODES = {
184
+ SignatureDoesNotMatch: UPLOAD_FAILURE_CODE.signature,
185
+ AccessDenied: UPLOAD_FAILURE_CODE.expired,
186
+ ExpiredToken: UPLOAD_FAILURE_CODE.expired,
187
+ BadDigest: UPLOAD_FAILURE_CODE.checksum,
188
+ XAmzContentSHA256Mismatch: UPLOAD_FAILURE_CODE.checksum,
189
+ EntityTooLarge: UPLOAD_FAILURE_CODE.tooLarge,
190
+ RequestTimeout: UPLOAD_FAILURE_CODE.network,
191
+ };
192
+ /** normalize a non-2xx answer from the storage endpoint (status 0 = no response) */
193
+ export const asStorageFailure = (status, code) => {
194
+ const known = code ? STORAGE_CODES[code] : undefined;
195
+ const byStatus = status === 0
196
+ ? UPLOAD_FAILURE_CODE.network
197
+ : status === 403
198
+ ? UPLOAD_FAILURE_CODE.signature
199
+ : status === 413
200
+ ? UPLOAD_FAILURE_CODE.tooLarge
201
+ : UPLOAD_FAILURE_CODE.unknown;
202
+ return { source: UPLOAD_FAILURE_SOURCE.storage, code: known !== null && known !== void 0 ? known : byStatus, status, reason: code };
203
+ };
204
+ //# 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,57 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { UPLOAD_FAILURE_CODE, UPLOAD_FAILURE_SOURCE, UPLOAD_TRANSFER_KIND } from '../types.js';
11
+ import { asApiFailure, asStorageFailure } from './engine.js';
12
+ import { asContentBytes } from './progress.js';
13
+ /** inline transfer — the only place that knows bytes go to the API as base64 */
14
+ export class InlineExecutor {
15
+ constructor(send,
16
+ /** shell-specific base64 encoder (btoa / Buffer / RN polyfill) */
17
+ toBase64) {
18
+ this.send = send;
19
+ this.toBase64 = toBase64;
20
+ this.kind = UPLOAD_TRANSFER_KIND.inline;
21
+ }
22
+ run(id, transfer, source, onProgress) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ if (source.contentSize > transfer.maxBytes) {
25
+ return { id, failure: { source: UPLOAD_FAILURE_SOURCE.client, code: UPLOAD_FAILURE_CODE.tooLarge } };
26
+ }
27
+ try {
28
+ const body = { content: this.toBase64(yield source.bytes()) };
29
+ // wire bytes are base64 JSON (~4/3 of the send): report in send bytes
30
+ yield this.send(id, body, (loaded, total) => onProgress(asContentBytes(loaded, total, source.contentSize)));
31
+ return { id };
32
+ }
33
+ catch (error) {
34
+ return { id, failure: asApiFailure(error) };
35
+ }
36
+ });
37
+ }
38
+ }
39
+ /** presigned PUT transfer — raw bytes straight to storage */
40
+ export class PresignedPutExecutor {
41
+ constructor(put) {
42
+ this.put = put;
43
+ this.kind = UPLOAD_TRANSFER_KIND.presignedPut;
44
+ }
45
+ run(id, transfer, source, onProgress) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ const res = yield this.put(transfer.url, transfer.headers, yield source.bytes(), (loaded, total) => onProgress(asContentBytes(loaded, total, source.contentSize)));
48
+ if (res.status >= 200 && res.status < 300)
49
+ return { id };
50
+ // 412 = object already exists under this key; the server decides at complete
51
+ if (res.status === 412)
52
+ return { id };
53
+ return { id, failure: asStorageFailure(res.status, res.code) };
54
+ });
55
+ }
56
+ }
57
+ //# sourceMappingURL=executors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executors.js","sourceRoot":"","sources":["../../../../src/upload/engine/executors.ts"],"names":[],"mappings":";;;;;;;;;AAgBA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAE5F,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAS5C,gFAAgF;AAChF,MAAM,OAAO,cAAc;IAGvB,YACqB,IAAoB;IACrC,kEAAkE;IACjD,QAAuC;QAFvC,SAAI,GAAJ,IAAI,CAAgB;QAEpB,aAAQ,GAAR,QAAQ,CAA+B;QAL5C,SAAI,GAAG,oBAAoB,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,qBAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,mBAAmB,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,cAAc,CAAC,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,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;aAC/C;QACL,CAAC;KAAA;CACJ;AAaD,6DAA6D;AAC7D,MAAM,OAAO,oBAAoB;IAG7B,YAAoC,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;QAF/B,SAAI,GAAG,oBAAoB,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,cAAc,CAAC,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,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnE,CAAC;KAAA;CACJ"}
@@ -0,0 +1,13 @@
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.js';
11
+ export * from './engine.js';
12
+ export * from './executors.js';
13
+ //# 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,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC"}
@@ -0,0 +1,52 @@
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
+ /** scale request-body progress (base64 JSON or raw bytes) to original-send bytes */
12
+ export const asContentBytes = (loaded, total, contentSize) => total > 0 ? Math.min(contentSize, Math.round((loaded / total) * contentSize)) : 0;
13
+ /** byte-weighted aggregation; one immutable snapshot per change so a UI store can replace state as-is */
14
+ export class UploadProgressTracker {
15
+ constructor(totals, emit) {
16
+ this.emit = emit;
17
+ this.files = totals.map((total, index) => ({ index, total: Math.max(0, total), settled: false }));
18
+ this.totalBytes = this.files.reduce((sum, file) => sum + file.total, 0);
19
+ }
20
+ /** the per-file sink handed to an executor: clamps to [previous, total], ignores reports after settle */
21
+ sink(index) {
22
+ return sent => {
23
+ var _a;
24
+ const file = this.files[index];
25
+ if (!file || file.settled)
26
+ return;
27
+ const next = Math.min(file.total, Math.max(sent, (_a = file.sent) !== null && _a !== void 0 ? _a : 0));
28
+ if (next === file.sent)
29
+ return;
30
+ this.files = this.files.map(item => (item.index === index ? Object.assign(Object.assign({}, item), { sent: next }) : item));
31
+ this.publish();
32
+ };
33
+ }
34
+ settle(index) {
35
+ const file = this.files[index];
36
+ if (!file || file.settled)
37
+ return;
38
+ this.files = this.files.map(item => (item.index === index ? Object.assign(Object.assign({}, item), { settled: true }) : item));
39
+ this.publish();
40
+ }
41
+ snapshot() {
42
+ 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);
43
+ const settled = this.files.filter(file => file.settled).length;
44
+ const ratio = this.totalBytes > 0 ? sentBytes / this.totalBytes : this.files.length > 0 ? settled / this.files.length : 1;
45
+ return { files: this.files, totalBytes: this.totalBytes, sentBytes, ratio };
46
+ }
47
+ publish() {
48
+ if (this.emit)
49
+ this.emit(this.snapshot());
50
+ }
51
+ }
52
+ //# 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,11 @@
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.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/upload/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,cAAc,SAAS,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { UPLOAD_STATUS, UPLOAD_STEREO, UPLOAD_TRANSFER_KIND } from './types.js';
2
+ /** a settled image as the server returns it after `complete`, with the preview the client sent */
3
+ export const SAMPLE_UPLOAD_STORED = {
4
+ id: 'up-001',
5
+ status: UPLOAD_STATUS.stored,
6
+ stereo: UPLOAD_STEREO.image,
7
+ name: 'photo.png',
8
+ contentType: 'image/png',
9
+ contentSize: 123456,
10
+ url: 'https://cdn.example.com/up-001.png',
11
+ width: 1024,
12
+ height: 768,
13
+ thumbnail: { url: 'https://cdn.example.com/up-001-thumb.jpg', contentType: 'image/jpeg' },
14
+ };
15
+ /** start slot: inline ticket */
16
+ export const SAMPLE_UPLOAD_TICKET_INLINE = {
17
+ upload: {
18
+ id: 'up-002',
19
+ status: UPLOAD_STATUS.pending,
20
+ stereo: UPLOAD_STEREO.image,
21
+ name: 'photo.png',
22
+ contentType: 'image/png',
23
+ contentSize: 123456,
24
+ },
25
+ transfer: { kind: UPLOAD_TRANSFER_KIND.inline, maxBytes: 4000000 },
26
+ };
27
+ /** start slot: presigned PUT ticket */
28
+ export const SAMPLE_UPLOAD_TICKET_PRESIGNED = {
29
+ upload: {
30
+ id: 'up-003',
31
+ status: UPLOAD_STATUS.pending,
32
+ stereo: UPLOAD_STEREO.image,
33
+ name: 'photo.png',
34
+ contentType: 'image/png',
35
+ contentSize: 123456,
36
+ },
37
+ transfer: {
38
+ kind: UPLOAD_TRANSFER_KIND.presignedPut,
39
+ method: 'PUT',
40
+ url: 'https://storage.example.com/up-003?X-Amz-Signature=masked',
41
+ headers: { 'content-type': 'image/png', 'content-length': '123456' },
42
+ maxBytes: 50000000,
43
+ expiresAt: 1757895300000,
44
+ },
45
+ };
46
+ /**
47
+ * start slot for an upload that is already `stored` and is only taking a thumbnail.
48
+ * - `transfer` absent + `thumbnailTransfer` present: the original's bytes are immutable.
49
+ * - an engine that returns early on a missing `transfer` silently drops the preview.
50
+ */
51
+ export const SAMPLE_UPLOAD_TICKET_THUMBNAIL_ONLY = {
52
+ upload: {
53
+ id: 'up-001',
54
+ status: UPLOAD_STATUS.stored,
55
+ stereo: UPLOAD_STEREO.image,
56
+ name: 'photo.png',
57
+ contentType: 'image/png',
58
+ contentSize: 123456,
59
+ url: 'https://cdn.example.com/up-001.png',
60
+ },
61
+ thumbnailTransfer: {
62
+ kind: UPLOAD_TRANSFER_KIND.presignedPut,
63
+ method: 'PUT',
64
+ url: 'https://storage.example.com/up-001-thumb?X-Amz-Signature=masked',
65
+ headers: { 'content-type': 'image/jpeg' },
66
+ maxBytes: 200000,
67
+ expiresAt: 1757895300000,
68
+ },
69
+ };
70
+ /** start slot rejected at validation: no id, nothing to transfer or complete */
71
+ export const SAMPLE_UPLOAD_REJECTED = {
72
+ upload: {
73
+ status: UPLOAD_STATUS.failed,
74
+ name: 'big.png',
75
+ contentType: 'image/png',
76
+ contentSize: 6291456,
77
+ error: '413 TOO LARGE - 6291456 > 4000000',
78
+ },
79
+ };
80
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../../src/upload/testing.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE7E,kGAAkG;AAClG,MAAM,CAAC,MAAM,oBAAoB,GAAW;IACxC,EAAE,EAAE,QAAQ;IACZ,MAAM,EAAE,aAAa,CAAC,MAAM;IAC5B,MAAM,EAAE,aAAa,CAAC,KAAK;IAC3B,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,WAAW;IACxB,WAAW,EAAE,MAAM;IACnB,GAAG,EAAE,oCAAoC;IACzC,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,GAAG;IACX,SAAS,EAAE,EAAE,GAAG,EAAE,0CAA0C,EAAE,WAAW,EAAE,YAAY,EAAE;CAC5F,CAAC;AAEF,gCAAgC;AAChC,MAAM,CAAC,MAAM,2BAA2B,GAAiB;IACrD,MAAM,EAAE;QACJ,EAAE,EAAE,QAAQ;QACZ,MAAM,EAAE,aAAa,CAAC,OAAO;QAC7B,MAAM,EAAE,aAAa,CAAC,KAAK;QAC3B,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE,MAAM;KACtB;IACD,QAAQ,EAAE,EAAE,IAAI,EAAE,oBAAoB,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE;CACrE,CAAC;AAEF,uCAAuC;AACvC,MAAM,CAAC,MAAM,8BAA8B,GAAiB;IACxD,MAAM,EAAE;QACJ,EAAE,EAAE,QAAQ;QACZ,MAAM,EAAE,aAAa,CAAC,OAAO;QAC7B,MAAM,EAAE,aAAa,CAAC,KAAK;QAC3B,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE,MAAM;KACtB;IACD,QAAQ,EAAE;QACN,IAAI,EAAE,oBAAoB,CAAC,YAAY;QACvC,MAAM,EAAE,KAAK;QACb,GAAG,EAAE,2DAA2D;QAChE,OAAO,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,gBAAgB,EAAE,QAAQ,EAAE;QACpE,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,aAAa;KAC3B;CACJ,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAiB;IAC7D,MAAM,EAAE;QACJ,EAAE,EAAE,QAAQ;QACZ,MAAM,EAAE,aAAa,CAAC,MAAM;QAC5B,MAAM,EAAE,aAAa,CAAC,KAAK;QAC3B,IAAI,EAAE,WAAW;QACjB,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE,MAAM;QACnB,GAAG,EAAE,oCAAoC;KAC5C;IACD,iBAAiB,EAAE;QACf,IAAI,EAAE,oBAAoB,CAAC,YAAY;QACvC,MAAM,EAAE,KAAK;QACb,GAAG,EAAE,iEAAiE;QACtE,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE;QACzC,QAAQ,EAAE,MAAM;QAChB,SAAS,EAAE,aAAa;KAC3B;CACJ,CAAC;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAChD,MAAM,EAAE;QACJ,MAAM,EAAE,aAAa,CAAC,MAAM;QAC5B,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,WAAW;QACxB,WAAW,EAAE,OAAO;QACpB,KAAK,EAAE,mCAAmC;KAC7C;CACJ,CAAC"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `upload/types.ts`
3
+ * - client <-> server contract for uploading files.
4
+ * - transport-neutral: the client always runs `start` -> transfer -> `complete`; whether the server
5
+ * stores bytes inline (base64 in JSON) or hands out a presigned PUT only changes which
6
+ * `UploadTransfer` variant it issues.
7
+ * - runtime-neutral like `socket/`: no Node, DOM, or AWS SDK types. Storage coordinates
8
+ * (bucket, key, region) never appear here.
9
+ * - model-neutral: nothing here imports or names lemon-core's Model/View/Body/Head layers or the `$`
10
+ * embed marker. two shapes carry the vocabulary instead — `UploadContent` (bytes as declared) and
11
+ * `UploadResource` (bytes as stored, with an address). how a server maps them onto its own model
12
+ * layer, and how it embeds an upload in another record, is the server's concern.
13
+ *
14
+ * @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
15
+ */
16
+ /** kind of stored content */
17
+ export const UPLOAD_STEREO = {
18
+ image: 'image',
19
+ video: 'video',
20
+ sound: 'sound',
21
+ docs: 'docs',
22
+ };
23
+ /** lifecycle of one upload as the client sees it. `url` is meaningful only when `stored` */
24
+ export const UPLOAD_STATUS = {
25
+ /** ticket issued; bytes not confirmed by the server yet */
26
+ pending: 'pending',
27
+ /** bytes durable and `url` valid */
28
+ stored: 'stored',
29
+ /** terminal; `error` carries the server's detail */
30
+ failed: 'failed',
31
+ };
32
+ /** how bytes travel. a new transfer method arrives as a new kind; the flow around it does not change */
33
+ export const UPLOAD_TRANSFER_KIND = {
34
+ /** base64 in a JSON body, through the `send` operation */
35
+ inline: 'inline',
36
+ /** HTTP PUT straight to storage, to a server-issued url with its headers */
37
+ presignedPut: 'presigned-put',
38
+ };
39
+ export const isUploadStored = (upload) => upload.status === UPLOAD_STATUS.stored && typeof upload.id === 'string' && typeof upload.url === 'string';
40
+ /** who produced a failure */
41
+ export const UPLOAD_FAILURE_SOURCE = {
42
+ /** the upload API answered with an error */
43
+ api: 'api',
44
+ /** the storage endpoint (presigned PUT) answered with an error */
45
+ storage: 'storage',
46
+ /** the client gave up before or without a response */
47
+ client: 'client',
48
+ };
49
+ /** normalized failure codes; the app branches on these, never on raw HTTP status */
50
+ export const UPLOAD_FAILURE_CODE = {
51
+ invalid: 'invalid',
52
+ tooLarge: 'too-large',
53
+ unsupported: 'unsupported',
54
+ notAcceptable: 'not-acceptable',
55
+ notFound: 'not-found',
56
+ conflict: 'conflict',
57
+ expired: 'expired',
58
+ signature: 'signature',
59
+ checksum: 'checksum',
60
+ network: 'network',
61
+ aborted: 'aborted',
62
+ unknown: 'unknown',
63
+ };
64
+ /** HTTP binding relative to the server's upload base path (lemon-core `/{type}/{id}/{cmd}` shape) */
65
+ export const UPLOAD_ROUTES = {
66
+ start: { method: 'POST', path: '/start' },
67
+ send: { method: 'POST', path: '/{id}/send' },
68
+ complete: { method: 'POST', path: '/complete' },
69
+ read: { method: 'GET', path: '/{id}' },
70
+ };
71
+ /** shared stereo derivation so an optimistic client tile and the server agree */
72
+ export const uploadStereoOf = (contentType) => {
73
+ const type = contentType.toLowerCase().split(';')[0].trim();
74
+ if (type.startsWith('image/'))
75
+ return UPLOAD_STEREO.image;
76
+ if (type.startsWith('video/'))
77
+ return UPLOAD_STEREO.video;
78
+ if (type.startsWith('audio/'))
79
+ return UPLOAD_STEREO.sound;
80
+ if (type === 'application/pdf')
81
+ return UPLOAD_STEREO.docs;
82
+ return undefined;
83
+ };
84
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/upload/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6BAA6B;AAC7B,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;CACN,CAAC;AAGX,4FAA4F;AAC5F,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,2DAA2D;IAC3D,OAAO,EAAE,SAAS;IAClB,oCAAoC;IACpC,MAAM,EAAE,QAAQ;IAChB,oDAAoD;IACpD,MAAM,EAAE,QAAQ;CACV,CAAC;AAGX,wGAAwG;AACxG,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAChC,0DAA0D;IAC1D,MAAM,EAAE,QAAQ;IAChB,4EAA4E;IAC5E,YAAY,EAAE,eAAe;CACvB,CAAC;AAoEX,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAc,EAA0B,EAAE,CACrE,MAAM,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC;AA0E9G,6BAA6B;AAC7B,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACjC,4CAA4C;IAC5C,GAAG,EAAE,KAAK;IACV,kEAAkE;IAClE,OAAO,EAAE,SAAS;IAClB,sDAAsD;IACtD,MAAM,EAAE,QAAQ;CACV,CAAC;AAGX,oFAAoF;AACpF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IAC/B,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,aAAa;IAC1B,aAAa,EAAE,gBAAgB;IAC/B,QAAQ,EAAE,WAAW;IACrB,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,SAAS,EAAE,WAAW;IACtB,QAAQ,EAAE,UAAU;IACpB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACZ,CAAC;AA+CX,qGAAqG;AACrG,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;IACzC,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE;IAC5C,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE;IAC/C,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE;CAChC,CAAC;AAEX,iFAAiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,WAAmB,EAA4B,EAAE;IAC5E,MAAM,IAAI,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC;IAC1D,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC;IAC1D,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,aAAa,CAAC,KAAK,CAAC;IAC1D,IAAI,IAAI,KAAK,iBAAiB;QAAE,OAAO,aAAa,CAAC,IAAI,CAAC;IAC1D,OAAO,SAAS,CAAC;AACrB,CAAC,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `upload/engine/engine.ts`
3
+ * - the one upload flow every shell shares: `start` -> run executors -> `complete`.
4
+ * - swapping the server's transfer method never touches this file; a new `UploadTransfer` kind
5
+ * arrives as one more `UploadTransferExecutor` passed to the constructor.
6
+ * - runtime-neutral like the contract itself: no Node, DOM, or AWS types. the shell injects the
7
+ * three things that are shell-specific — reading bytes (`UploadSource`), the HTTP primitives
8
+ * (see `./executors`), and hashing.
9
+ *
10
+ * @copyright (C) 2026 LemonCloud Co Ltd. - All Rights Reserved.
11
+ */
12
+ import type { Upload, UploadCompleteItem, UploadFailure, UploadSupportable, UploadTransfer } from '../types';
13
+ import type { UploadBatchProgressSink, UploadProgressSink } from './progress';
14
+ /**
15
+ * one payload of bytes a shell can hand over: the contract's `UploadContent`, with the bytes behind a call.
16
+ * - the contract's `hash` is a string and this one is a call, so this does not extend `UploadContent`.
17
+ */
18
+ export interface UploadContentSource {
19
+ contentType: string;
20
+ contentSize: number;
21
+ /** pixel size for image/video; echoed into the intent so a list screen can reserve the box */
22
+ width?: number;
23
+ height?: number;
24
+ /** the whole payload in one call */
25
+ bytes(): Promise<Uint8Array>;
26
+ /** sha256 hex(64) when this shell chooses to hash at send time; omit to skip */
27
+ hash?(): Promise<string | undefined>;
28
+ }
29
+ /** bytes provider; hides DOM `File`, RN uri, Electron path behind one shape */
30
+ export interface UploadSource extends UploadContentSource {
31
+ name: string;
32
+ /** a preview this shell already made. the engine sends it beside the original and never derives one */
33
+ thumbnail?: UploadContentSource;
34
+ }
35
+ /** moves bytes for one transfer kind and reports the receipt the server expects in `complete` */
36
+ export interface UploadTransferExecutor<T extends UploadTransfer = UploadTransfer> {
37
+ readonly kind: T['kind'];
38
+ /**
39
+ * `onProgress` is optional to CALL, not to accept: an executor that cannot observe bytes never calls it.
40
+ * - `source` is a payload, not a file: the engine passes `source.thumbnail` here too, and that has no `name`.
41
+ */
42
+ run(id: string, transfer: T, source: UploadContentSource, onProgress: UploadProgressSink): Promise<UploadCompleteItem>;
43
+ }
44
+ /**
45
+ * transfers in flight at once.
46
+ * - a browser queues beyond ~6 per origin, and a queued presigned url burns its `expiresAt` while it waits.
47
+ * - override per engine when a shell measures something better.
48
+ */
49
+ export declare const UPLOAD_DEFAULT_CONCURRENCY = 3;
50
+ export declare class UploadEngine {
51
+ private readonly service;
52
+ private readonly executors;
53
+ private readonly options?;
54
+ private readonly kinds;
55
+ constructor(service: UploadSupportable, executors: ReadonlyArray<UploadTransferExecutor>, options?: {
56
+ concurrency?: number;
57
+ });
58
+ /** returns one view per source, in order: `stored`, or `failed` with the reason. `onProgress` gets a snapshot per change */
59
+ upload(sources: ReadonlyArray<UploadSource>, onProgress?: UploadBatchProgressSink): Promise<Upload[]>;
60
+ private transferTracked;
61
+ private transfer;
62
+ private transferOriginal;
63
+ /**
64
+ * the preview is subordinate: whatever happens here never reaches `complete`.
65
+ * - `UploadCompleteItem` is one per upload, so reporting a thumbnail failure would fail the original.
66
+ * - the server settles the thumbnail by looking at storage at `complete` time, so silence is enough.
67
+ */
68
+ private transferThumbnail;
69
+ }
70
+ /** normalize an error thrown by the API adapter */
71
+ export declare const asApiFailure: (error: unknown) => UploadFailure;
72
+ /** normalize a non-2xx answer from the storage endpoint (status 0 = no response) */
73
+ export declare const asStorageFailure: (status: number, code?: string) => UploadFailure;