@solana-mobile/dapp-store-cli 0.13.0 → 0.14.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/lib/CliSetup.js +7 -1
- package/lib/CliUtils.js +37 -9
- package/lib/__tests__/CliSetupTest.js +3 -3
- package/lib/commands/create/index.js +1 -1
- package/lib/generated/config_obj.json +1 -1
- package/lib/generated/config_schema.json +1 -1
- package/lib/package.json +4 -2
- package/lib/prebuild_schema/publishing_source.yaml +1 -0
- package/lib/prebuild_schema/schemagen.js +5 -0
- package/lib/upload/TurboStorageDriver.js +696 -0
- package/lib/upload/index.js +1 -0
- package/package.json +4 -2
- package/src/CliSetup.ts +10 -0
- package/src/CliUtils.ts +20 -16
- package/src/__tests__/CliSetupTest.ts +7 -4
- package/src/commands/create/index.ts +1 -1
- package/src/prebuild_schema/publishing_source.yaml +1 -0
- package/src/prebuild_schema/schemagen.js +4 -0
- package/src/upload/TurboStorageDriver.ts +248 -0
- package/src/upload/index.ts +1 -0
package/lib/upload/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana-mobile/dapp-store-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -44,13 +44,15 @@
|
|
|
44
44
|
"ts-node": "^10.9.1"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@ardrive/turbo-sdk": "^1.31.1",
|
|
47
48
|
"@aws-sdk/client-s3": "^3.321.1",
|
|
48
49
|
"@metaplex-foundation/js-plugin-aws": "^0.20.0",
|
|
49
|
-
"@solana-mobile/dapp-store-publishing-tools": "0.
|
|
50
|
+
"@solana-mobile/dapp-store-publishing-tools": "0.14.0",
|
|
50
51
|
"@solana/web3.js": "1.92.1",
|
|
51
52
|
"@types/semver": "^7.3.13",
|
|
52
53
|
"ajv": "^8.11.0",
|
|
53
54
|
"boxen": "^7.0.1",
|
|
55
|
+
"bs58": "^5.0.0",
|
|
54
56
|
"chokidar": "^3.5.3",
|
|
55
57
|
"commander": "^9.4.1",
|
|
56
58
|
"debug": "^4.3.4",
|
package/src/CliSetup.ts
CHANGED
|
@@ -52,6 +52,7 @@ function latestReleaseMessage() {
|
|
|
52
52
|
const messages = [
|
|
53
53
|
`- Banner Graphic image of size 1200x600px is now manadatory for publishing updates.`,
|
|
54
54
|
`- Feature Graphic image of size 1200x1200px is required to be featured in Editor's choice carousel. (optional)`,
|
|
55
|
+
`- Release metadata now publishes publisher.support_email when provided; otherwise we reuse publisher.email for end-user support.`,
|
|
55
56
|
].join('\n\n')
|
|
56
57
|
showMessage(
|
|
57
58
|
`Publishing Tools Version ${ Constants.CLI_VERSION }`,
|
|
@@ -91,6 +92,15 @@ export const createCliCmd = mainCli
|
|
|
91
92
|
.command("create")
|
|
92
93
|
.description("Create a `app`, or `release`")
|
|
93
94
|
|
|
95
|
+
createCliCmd.addHelpText(
|
|
96
|
+
"after",
|
|
97
|
+
[
|
|
98
|
+
"",
|
|
99
|
+
"Release metadata notes:",
|
|
100
|
+
" We include publisher.support_email when provided; if omitted we fall back to publisher.email.",
|
|
101
|
+
].join("\n")
|
|
102
|
+
);
|
|
103
|
+
|
|
94
104
|
export const createAppCliCmd = createCliCmd
|
|
95
105
|
.command("app")
|
|
96
106
|
.description("Create a app")
|
package/src/CliUtils.ts
CHANGED
|
@@ -2,11 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import type { Connection } from "@solana/web3.js";
|
|
3
3
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
|
4
4
|
import debugModule from "debug";
|
|
5
|
-
import {
|
|
6
|
-
IrysStorageDriver,
|
|
7
|
-
keypairIdentity,
|
|
8
|
-
Metaplex,
|
|
9
|
-
} from "@metaplex-foundation/js";
|
|
5
|
+
import { keypairIdentity, Metaplex, type MetaplexFile, type Amount, lamports } from "@metaplex-foundation/js";
|
|
10
6
|
import updateNotifier from "update-notifier";
|
|
11
7
|
import { readFile } from 'fs/promises';
|
|
12
8
|
const cliPackage = JSON.parse((await readFile(new URL("./package.json", import.meta.url))).toString());
|
|
@@ -14,13 +10,14 @@ import boxen from "boxen";
|
|
|
14
10
|
import ver from "semver";
|
|
15
11
|
import path from "path";
|
|
16
12
|
import { CachedStorageDriver } from "./upload/CachedStorageDriver.js";
|
|
13
|
+
import { TurboStorageDriver } from "./upload/TurboStorageDriver.js";
|
|
17
14
|
import { EnvVariables } from "./config/index.js";
|
|
18
15
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
19
16
|
import { awsStorage } from "@metaplex-foundation/js-plugin-aws";
|
|
20
17
|
import { S3StorageManager } from "./config/index.js";
|
|
21
18
|
|
|
22
19
|
export class Constants {
|
|
23
|
-
static CLI_VERSION = "0.
|
|
20
|
+
static CLI_VERSION = "0.14.0";
|
|
24
21
|
static CONFIG_FILE_NAME = "config.yaml";
|
|
25
22
|
static DEFAULT_RPC_DEVNET = "https://api.devnet.solana.com";
|
|
26
23
|
static DEFAULT_PRIORITY_FEE = 500000;
|
|
@@ -205,16 +202,23 @@ export const getMetaplexInstance = (
|
|
|
205
202
|
const bucketPlugin = awsStorage(awsClient, s3Mgr.s3Config.bucketName);
|
|
206
203
|
metaplex.use(bucketPlugin);
|
|
207
204
|
} else {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
205
|
+
const turboDriver = new TurboStorageDriver(
|
|
206
|
+
keypair,
|
|
207
|
+
isDevnet ? "devnet" : "mainnet",
|
|
208
|
+
Number(process.env.TURBO_BUFFER_PERCENTAGE || 20)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const metaplexAdapter = {
|
|
212
|
+
async upload(file: MetaplexFile): Promise<string> {
|
|
213
|
+
return turboDriver.upload(file);
|
|
214
|
+
},
|
|
215
|
+
async getUploadPrice(bytes: number): Promise<Amount> {
|
|
216
|
+
const price = await turboDriver.getUploadPrice(bytes);
|
|
217
|
+
return lamports(price);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
metaplex.storage().setDriver(metaplexAdapter);
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
metaplex.storage().setDriver(
|
|
@@ -24,11 +24,11 @@ describe("Cli Setup & Execution", () => {
|
|
|
24
24
|
getErrHelpWidth(): number { return 250;},
|
|
25
25
|
|
|
26
26
|
writeOut(str: string) {
|
|
27
|
-
otherOutput
|
|
27
|
+
otherOutput += str;
|
|
28
28
|
},
|
|
29
29
|
|
|
30
30
|
writeErr(str: string) {
|
|
31
|
-
errorOutput
|
|
31
|
+
errorOutput += str;
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
});
|
|
@@ -136,7 +136,7 @@ Options:
|
|
|
136
136
|
|
|
137
137
|
const keyPairArgHelp = "error: required option '-k, --keypair <path-to-keypair-file>' not specified"
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
const createHelp = `Usage: dapp-store create [options] [command]
|
|
140
140
|
|
|
141
141
|
Create a \`app\`, or \`release\`
|
|
142
142
|
|
|
@@ -147,6 +147,9 @@ Commands:
|
|
|
147
147
|
app [options] Create a app
|
|
148
148
|
release [options] Create a release
|
|
149
149
|
help [command] display help for command
|
|
150
|
+
|
|
151
|
+
Release metadata notes:
|
|
152
|
+
We include publisher.support_email when provided; if omitted we fall back to publisher.email.
|
|
150
153
|
`;
|
|
151
154
|
|
|
152
155
|
const createAppHelp = `Usage: dapp-store create app [options]
|
|
@@ -177,4 +180,4 @@ Options:
|
|
|
177
180
|
-h, --help display help for command
|
|
178
181
|
`;
|
|
179
182
|
|
|
180
|
-
});
|
|
183
|
+
});
|
|
@@ -15,7 +15,7 @@ export * from "./CreateCliRelease.js";
|
|
|
15
15
|
// Publisher
|
|
16
16
|
// Public key attached to a publisher must also verify applications and releases
|
|
17
17
|
// Most information here can be be edited after the fact
|
|
18
|
-
// Only required fields are name, address, publisher website, and contact
|
|
18
|
+
// Only required fields are name, address, publisher website, and contact email
|
|
19
19
|
// Optional fields are: description, image_url (need dimensions!)
|
|
20
20
|
|
|
21
21
|
// App
|
|
@@ -2,6 +2,7 @@ publisher:
|
|
|
2
2
|
name: <<[REQUIRED] YOUR_PUBLISHER_NAME>>
|
|
3
3
|
website: <<[REQUIRED] URL_OF_PUBLISHER_WEBSITE>>
|
|
4
4
|
email: <<[REQUIRED] EMAIL_ADDRESS_TO_CONTACT_PUBLISHER>>
|
|
5
|
+
support_email: <<[Optional] SUPPORT_EMAIL_ADDRESS_FOR_END_USERS>>
|
|
5
6
|
app:
|
|
6
7
|
name: <<[REQUIRED] APP_NAME>>
|
|
7
8
|
address: ""
|
|
@@ -16,6 +16,10 @@ try {
|
|
|
16
16
|
["properties"]
|
|
17
17
|
["en-US"].required = ["short_description"];
|
|
18
18
|
|
|
19
|
+
schema["properties"]
|
|
20
|
+
["publisher"]
|
|
21
|
+
.required = ["name", "website", "email"];
|
|
22
|
+
|
|
19
23
|
// Generator adds some keys/values we don't need & mess up validation
|
|
20
24
|
delete schema.$schema;
|
|
21
25
|
delete schema.title;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { Keypair } from "@solana/web3.js";
|
|
2
|
+
import type { MetaplexFile } from "@metaplex-foundation/js";
|
|
3
|
+
import { TurboFactory, lamportToTokenAmount } from "@ardrive/turbo-sdk";
|
|
4
|
+
import bs58 from "bs58";
|
|
5
|
+
import debugModule from "debug";
|
|
6
|
+
|
|
7
|
+
const debug = debugModule("cli:turbo-storage");
|
|
8
|
+
|
|
9
|
+
interface TurboClient {
|
|
10
|
+
getUploadCosts(args: {
|
|
11
|
+
bytes: number[];
|
|
12
|
+
}): Promise<Array<{ winc: string | number | bigint }>>;
|
|
13
|
+
getBalance(): Promise<{ winc: string | number | bigint }>;
|
|
14
|
+
getWincForToken?(args: {
|
|
15
|
+
tokenAmount: number;
|
|
16
|
+
}): Promise<{ winc: string | number | bigint }>;
|
|
17
|
+
topUpWithTokens?(args: { tokenAmount: string | number }): Promise<unknown>;
|
|
18
|
+
uploadFile(args: {
|
|
19
|
+
fileStreamFactory: () => Buffer;
|
|
20
|
+
fileSizeFactory: () => number;
|
|
21
|
+
dataItemOpts?: { tags?: Array<{ name: string; value: string }> };
|
|
22
|
+
}): Promise<{ id: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CONSTANTS = {
|
|
26
|
+
FREE_UPLOAD_LIMIT: 97_280, // 95 KiB
|
|
27
|
+
UPLOAD_DELAY_MS: 2000,
|
|
28
|
+
MAX_RETRIES: 5,
|
|
29
|
+
SOL_IN_LAMPORTS: 1_000_000_000,
|
|
30
|
+
BACKOFF: {
|
|
31
|
+
BASE_MS: 500,
|
|
32
|
+
MAX_MS: 8000,
|
|
33
|
+
},
|
|
34
|
+
GATEWAYS: {
|
|
35
|
+
devnet: "https://turbo.ardrive.dev/raw",
|
|
36
|
+
mainnet: "https://arweave.net",
|
|
37
|
+
},
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const delay = (ms: number): Promise<void> =>
|
|
41
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
|
|
43
|
+
export class TurboStorageDriver {
|
|
44
|
+
private turbo: TurboClient;
|
|
45
|
+
private bufferPercentage: number;
|
|
46
|
+
private network: "devnet" | "mainnet";
|
|
47
|
+
|
|
48
|
+
private uploadQueue: Array<{
|
|
49
|
+
file: MetaplexFile;
|
|
50
|
+
resolve: (url: string) => void;
|
|
51
|
+
reject: (error: Error) => void;
|
|
52
|
+
}> = [];
|
|
53
|
+
private isProcessingQueue = false;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
keypair: Keypair,
|
|
57
|
+
network: "devnet" | "mainnet" = "mainnet",
|
|
58
|
+
bufferPercentage = 20
|
|
59
|
+
) {
|
|
60
|
+
this.network = network;
|
|
61
|
+
this.bufferPercentage = bufferPercentage;
|
|
62
|
+
|
|
63
|
+
this.turbo = TurboFactory.authenticated({
|
|
64
|
+
privateKey: bs58.encode(keypair.secretKey),
|
|
65
|
+
token: "solana",
|
|
66
|
+
...this.getServiceUrls(network === "devnet"),
|
|
67
|
+
}) as TurboClient;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private getServiceUrls(isDev: boolean) {
|
|
71
|
+
const base = isDev ? "ardrive.dev" : "ardrive.io";
|
|
72
|
+
return {
|
|
73
|
+
uploadUrl: `https://upload.${base}`,
|
|
74
|
+
paymentUrl: `https://payment.${base}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getUploadPrice(bytes: number): Promise<bigint> {
|
|
79
|
+
if (bytes <= CONSTANTS.FREE_UPLOAD_LIMIT) return BigInt(0);
|
|
80
|
+
|
|
81
|
+
const [cost] = await this.turbo.getUploadCosts({ bytes: [bytes] });
|
|
82
|
+
const base = BigInt(String(cost.winc));
|
|
83
|
+
return (base * BigInt(100 + this.bufferPercentage)) / BigInt(100);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async withRetry<T>(
|
|
87
|
+
operation: () => Promise<T>,
|
|
88
|
+
isRetriable: (error: string) => boolean = (msg) =>
|
|
89
|
+
msg.includes("429") || msg.includes("Too Many Requests")
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
for (let retry = 0; retry <= CONSTANTS.MAX_RETRIES; retry++) {
|
|
92
|
+
try {
|
|
93
|
+
return await operation();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const errorMessage =
|
|
96
|
+
error instanceof Error ? error.message : String(error);
|
|
97
|
+
|
|
98
|
+
if (retry < CONSTANTS.MAX_RETRIES && isRetriable(errorMessage)) {
|
|
99
|
+
const delayMs = Math.min(
|
|
100
|
+
CONSTANTS.BACKOFF.BASE_MS * Math.pow(2, retry),
|
|
101
|
+
CONSTANTS.BACKOFF.MAX_MS
|
|
102
|
+
);
|
|
103
|
+
console.log(
|
|
104
|
+
`Rate limited, retrying after ${delayMs}ms (attempt ${retry + 1}/${
|
|
105
|
+
CONSTANTS.MAX_RETRIES
|
|
106
|
+
})...`
|
|
107
|
+
);
|
|
108
|
+
await delay(delayMs);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new Error("Max retries exceeded");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private formatInsufficientFundsError(errorMessage: string): void {
|
|
118
|
+
const match = errorMessage.match(/insufficient lamports (\d+), need (\d+)/);
|
|
119
|
+
if (!match) return;
|
|
120
|
+
|
|
121
|
+
const [current, needed] = [BigInt(match[1]), BigInt(match[2])];
|
|
122
|
+
const [currentSOL, neededSOL] = [
|
|
123
|
+
Number(current) / 1e9,
|
|
124
|
+
Number(needed) / 1e9,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
console.error(`\nInsufficient SOL balance for top-up:`);
|
|
128
|
+
console.error(` Current: ${currentSOL.toFixed(9)} SOL`);
|
|
129
|
+
console.error(` Required: ${neededSOL.toFixed(9)} SOL`);
|
|
130
|
+
console.error(` Shortfall: ${(neededSOL - currentSOL).toFixed(9)} SOL\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async topUpCredits(wincAmount: bigint): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
await this.withRetry(async () => {
|
|
136
|
+
const exchangeRate = await this.turbo.getWincForToken?.({
|
|
137
|
+
tokenAmount: CONSTANTS.SOL_IN_LAMPORTS,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!exchangeRate) {
|
|
141
|
+
throw new Error("Unable to get Winston Credits exchange rate");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const wincPerSol = BigInt(String(exchangeRate.winc));
|
|
145
|
+
const lamportsNeeded =
|
|
146
|
+
(wincAmount * BigInt(CONSTANTS.SOL_IN_LAMPORTS)) / wincPerSol;
|
|
147
|
+
|
|
148
|
+
debug(
|
|
149
|
+
`Buying ${wincAmount} Winston Credits for ~${
|
|
150
|
+
Number(lamportsNeeded) / 1e9
|
|
151
|
+
} SOL`
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await this.turbo.topUpWithTokens?.({
|
|
155
|
+
tokenAmount: String(lamportToTokenAmount(lamportsNeeded.toString())),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
debug(`Top-up initiated for ${wincAmount} Winston Credits`);
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMessage =
|
|
162
|
+
error instanceof Error ? error.message : String(error);
|
|
163
|
+
debug("Top-up failed:", error);
|
|
164
|
+
|
|
165
|
+
if (errorMessage.includes("insufficient lamports")) {
|
|
166
|
+
this.formatInsufficientFundsError(errorMessage);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Failed to top up ${wincAmount} Winston Credits: ${errorMessage}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async checkBalanceAndTopUp(requiredWinc: bigint): Promise<void> {
|
|
176
|
+
if (requiredWinc === BigInt(0)) return;
|
|
177
|
+
|
|
178
|
+
const current = BigInt(String((await this.turbo.getBalance()).winc));
|
|
179
|
+
|
|
180
|
+
if (current >= requiredWinc) {
|
|
181
|
+
debug(
|
|
182
|
+
`Sufficient balance: ${current} Winston Credits (required: ${requiredWinc})`
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const deficit = requiredWinc - current;
|
|
188
|
+
debug(
|
|
189
|
+
`Current: ${current}, Required: ${requiredWinc}, Topping up: ${deficit}`
|
|
190
|
+
);
|
|
191
|
+
await this.topUpCredits(deficit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async processQueue(): Promise<void> {
|
|
195
|
+
if (this.isProcessingQueue || !this.uploadQueue.length) return;
|
|
196
|
+
|
|
197
|
+
this.isProcessingQueue = true;
|
|
198
|
+
|
|
199
|
+
while (this.uploadQueue.length > 0) {
|
|
200
|
+
const item = this.uploadQueue.shift();
|
|
201
|
+
if (!item) continue;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
debug(
|
|
205
|
+
`Processing upload for ${item.file.fileName} (${item.file.buffer.length} bytes)`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const estimated = await this.getUploadPrice(item.file.buffer.length);
|
|
209
|
+
await this.checkBalanceAndTopUp(estimated);
|
|
210
|
+
|
|
211
|
+
const tags = [...(item.file.tags ?? [])];
|
|
212
|
+
if (item.file.contentType) {
|
|
213
|
+
tags.push({ name: "Content-Type", value: item.file.contentType });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const uploadResult = await this.turbo.uploadFile({
|
|
217
|
+
fileStreamFactory: () => item.file.buffer,
|
|
218
|
+
fileSizeFactory: () => item.file.buffer.byteLength,
|
|
219
|
+
dataItemOpts: { tags },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const gateway = CONSTANTS.GATEWAYS[this.network];
|
|
223
|
+
const url = `${gateway}/${uploadResult.id}`;
|
|
224
|
+
debug(`Upload complete: ${url}`);
|
|
225
|
+
item.resolve(url);
|
|
226
|
+
|
|
227
|
+
if (this.uploadQueue.length > 0) {
|
|
228
|
+
debug(`Waiting ${CONSTANTS.UPLOAD_DELAY_MS}ms before next upload...`);
|
|
229
|
+
await delay(CONSTANTS.UPLOAD_DELAY_MS);
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
item.reject(error instanceof Error ? error : new Error(String(error)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.isProcessingQueue = false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async upload(file: MetaplexFile): Promise<string> {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
debug(
|
|
242
|
+
`Queueing upload for ${file.fileName} (${file.buffer.length} bytes)`
|
|
243
|
+
);
|
|
244
|
+
this.uploadQueue.push({ file, resolve, reject });
|
|
245
|
+
this.processQueue().catch(reject);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
package/src/upload/index.ts
CHANGED