@stackshift-cloud/sdk 1.3.0 → 1.4.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 +50 -0
- package/dist/asset-workflows.d.ts +5 -13
- package/dist/assets-platform-types.d.ts +19 -2
- package/dist/assets.d.ts +2 -1
- package/dist/assets.js +3 -0
- package/dist/client.js +2 -1
- package/dist/generated/assets-contract.d.ts +41 -6
- package/dist/generated/assets-contract.js +8 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +8 -0
- package/dist/mail-advanced-types.d.ts +11 -0
- package/dist/mail-billing.d.ts +4 -0
- package/dist/mail-campaigns.d.ts +1 -1
- package/dist/mail-mailboxes.d.ts +358 -0
- package/dist/mail-mailboxes.js +202 -0
- package/dist/mail-marketing.d.ts +9 -2
- package/dist/mail.d.ts +8 -0
- package/dist/mail.js +14 -0
- package/dist/s2-upload.d.ts +22 -0
- package/dist/s2-upload.js +49 -0
- package/dist/s2.d.ts +34 -0
- package/dist/s2.js +31 -0
- package/dist/support-action-manifest.d.ts +3 -0
- package/dist/support-action-manifest.js +40 -0
- package/dist/support-action-receiver.d.ts +20 -0
- package/dist/support-action-receiver.js +37 -0
- package/dist/support-action-sqlite.d.ts +17 -0
- package/dist/support-action-sqlite.js +30 -0
- package/dist/support-action-types.d.ts +93 -0
- package/dist/support-action-types.js +1 -0
- package/dist/support-actions.d.ts +19 -0
- package/dist/support-actions.js +213 -0
- package/dist/types.d.ts +17 -0
- package/dist/upload-controller.d.ts +2 -0
- package/dist/upload-controller.js +41 -18
- package/package.json +3 -2
|
@@ -16,13 +16,13 @@ export class ChunkedUploadController {
|
|
|
16
16
|
throwIfAborted(options.signal);
|
|
17
17
|
const resumeKey = options.resumeKey;
|
|
18
18
|
const fingerprint = fileFingerprint(file, request);
|
|
19
|
-
let session = await this.restore(resumeKey, fingerprint);
|
|
19
|
+
let session = await this.restore(resumeKey, fingerprint, options.requireMatchingFile);
|
|
20
20
|
if (!session)
|
|
21
21
|
session = await this.transport.create(file, request);
|
|
22
22
|
validateSession(session, file.size);
|
|
23
23
|
options.onSession?.(session);
|
|
24
24
|
await this.persist(resumeKey, fingerprint, session);
|
|
25
|
-
const completed = await verifiedCompletedParts(file, session);
|
|
25
|
+
const completed = await verifiedCompletedParts(file, session, options.requireMatchingFile);
|
|
26
26
|
const loadedByPart = new Map(completed);
|
|
27
27
|
const tasks = [];
|
|
28
28
|
for (let offset = 0, partNumber = 1; offset < file.size; offset += session.partSize, partNumber += 1) {
|
|
@@ -37,8 +37,9 @@ export class ChunkedUploadController {
|
|
|
37
37
|
const concurrency = boundedInteger(options.concurrency, 4, 1, 8);
|
|
38
38
|
const attempts = boundedInteger(options.retryAttempts, 4, 1, 8);
|
|
39
39
|
let next = 0;
|
|
40
|
+
let workerFailed = false;
|
|
40
41
|
const worker = async () => {
|
|
41
|
-
while (next < tasks.length) {
|
|
42
|
+
while (!workerFailed && next < tasks.length) {
|
|
42
43
|
throwIfAborted(options.signal);
|
|
43
44
|
const task = tasks[next++];
|
|
44
45
|
const checksum = await sha256Blob(task.chunk);
|
|
@@ -61,7 +62,16 @@ export class ChunkedUploadController {
|
|
|
61
62
|
}
|
|
62
63
|
};
|
|
63
64
|
try {
|
|
64
|
-
await Promise.
|
|
65
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(concurrency, tasks.length || 1) }, () => worker().catch((error) => {
|
|
66
|
+
workerFailed = true;
|
|
67
|
+
throw error;
|
|
68
|
+
})));
|
|
69
|
+
const failed = workers.find((result) => result.status === 'rejected');
|
|
70
|
+
if (failed?.status === 'rejected')
|
|
71
|
+
throw failed.reason;
|
|
72
|
+
throwIfAborted(options.signal);
|
|
73
|
+
session.completionPending = true;
|
|
74
|
+
await this.persist(resumeKey, fingerprint, session);
|
|
65
75
|
const result = await retryWithBackoff(() => this.transport.complete(session), attempts, options.signal);
|
|
66
76
|
if (resumeKey)
|
|
67
77
|
await this.store?.delete(resumeKey);
|
|
@@ -69,9 +79,12 @@ export class ChunkedUploadController {
|
|
|
69
79
|
}
|
|
70
80
|
catch (error) {
|
|
71
81
|
if (isAbortError(error) && options.cancelOnAbort) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
try {
|
|
83
|
+
await this.transport.cancel(session);
|
|
84
|
+
if (resumeKey)
|
|
85
|
+
await this.store?.delete(resumeKey);
|
|
86
|
+
}
|
|
87
|
+
catch { /* Retain recovery state when remote cancellation is uncertain. */ }
|
|
75
88
|
}
|
|
76
89
|
throw error;
|
|
77
90
|
}
|
|
@@ -80,19 +93,24 @@ export class ChunkedUploadController {
|
|
|
80
93
|
const record = await this.store?.get(resumeKey);
|
|
81
94
|
if (!record)
|
|
82
95
|
return;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
await this.store?.delete(resumeKey);
|
|
88
|
-
}
|
|
96
|
+
await this.transport.cancel(record.session);
|
|
97
|
+
await this.store?.delete(resumeKey);
|
|
89
98
|
}
|
|
90
|
-
async restore(resumeKey, fingerprint) {
|
|
99
|
+
async restore(resumeKey, fingerprint, requireMatchingFile = false) {
|
|
91
100
|
if (!resumeKey || !this.store)
|
|
92
101
|
return undefined;
|
|
93
102
|
const record = await this.store.get(resumeKey);
|
|
94
103
|
if (!record)
|
|
95
104
|
return undefined;
|
|
105
|
+
if (record.fingerprint !== fingerprint && requireMatchingFile) {
|
|
106
|
+
throw new Error('Reselect the original file to resume this upload, or cancel the saved upload first');
|
|
107
|
+
}
|
|
108
|
+
if (record.fingerprint === fingerprint && record.session.completionPending) {
|
|
109
|
+
if (Date.parse(record.session.expiresAt) <= Date.now()) {
|
|
110
|
+
throw new Error('Completion is uncertain and its recovery window expired; verify the destination before clearing saved state');
|
|
111
|
+
}
|
|
112
|
+
return record.session;
|
|
113
|
+
}
|
|
96
114
|
if (record.fingerprint !== fingerprint || Date.parse(record.session.expiresAt) <= Date.now()) {
|
|
97
115
|
await this.transport.cancel(record.session).catch(() => undefined);
|
|
98
116
|
await this.store.delete(resumeKey);
|
|
@@ -117,7 +135,7 @@ export class ChunkedUploadController {
|
|
|
117
135
|
session: { ...session, parts: session.parts.map((part) => ({ ...part })) },
|
|
118
136
|
updatedAt: new Date().toISOString(),
|
|
119
137
|
};
|
|
120
|
-
this.persistence = this.persistence.then(() => this.store.set(record));
|
|
138
|
+
this.persistence = this.persistence.catch(() => undefined).then(() => this.store.set(record));
|
|
121
139
|
return this.persistence;
|
|
122
140
|
}
|
|
123
141
|
}
|
|
@@ -154,9 +172,10 @@ export async function sha256Blob(blob) {
|
|
|
154
172
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
155
173
|
}
|
|
156
174
|
function validateSession(session, size) {
|
|
157
|
-
if (!session.id || !session.uploadToken || session.partSize <= 0
|
|
175
|
+
if (!session.id || !session.uploadToken || !Number.isSafeInteger(session.partSize) || session.partSize <= 0 ||
|
|
176
|
+
!Number.isSafeInteger(session.maxBytes) || session.maxBytes < 0 || !Array.isArray(session.parts))
|
|
158
177
|
throw new Error('Upload session is incomplete');
|
|
159
|
-
if (Date.parse(session.expiresAt) <= Date.now())
|
|
178
|
+
if (!Number.isFinite(Date.parse(session.expiresAt)) || Date.parse(session.expiresAt) <= Date.now())
|
|
160
179
|
throw new Error('Upload session has expired');
|
|
161
180
|
if (size > session.maxBytes)
|
|
162
181
|
throw new Error(`File exceeds upload limit of ${session.maxBytes} bytes`);
|
|
@@ -167,7 +186,7 @@ function expectedPartSize(fileSize, partSize, partNumber) {
|
|
|
167
186
|
return -1;
|
|
168
187
|
return Math.min(partSize, fileSize - offset);
|
|
169
188
|
}
|
|
170
|
-
async function verifiedCompletedParts(file, session) {
|
|
189
|
+
async function verifiedCompletedParts(file, session, requireMatchingFile = false) {
|
|
171
190
|
const completed = new Map();
|
|
172
191
|
for (const part of session.parts) {
|
|
173
192
|
const expectedSize = expectedPartSize(file.size, session.partSize, part.partNumber);
|
|
@@ -178,6 +197,9 @@ async function verifiedCompletedParts(file, session) {
|
|
|
178
197
|
if (checksum.toLowerCase() === part.checksumSHA256.toLowerCase()) {
|
|
179
198
|
completed.set(part.partNumber, part.size);
|
|
180
199
|
}
|
|
200
|
+
else if (requireMatchingFile) {
|
|
201
|
+
throw new Error('The selected file differs from an uploaded part; reselect the original file or cancel this upload');
|
|
202
|
+
}
|
|
181
203
|
}
|
|
182
204
|
return completed;
|
|
183
205
|
}
|
|
@@ -195,6 +217,7 @@ function boundedInteger(value, fallback, minimum, maximum) {
|
|
|
195
217
|
async function retryWithBackoff(operation, attempts, signal) {
|
|
196
218
|
let lastError;
|
|
197
219
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
220
|
+
throwIfAborted(signal);
|
|
198
221
|
try {
|
|
199
222
|
return await operation();
|
|
200
223
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackshift-cloud/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Official JavaScript and TypeScript SDK for StackShift.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc -p tsconfig.json",
|
|
20
|
-
"test": "npm run build && node test/mail.test.mjs && node test/mail-simulation.test.mjs && node test/mail-platform.test.mjs && node test/mail-webhooks.test.mjs && node test/mail-advanced.test.mjs && node test/mail-tracking.test.mjs && node test/mail-subscribers.test.mjs && node test/mail-exports.test.mjs && node test/mail-diagnostics.test.mjs && node test/assets.test.mjs && node test/assets-platform.test.mjs && node test/assets-video.test.mjs && node test/assets-dam.test.mjs && node test/ai-credits.test.mjs"
|
|
20
|
+
"test": "npm run build && node test/mail.test.mjs && node test/mail-simulation.test.mjs && node test/mail-platform.test.mjs && node test/mail-webhooks.test.mjs && node test/mail-advanced.test.mjs && node test/mail-tracking.test.mjs && node test/mail-subscribers.test.mjs && node test/mail-exports.test.mjs && node test/mail-mailboxes.test.mjs && node test/mail-diagnostics.test.mjs && node test/assets.test.mjs && node test/assets-platform.test.mjs && node test/assets-video.test.mjs && node test/assets-dam.test.mjs && node test/ai-credits.test.mjs && node --test test/s2-upload.test.mjs",
|
|
21
|
+
"test:support-actions": "npm run build && node --test tests/support-actions.test.mjs"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"typescript": "^5.4.0"
|