@ardrive/turbo-sdk 1.42.0-alpha.13 → 1.42.0-alpha.14
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/cjs/common/folderIndex.js +394 -0
- package/lib/cjs/common/index.js +1 -0
- package/lib/cjs/common/upload.js +267 -17
- package/lib/cjs/node/folderIndex.js +132 -0
- package/lib/cjs/node/index.js +1 -0
- package/lib/cjs/node/upload.js +7 -0
- package/lib/cjs/utils/folderIndex.js +88 -0
- package/lib/esm/common/folderIndex.js +389 -0
- package/lib/esm/common/index.js +1 -0
- package/lib/esm/common/upload.js +268 -18
- package/lib/esm/node/folderIndex.js +129 -0
- package/lib/esm/node/index.js +1 -0
- package/lib/esm/node/upload.js +7 -0
- package/lib/esm/utils/folderIndex.js +81 -0
- package/lib/types/common/folderIndex.d.ts +54 -0
- package/lib/types/common/folderIndex.d.ts.map +1 -0
- package/lib/types/common/index.d.ts +1 -0
- package/lib/types/common/index.d.ts.map +1 -1
- package/lib/types/common/upload.d.ts +41 -0
- package/lib/types/common/upload.d.ts.map +1 -1
- package/lib/types/node/folderIndex.d.ts +28 -0
- package/lib/types/node/folderIndex.d.ts.map +1 -0
- package/lib/types/node/index.d.ts +1 -0
- package/lib/types/node/index.d.ts.map +1 -1
- package/lib/types/node/upload.d.ts +1 -0
- package/lib/types/node/upload.d.ts.map +1 -1
- package/lib/types/types.d.ts +163 -0
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/utils/folderIndex.d.ts +47 -0
- package/lib/types/utils/folderIndex.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMemoryFolderIndex = createMemoryFolderIndex;
|
|
4
|
+
exports.createChainFolderIndex = createChainFolderIndex;
|
|
5
|
+
exports.composeFolderIndex = composeFolderIndex;
|
|
6
|
+
const base64_js_1 = require("../utils/base64.js");
|
|
7
|
+
const common_js_1 = require("../utils/common.js");
|
|
8
|
+
const errors_js_1 = require("../utils/errors.js");
|
|
9
|
+
const folderIndex_js_1 = require("../utils/folderIndex.js");
|
|
10
|
+
/**
|
|
11
|
+
* The base64url address a gateway indexes an upload under.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately refuses to guess. A gateway matches `owners:` on the base64url
|
|
14
|
+
* sha-256 of the signer's public key, and a raw 32 byte ed25519 public key
|
|
15
|
+
* base64urls to exactly 43 characters -- indistinguishable from that address.
|
|
16
|
+
* Sniffing the shape of a bare string therefore has a silent failure mode that
|
|
17
|
+
* costs real money: the unhashed key matches nothing, every file misses, and
|
|
18
|
+
* the whole folder is re-uploaded with no error anywhere. So the caller says
|
|
19
|
+
* which one they have, and `getPublicKey()` bytes are taken directly.
|
|
20
|
+
*/
|
|
21
|
+
function resolveOwnerAddress(owner) {
|
|
22
|
+
const fromPublicKey = (publicKey) => {
|
|
23
|
+
if (typeof publicKey === 'string' && !/^[a-zA-Z0-9_-]+$/.test(publicKey)) {
|
|
24
|
+
throw new errors_js_1.ProvidedInputError('createChainFolderIndex owner.publicKey must be base64url, or the raw bytes from await turbo.signer.getPublicKey()');
|
|
25
|
+
}
|
|
26
|
+
const raw = typeof publicKey === 'string'
|
|
27
|
+
? (0, base64_js_1.fromB64Url)(publicKey)
|
|
28
|
+
: Buffer.from(publicKey);
|
|
29
|
+
// Declaring which one you have is not the same as having it. Anything gets
|
|
30
|
+
// hashed to a well formed address that simply matches nothing, which is the
|
|
31
|
+
// same silent full price re-upload the tagged union exists to prevent, one
|
|
32
|
+
// level in. These are the key sizes this SDK's signers produce.
|
|
33
|
+
if (!publicKeyByteLengths.has(raw.length)) {
|
|
34
|
+
throw new errors_js_1.ProvidedInputError(`createChainFolderIndex owner.publicKey must be an ed25519 (32 byte), secp256k1 (65 byte) or RSA (512 byte) public key, got ${raw.length} bytes. Pass await turbo.signer.getPublicKey(), or use { address } if what you have is an owner address.`);
|
|
35
|
+
}
|
|
36
|
+
return (0, base64_js_1.ownerToAddress)((0, base64_js_1.toB64Url)(raw));
|
|
37
|
+
};
|
|
38
|
+
if (owner instanceof Uint8Array) {
|
|
39
|
+
return fromPublicKey(owner);
|
|
40
|
+
}
|
|
41
|
+
if (owner !== null && typeof owner === 'object') {
|
|
42
|
+
if (owner.publicKey !== undefined) {
|
|
43
|
+
return fromPublicKey(owner.publicKey);
|
|
44
|
+
}
|
|
45
|
+
if (typeof owner.address === 'string') {
|
|
46
|
+
if (!(0, common_js_1.isValidArweaveBase64URL)(owner.address)) {
|
|
47
|
+
throw new errors_js_1.ProvidedInputError(`createChainFolderIndex owner.address must be a 43 character base64url address, got '${owner.address}'. A native address (an 0x... or a base58 Solana address) is not what a gateway indexes uploads under.`);
|
|
48
|
+
}
|
|
49
|
+
return owner.address;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new errors_js_1.ProvidedInputError('createChainFolderIndex needs owner: await turbo.signer.getPublicKey(), or { publicKey } or { address }. ' +
|
|
53
|
+
'A bare string is ambiguous -- a 32 byte ed25519 public key and an owner address are both 43 base64url characters -- ' +
|
|
54
|
+
'and guessing wrong re-uploads the whole folder without an error.');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What `getPublicKey()` returns across the supported signers: ed25519 for
|
|
58
|
+
* Solana and ario, uncompressed secp256k1 for Ethereum, Base, Polygon and KYVE,
|
|
59
|
+
* and a 4096 bit RSA modulus for Arweave.
|
|
60
|
+
*
|
|
61
|
+
* A 32 byte value declared as a `publicKey` is still taken at its word, since
|
|
62
|
+
* an owner address is also 32 bytes and nothing can tell them apart -- that is
|
|
63
|
+
* why the caller has to say which one they mean.
|
|
64
|
+
*/
|
|
65
|
+
const publicKeyByteLengths = new Set([32, 65, 512]);
|
|
66
|
+
function requirePositiveInteger(value, name, max) {
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
// `1e21` is an integer to Number.isInteger and stringifies as "1e+21", which
|
|
69
|
+
// is not a GraphQL Int. An upper bound settles both that and a nonsense page.
|
|
70
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > max) {
|
|
71
|
+
throw new errors_js_1.ProvidedInputError(`createChainFolderIndex ${name} must be an integer between 1 and ${max}, got '${value}'`);
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One signal that aborts on whichever comes first, the caller giving up or the
|
|
77
|
+
* request timing out. `AbortSignal.any` is not available on every supported
|
|
78
|
+
* runtime, so this is wired by hand.
|
|
79
|
+
*/
|
|
80
|
+
function abortAfter(timeoutMs, signal) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
83
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
84
|
+
if (signal !== undefined) {
|
|
85
|
+
if (signal.aborted) {
|
|
86
|
+
controller.abort(signal.reason);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
done: () => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
signal?.removeEventListener('abort', onAbort);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const contentHashesOf = (keys) => new Set([...keys].map(folderIndex_js_1.contentHashFromFolderIndexKey));
|
|
101
|
+
/**
|
|
102
|
+
* An in-memory folder index.
|
|
103
|
+
*
|
|
104
|
+
* On its own it only deduplicates identical files within a single
|
|
105
|
+
* `uploadFolder` call, which `uploadFolder` already does. Its real use is as
|
|
106
|
+
* the writable floor of a {@link composeFolderIndex} stack, or seeded from a
|
|
107
|
+
* mapping the caller persisted itself.
|
|
108
|
+
*/
|
|
109
|
+
function createMemoryFolderIndex(seed = {}) {
|
|
110
|
+
const map = new Map(Object.entries(seed).filter(([key, id]) => (0, folderIndex_js_1.isValidFolderIndexKey)(key) && (0, common_js_1.isValidArweaveBase64URL)(id)));
|
|
111
|
+
return {
|
|
112
|
+
name: 'memory',
|
|
113
|
+
get: (key) => map.get(key),
|
|
114
|
+
set: (key, id) => {
|
|
115
|
+
map.set(key, id);
|
|
116
|
+
},
|
|
117
|
+
knownContentHashes: (contentHashes) => {
|
|
118
|
+
const known = contentHashesOf(map.keys());
|
|
119
|
+
return contentHashes.filter((contentHash) => known.has(contentHash));
|
|
120
|
+
},
|
|
121
|
+
entries: () => Object.fromEntries(map),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Rebuilds a folder index by sweeping the uploader's own past uploads over a
|
|
126
|
+
* gateway's GraphQL endpoint, filtering on the `File-SHA256` tag that
|
|
127
|
+
* index-backed uploads always write.
|
|
128
|
+
*
|
|
129
|
+
* Read only, and the only layer that survives a fresh checkout on a machine
|
|
130
|
+
* that has never deployed before -- a CI runner, most obviously.
|
|
131
|
+
*
|
|
132
|
+
* Known limitation: gateways index an upload minutes after it lands, so two
|
|
133
|
+
* machines deploying the same new file at the same moment will each pay for it
|
|
134
|
+
* once. The manifest is correct either way; only the bill is affected, and only
|
|
135
|
+
* for genuinely new bytes.
|
|
136
|
+
*/
|
|
137
|
+
function createChainFolderIndex({ owner, appName, gatewayUrl = 'https://arweave.net', hashTagName = folderIndex_js_1.contentHashTagName, maxPages = 20, pageSize = 100, timeoutMs = 30_000, fetchImpl = fetch, logger, }) {
|
|
138
|
+
const ownerAddress = resolveOwnerAddress(owner);
|
|
139
|
+
const first = requirePositiveInteger(pageSize, 'pageSize', 1000);
|
|
140
|
+
const pageLimit = requirePositiveInteger(maxPages, 'maxPages', 10_000);
|
|
141
|
+
const requestTimeoutMs = requirePositiveInteger(timeoutMs, 'timeoutMs', 24 * 60 * 60 * 1000);
|
|
142
|
+
const map = new Map();
|
|
143
|
+
// Every content hash a sweep has actually seen on chain, whatever tags the
|
|
144
|
+
// item carried. The bytes-only GraphQL filter hands this over for free, and
|
|
145
|
+
// it is the only evidence anywhere that a file's content is already paid for
|
|
146
|
+
// under a different tag set.
|
|
147
|
+
const seenContentHashes = new Set();
|
|
148
|
+
// Filtering on the hash tag itself keeps the sweep to items this run cares
|
|
149
|
+
// about, so a long deployment history costs nothing to walk past.
|
|
150
|
+
const tagFilter = appName !== undefined
|
|
151
|
+
? `tags:[{name:"App-Name",values:[${JSON.stringify(appName)}]},{name:${JSON.stringify(hashTagName)},values:$hashes}]`
|
|
152
|
+
: `tags:[{name:${JSON.stringify(hashTagName)},values:$hashes}]`;
|
|
153
|
+
const query = `query($owner:String!,$hashes:[String!]!,$after:String){
|
|
154
|
+
transactions(owners:[$owner] ${tagFilter} sort:HEIGHT_DESC first:${first} after:$after){
|
|
155
|
+
pageInfo{hasNextPage}
|
|
156
|
+
edges{cursor node{id tags{name value}}}
|
|
157
|
+
}
|
|
158
|
+
}`;
|
|
159
|
+
return {
|
|
160
|
+
name: `chain:${gatewayUrl}`,
|
|
161
|
+
readOnly: true,
|
|
162
|
+
// Declared so `uploadFolder` writes the tag this sweep filters on. Without
|
|
163
|
+
// it a non-default `hashTagName` matches nothing, on every run, silently.
|
|
164
|
+
hashTagName,
|
|
165
|
+
get: (key) => map.get(key),
|
|
166
|
+
set: () => undefined,
|
|
167
|
+
knownContentHashes: (contentHashes) => contentHashes.filter((contentHash) => seenContentHashes.has(contentHash)),
|
|
168
|
+
resolve: async (keys, options) => {
|
|
169
|
+
const wanted = new Set(keys.filter(folderIndex_js_1.isValidFolderIndexKey));
|
|
170
|
+
if (wanted.size === 0) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
const hashes = [...contentHashesOf(wanted)];
|
|
174
|
+
const found = {};
|
|
175
|
+
let cursor = null;
|
|
176
|
+
let pagesWalked = 0;
|
|
177
|
+
let moreToWalk = false;
|
|
178
|
+
for (let page = 0; page < pageLimit && Object.keys(found).length < wanted.size; page++) {
|
|
179
|
+
pagesWalked++;
|
|
180
|
+
const abort = abortAfter(requestTimeoutMs, options?.signal);
|
|
181
|
+
let body;
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetchImpl(`${gatewayUrl}/graphql`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: { 'content-type': 'application/json' },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
query,
|
|
188
|
+
variables: { owner: ownerAddress, hashes, after: cursor },
|
|
189
|
+
}),
|
|
190
|
+
signal: abort.signal,
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
throw new Error(`Failed to query ${gatewayUrl}/graphql for a folder index: ${response.status}`);
|
|
194
|
+
}
|
|
195
|
+
// Read inside the same guard. Headers arriving is not the request
|
|
196
|
+
// finishing: a gateway that flushes them and then stalls the body
|
|
197
|
+
// would hang here forever with the timer already cleared and the
|
|
198
|
+
// caller's abort listener already removed, and `uploadFolder` awaits
|
|
199
|
+
// this inline.
|
|
200
|
+
body = await response.json();
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
abort.done();
|
|
204
|
+
}
|
|
205
|
+
const transactions = body?.data?.transactions;
|
|
206
|
+
// A gateway may answer with an error payload, a null field, or a shape
|
|
207
|
+
// this code has never seen, and every node in a page it does return is
|
|
208
|
+
// equally untrusted.
|
|
209
|
+
if (!Array.isArray(transactions?.edges)) {
|
|
210
|
+
throw new Error(`Failed to query ${gatewayUrl}/graphql for a folder index: ${JSON.stringify(body?.errors ?? body)}`);
|
|
211
|
+
}
|
|
212
|
+
if (transactions.edges.length === 0) {
|
|
213
|
+
// `after` only advances from an edge, so a page with none would
|
|
214
|
+
// re-issue the identical query until maxPages ran out.
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
for (const edge of transactions.edges) {
|
|
218
|
+
if (typeof edge?.cursor === 'string') {
|
|
219
|
+
cursor = edge.cursor;
|
|
220
|
+
}
|
|
221
|
+
const node = edge?.node;
|
|
222
|
+
const tags = node?.tags;
|
|
223
|
+
if (typeof node?.id !== 'string' || !Array.isArray(tags)) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const contentHash = tags.find((tag) => tag?.name === hashTagName)
|
|
227
|
+
?.value;
|
|
228
|
+
if (!(0, folderIndex_js_1.isValidContentHash)(contentHash)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Seen on chain under whatever tags this item happens to carry. A
|
|
232
|
+
// key that does not match is still worth remembering as bytes that
|
|
233
|
+
// are already paid for.
|
|
234
|
+
seenContentHashes.add(contentHash);
|
|
235
|
+
// A node carries exactly the tags that were written, so the key can
|
|
236
|
+
// be recomputed and matched against the ones this run needs.
|
|
237
|
+
const key = await (0, folderIndex_js_1.folderIndexKey)({ contentHash, tags });
|
|
238
|
+
// Newest first, and every upload of these exact bytes and tags is
|
|
239
|
+
// equally valid, so the first sighting wins.
|
|
240
|
+
if (wanted.has(key) && found[key] === undefined) {
|
|
241
|
+
found[key] = node.id;
|
|
242
|
+
map.set(key, node.id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (transactions.pageInfo?.hasNextPage !== true) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
moreToWalk = true;
|
|
249
|
+
}
|
|
250
|
+
// Running out of pages is not the same as running out of matches, and it
|
|
251
|
+
// is the one outcome that costs money without looking like anything: the
|
|
252
|
+
// unresolved files are re-uploaded at full price, on every deploy, and
|
|
253
|
+
// the summary reports them as ordinary new files. `pageSize * maxPages`
|
|
254
|
+
// caps how many items a sweep can ever see, so a folder larger than that
|
|
255
|
+
// cannot resolve in full however many times it is run.
|
|
256
|
+
const resolved = Object.keys(found).length;
|
|
257
|
+
if (pagesWalked >= pageLimit && moreToWalk && resolved < wanted.size) {
|
|
258
|
+
logger?.warn(`The folder index sweep of ${gatewayUrl} stopped at its ${pageLimit} page limit with ` +
|
|
259
|
+
`${wanted.size - resolved} of ${wanted.size} file(s) still unresolved, and the gateway ` +
|
|
260
|
+
'had more to give. Those files are about to be uploaded and paid for again even though ' +
|
|
261
|
+
`they may already be on Arweave. A sweep can see at most pageSize * maxPages items ` +
|
|
262
|
+
`(${first} * ${pageLimit} = ${first * pageLimit} here), so raise maxPages or pageSize, ` +
|
|
263
|
+
'or put a persistent local index in front of this one.');
|
|
264
|
+
}
|
|
265
|
+
return found;
|
|
266
|
+
},
|
|
267
|
+
entries: () => Object.fromEntries(map),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Layers folder indexes: reads fall through in order, writes go to every layer
|
|
272
|
+
* that is not read only.
|
|
273
|
+
*
|
|
274
|
+
* The usual stack is a local cache in front of a chain index -- the cache
|
|
275
|
+
* answers instantly on a developer machine, and the chain index is what a CI
|
|
276
|
+
* runner with an empty working directory falls back to.
|
|
277
|
+
*
|
|
278
|
+
* A layer that throws is skipped, not propagated. That is the whole point of
|
|
279
|
+
* stacking them: a full disk under the file layer must not stop the memory
|
|
280
|
+
* layer from holding ids the run has already paid for, and an unreachable
|
|
281
|
+
* gateway must not stop the local cache from answering.
|
|
282
|
+
*/
|
|
283
|
+
function composeFolderIndex(layers, { logger } = {}) {
|
|
284
|
+
const stack = layers.filter((layer) => layer !== undefined);
|
|
285
|
+
if (stack.length === 0) {
|
|
286
|
+
return createMemoryFolderIndex();
|
|
287
|
+
}
|
|
288
|
+
const writableLayers = stack.filter((layer) => layer.readOnly !== true);
|
|
289
|
+
// Every layer that cares which tag holds a content hash has to want the same
|
|
290
|
+
// one, because `uploadFolder` writes exactly one tag per file. Two layers
|
|
291
|
+
// disagreeing is not something a run can degrade around: whichever one loses
|
|
292
|
+
// matches nothing, for ever, without an error.
|
|
293
|
+
const declaredHashTagNames = [
|
|
294
|
+
...new Set(stack
|
|
295
|
+
.map((layer) => layer.hashTagName)
|
|
296
|
+
.filter((name) => name !== undefined)),
|
|
297
|
+
];
|
|
298
|
+
if (declaredHashTagNames.length > 1) {
|
|
299
|
+
throw new errors_js_1.ProvidedInputError(`composeFolderIndex layers disagree about which tag holds a file's content hash: ${declaredHashTagNames
|
|
300
|
+
.map((name) => `'${name}'`)
|
|
301
|
+
.join(', ')}. uploadFolder writes one tag per file, so every layer that declares one must declare the same one.`);
|
|
302
|
+
}
|
|
303
|
+
const failed = (layer, what, error) => logger?.error(`Folder index layer ${layer.name ?? 'anonymous'} failed to ${what}, skipping it`, error);
|
|
304
|
+
const set = async (key, id) => {
|
|
305
|
+
for (const layer of writableLayers) {
|
|
306
|
+
try {
|
|
307
|
+
await layer.set(key, id);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
failed(layer, 'write', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
name: `composed(${stack
|
|
316
|
+
.map((layer) => layer.name ?? 'anonymous')
|
|
317
|
+
.join(', ')})`,
|
|
318
|
+
readOnly: writableLayers.length === 0,
|
|
319
|
+
hashTagName: declaredHashTagNames[0],
|
|
320
|
+
get: async (key) => {
|
|
321
|
+
for (const layer of stack) {
|
|
322
|
+
try {
|
|
323
|
+
const id = await layer.get(key);
|
|
324
|
+
if ((0, common_js_1.isValidArweaveBase64URL)(id ?? '')) {
|
|
325
|
+
return id;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
failed(layer, 'read', error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
},
|
|
334
|
+
set,
|
|
335
|
+
knownContentHashes: async (contentHashes) => {
|
|
336
|
+
const known = new Set();
|
|
337
|
+
for (const layer of stack) {
|
|
338
|
+
try {
|
|
339
|
+
for (const contentHash of (await layer.knownContentHashes?.(contentHashes)) ?? []) {
|
|
340
|
+
known.add(contentHash);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
failed(layer, 'report known content hashes', error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return [...known];
|
|
348
|
+
},
|
|
349
|
+
resolve: async (keys, options) => {
|
|
350
|
+
const found = {};
|
|
351
|
+
let remaining = keys;
|
|
352
|
+
for (const layer of stack) {
|
|
353
|
+
if (remaining.length === 0) {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (layer.resolve === undefined) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const resolved = await layer.resolve(remaining, options);
|
|
361
|
+
for (const [key, id] of Object.entries(resolved)) {
|
|
362
|
+
if (!(0, common_js_1.isValidArweaveBase64URL)(id)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
found[key] = id;
|
|
366
|
+
}
|
|
367
|
+
remaining = remaining.filter((key) => found[key] === undefined);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
failed(layer, 'resolve', error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// An id recovered from a read only layer is worth caching in the writable
|
|
374
|
+
// ones so the next run can skip the network entirely.
|
|
375
|
+
for (const [key, id] of Object.entries(found)) {
|
|
376
|
+
await set(key, id);
|
|
377
|
+
}
|
|
378
|
+
return found;
|
|
379
|
+
},
|
|
380
|
+
entries: async () => {
|
|
381
|
+
const entries = {};
|
|
382
|
+
// Reverse so the front of the stack wins on conflict.
|
|
383
|
+
for (const layer of [...stack].reverse()) {
|
|
384
|
+
try {
|
|
385
|
+
Object.assign(entries, (await layer.entries?.()) ?? {});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
failed(layer, 'enumerate', error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return entries;
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
package/lib/cjs/common/index.js
CHANGED
|
@@ -30,6 +30,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
30
30
|
* limitations under the License.
|
|
31
31
|
*/
|
|
32
32
|
__exportStar(require("./upload.js"), exports);
|
|
33
|
+
__exportStar(require("./folderIndex.js"), exports);
|
|
33
34
|
__exportStar(require("./payment.js"), exports);
|
|
34
35
|
// The ANT-owner helpers. Without this the documented
|
|
35
36
|
// `import { solanaOwnerSigner } from '@ardrive/turbo-sdk'` does not resolve.
|