wao 0.3.0 → 0.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/cjs/accounts.js +12 -12
- package/cjs/ao.js +1 -1
- package/cjs/aoconnect.js +410 -1450
- package/cjs/ar.js +1 -1
- package/cjs/armem.js +52 -0
- package/cjs/gql.js +393 -0
- package/cjs/helpers.js +4 -4
- package/cjs/index.js +7 -0
- package/cjs/lua/apm.lua +61 -0
- package/cjs/tao.js +9 -9
- package/cjs/tar.js +279 -0
- package/cjs/test.js +57 -3
- package/cjs/utils.js +41 -1
- package/cjs/weavedrive.js +979 -0
- package/esm/accounts.js +36 -30
- package/esm/ao.js +1 -0
- package/esm/aoconnect.js +65 -805
- package/esm/ar.js +1 -1
- package/esm/armem.js +38 -0
- package/esm/gql.js +218 -0
- package/esm/helpers.js +1 -1
- package/esm/index.js +2 -1
- package/esm/lua/apm.lua +61 -0
- package/esm/tao.js +7 -9
- package/esm/tar.js +98 -0
- package/esm/test.js +13 -2
- package/esm/utils.js +10 -0
- package/esm/weavedrive.js +709 -0
- package/package.json +1 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import Arweave from "arweave"
|
|
2
|
+
const KB = 1024
|
|
3
|
+
const MB = KB * 1024
|
|
4
|
+
const CACHE_SZ = 32 * KB
|
|
5
|
+
const CHUNK_SZ = 128 * MB
|
|
6
|
+
const NOTIFY_SZ = 512 * MB
|
|
7
|
+
|
|
8
|
+
export default class WeaveDrive {
|
|
9
|
+
constructor(ar) {
|
|
10
|
+
this.drive = function WeaveDrive(mod, FS) {
|
|
11
|
+
return {
|
|
12
|
+
reset(fd) {
|
|
13
|
+
//console.log("WeaveDrive: Resetting fd: ", fd)
|
|
14
|
+
FS.streams[fd].node.position = 0
|
|
15
|
+
FS.streams[fd].node.cache = new Uint8Array(0)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
joinUrl({ url, path }) {
|
|
19
|
+
if (!path) return url
|
|
20
|
+
if (path.startsWith("/"))
|
|
21
|
+
return this.joinUrl({ url, path: path.slice(1) })
|
|
22
|
+
|
|
23
|
+
url = new URL(url)
|
|
24
|
+
url.pathname += path
|
|
25
|
+
return url.toString()
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async customFetch(path, options) {
|
|
29
|
+
/**
|
|
30
|
+
* mod.ARWEAVE may be a comma-delimited list of urls.
|
|
31
|
+
* So we parse it into an array that we sequentially consume
|
|
32
|
+
* using fetch, and return the first successful response.
|
|
33
|
+
*
|
|
34
|
+
* The first url is considered "primary". So if all urls fail
|
|
35
|
+
* to produce a successful response, then we return the primary's
|
|
36
|
+
* error response
|
|
37
|
+
*/
|
|
38
|
+
const urlList = mod.ARWEAVE.includes(",")
|
|
39
|
+
? mod.ARWEAVE.split(",").map(url => url.trim())
|
|
40
|
+
: [mod.ARWEAVE]
|
|
41
|
+
|
|
42
|
+
let p
|
|
43
|
+
for (const url of urlList) {
|
|
44
|
+
const res = fetch(this.joinUrl({ url, path }), options)
|
|
45
|
+
if (await res.then(r => r.ok).catch(() => false)) return res
|
|
46
|
+
if (!p) p = res
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* None succeeded so fallback to the primary and accept
|
|
51
|
+
* whatever it returned
|
|
52
|
+
*/
|
|
53
|
+
return p
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async create(id) {
|
|
57
|
+
var properties = { isDevice: false, contents: null }
|
|
58
|
+
|
|
59
|
+
if (!(await this.checkAdmissible(id))) {
|
|
60
|
+
//console.log("WeaveDrive: Arweave ID is not admissable! ", id)
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create the file in the emscripten FS
|
|
65
|
+
|
|
66
|
+
// This check/mkdir was added for AOP 6 Boot loader because create is
|
|
67
|
+
// called first because were only loading Data, we needed to create
|
|
68
|
+
// the directory. See: https://github.com/permaweb/aos/issues/342
|
|
69
|
+
if (!FS.analyzePath("/data/").exists) {
|
|
70
|
+
FS.mkdir("/data/")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var node = FS.createFile("/", "data/" + id, properties, true, false)
|
|
74
|
+
// Set initial parameters
|
|
75
|
+
/*
|
|
76
|
+
var bytesLength = await this.customFetch(`/${id}`, {
|
|
77
|
+
method: "HEAD",
|
|
78
|
+
}).then(res => res.headers.get("Content-Length"))
|
|
79
|
+
*/
|
|
80
|
+
let data = await ar.data(id)
|
|
81
|
+
const bytesLength = data ? new TextEncoder().encode(data).length : 100
|
|
82
|
+
node.total_size = Number(bytesLength)
|
|
83
|
+
node.cache = new Uint8Array(0)
|
|
84
|
+
node.position = 0
|
|
85
|
+
|
|
86
|
+
// Add a function that defers querying the file size until it is asked the first time.
|
|
87
|
+
Object.defineProperties(node, {
|
|
88
|
+
usedBytes: {
|
|
89
|
+
get: function () {
|
|
90
|
+
return bytesLength
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Now we have created the file in the emscripten FS, we can open it as a stream
|
|
96
|
+
var stream = FS.open("/data/" + id, "r")
|
|
97
|
+
|
|
98
|
+
//console.log("JS: Created file: ", id, " fd: ", stream.fd);
|
|
99
|
+
return stream
|
|
100
|
+
},
|
|
101
|
+
async createBlockHeader(id) {
|
|
102
|
+
const customFetch = this.customFetch
|
|
103
|
+
// todo: add a bunch of retries
|
|
104
|
+
async function retry(x) {
|
|
105
|
+
return new Promise(r => {
|
|
106
|
+
setTimeout(function () {
|
|
107
|
+
r(customFetch(`/block/height/${id}`))
|
|
108
|
+
}, x * 10000)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
var result = await this.customFetch(`/block/height/${id}`)
|
|
112
|
+
.then(res => (!res.ok ? retry(1) : res))
|
|
113
|
+
.then(res => (!res.ok ? retry(2) : res))
|
|
114
|
+
.then(res => (!res.ok ? retry(3) : res))
|
|
115
|
+
.then(res => (!res.ok ? retry(4) : res))
|
|
116
|
+
.then(res => res.text())
|
|
117
|
+
|
|
118
|
+
var bytesLength = result.length
|
|
119
|
+
|
|
120
|
+
var node = FS.createDataFile(
|
|
121
|
+
"/",
|
|
122
|
+
"block/" + id,
|
|
123
|
+
Buffer.from(result, "utf-8"),
|
|
124
|
+
true,
|
|
125
|
+
false,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
var stream = FS.open("/block/" + id, "r")
|
|
129
|
+
return stream
|
|
130
|
+
},
|
|
131
|
+
async createTxHeader(id) {
|
|
132
|
+
const customFetch = this.customFetch
|
|
133
|
+
async function toAddress(owner) {
|
|
134
|
+
return Arweave.utils.bufferTob64Url(
|
|
135
|
+
await Arweave.crypto.hash(Arweave.utils.b64UrlToBuffer(owner)),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
async function retry(x) {
|
|
139
|
+
return new Promise(r => {
|
|
140
|
+
setTimeout(function () {
|
|
141
|
+
r(customFetch(`/tx/${id}`))
|
|
142
|
+
}, x * 10000)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
// todo: add a bunch of retries
|
|
146
|
+
var result = await this.customFetch(`/tx/${id}`)
|
|
147
|
+
.then(res => (!res.ok ? retry(1) : res))
|
|
148
|
+
.then(res => (!res.ok ? retry(2) : res))
|
|
149
|
+
.then(res => (!res.ok ? retry(3) : res))
|
|
150
|
+
.then(res => (!res.ok ? retry(4) : res))
|
|
151
|
+
.then(res => res.json())
|
|
152
|
+
.then(async entry => ({
|
|
153
|
+
...entry,
|
|
154
|
+
ownerAddress: await toAddress(entry.owner),
|
|
155
|
+
}))
|
|
156
|
+
//.then(x => (console.error(x), x))
|
|
157
|
+
.then(x => JSON.stringify(x))
|
|
158
|
+
|
|
159
|
+
var node = FS.createDataFile(
|
|
160
|
+
"/",
|
|
161
|
+
"tx/" + id,
|
|
162
|
+
Buffer.from(result, "utf-8"),
|
|
163
|
+
true,
|
|
164
|
+
false,
|
|
165
|
+
)
|
|
166
|
+
var stream = FS.open("/tx/" + id, "r")
|
|
167
|
+
return stream
|
|
168
|
+
},
|
|
169
|
+
async createDataItemTxHeader(id) {
|
|
170
|
+
const gqlQuery = this.gqlQuery
|
|
171
|
+
var GET_TRANSACTION_QUERY = `
|
|
172
|
+
query GetTransactions ($transactionIds: [ID!]!) {
|
|
173
|
+
transactions(ids: $transactionIds) {
|
|
174
|
+
edges {
|
|
175
|
+
node {
|
|
176
|
+
id
|
|
177
|
+
anchor
|
|
178
|
+
data {
|
|
179
|
+
size
|
|
180
|
+
}
|
|
181
|
+
signature
|
|
182
|
+
recipient
|
|
183
|
+
owner {
|
|
184
|
+
address
|
|
185
|
+
key
|
|
186
|
+
}
|
|
187
|
+
fee {
|
|
188
|
+
ar
|
|
189
|
+
winston
|
|
190
|
+
}
|
|
191
|
+
quantity {
|
|
192
|
+
winston
|
|
193
|
+
ar
|
|
194
|
+
}
|
|
195
|
+
tags {
|
|
196
|
+
name
|
|
197
|
+
value
|
|
198
|
+
}
|
|
199
|
+
bundledIn {
|
|
200
|
+
id
|
|
201
|
+
}
|
|
202
|
+
block {
|
|
203
|
+
id
|
|
204
|
+
timestamp
|
|
205
|
+
height
|
|
206
|
+
previous
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}`
|
|
212
|
+
var variables = { transactionIds: [id] }
|
|
213
|
+
async function retry(x) {
|
|
214
|
+
return new Promise(r => {
|
|
215
|
+
setTimeout(function () {
|
|
216
|
+
r(gqlQuery(GET_TRANSACTION_QUERY, variables))
|
|
217
|
+
}, x * 10000)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const gqlExists = await this.gqlExists()
|
|
222
|
+
if (!gqlExists) {
|
|
223
|
+
return "GQL Not Found!"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// todo: add a bunch of retries
|
|
227
|
+
var result = await this.gqlQuery(GET_TRANSACTION_QUERY, variables)
|
|
228
|
+
.then(res => (!res.ok ? retry(1) : res))
|
|
229
|
+
.then(res => (!res.ok ? retry(2) : res))
|
|
230
|
+
.then(res => (!res.ok ? retry(3) : res))
|
|
231
|
+
.then(res => (!res.ok ? retry(4) : res))
|
|
232
|
+
.then(res => res.json())
|
|
233
|
+
.then(res => {
|
|
234
|
+
return res?.data?.transactions?.edges?.[0]?.node
|
|
235
|
+
? res.data.transactions.edges[0].node
|
|
236
|
+
: "No results"
|
|
237
|
+
})
|
|
238
|
+
.then(async entry => {
|
|
239
|
+
return typeof entry == "string"
|
|
240
|
+
? entry
|
|
241
|
+
: {
|
|
242
|
+
format: 3,
|
|
243
|
+
...entry,
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
.then(x => {
|
|
247
|
+
return typeof x == "string" ? x : JSON.stringify(x)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (result === "No results") {
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
FS.createDataFile(
|
|
254
|
+
"/",
|
|
255
|
+
"tx2/" + id,
|
|
256
|
+
Buffer.from(result, "utf-8"),
|
|
257
|
+
true,
|
|
258
|
+
false,
|
|
259
|
+
)
|
|
260
|
+
var stream = FS.open("/tx2/" + id, "r")
|
|
261
|
+
|
|
262
|
+
return stream
|
|
263
|
+
},
|
|
264
|
+
async open(filename) {
|
|
265
|
+
const pathCategory = filename.split("/")[1]
|
|
266
|
+
const id = filename.split("/")[2]
|
|
267
|
+
console.log("JS: Opening ID: ", id)
|
|
268
|
+
if (pathCategory === "tx") {
|
|
269
|
+
FS.createPath("/", "tx", true, false)
|
|
270
|
+
if (FS.analyzePath(filename).exists) {
|
|
271
|
+
var stream = FS.open(filename, "r")
|
|
272
|
+
if (stream.fd) return stream.fd
|
|
273
|
+
return 0
|
|
274
|
+
} else {
|
|
275
|
+
const stream = await this.createTxHeader(id)
|
|
276
|
+
return stream.fd
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (pathCategory === "tx2") {
|
|
280
|
+
FS.createPath("/", "tx2", true, false)
|
|
281
|
+
if (FS.analyzePath(filename).exists) {
|
|
282
|
+
var stream = FS.open(filename, "r")
|
|
283
|
+
if (stream.fd) return stream.fd
|
|
284
|
+
return 0
|
|
285
|
+
} else {
|
|
286
|
+
const stream = await this.createDataItemTxHeader(id)
|
|
287
|
+
if (stream.fd) return stream.fd
|
|
288
|
+
return 0
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (pathCategory === "block") {
|
|
292
|
+
FS.createPath("/", "block", true, false)
|
|
293
|
+
if (FS.analyzePath(filename).exists) {
|
|
294
|
+
var stream = FS.open(filename, "r")
|
|
295
|
+
if (stream.fd) return stream.fd
|
|
296
|
+
return 0
|
|
297
|
+
} else {
|
|
298
|
+
const stream = await this.createBlockHeader(id)
|
|
299
|
+
return stream.fd
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (pathCategory === "data") {
|
|
303
|
+
if (FS.analyzePath(filename).exists) {
|
|
304
|
+
var stream = FS.open(filename, "r")
|
|
305
|
+
if (stream.fd) return stream.fd
|
|
306
|
+
console.log("JS: File not found: ", filename)
|
|
307
|
+
return 0
|
|
308
|
+
} else {
|
|
309
|
+
//console.log("JS: Open => Creating file: ", id);
|
|
310
|
+
const stream = await this.create(id)
|
|
311
|
+
//console.log("JS: Open => Created file: ", id, " fd: ", stream.fd);
|
|
312
|
+
return stream.fd
|
|
313
|
+
}
|
|
314
|
+
} else if (pathCategory === "headers") {
|
|
315
|
+
console.log("Header access not implemented yet.")
|
|
316
|
+
return 0
|
|
317
|
+
} else {
|
|
318
|
+
console.log("JS: Invalid path category: ", pathCategory)
|
|
319
|
+
return 0
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
async read(fd, raw_dst_ptr, raw_length) {
|
|
323
|
+
// Note: The length and dst_ptr are 53 bit integers in JS, so this _should_ be ok into a large memspace.
|
|
324
|
+
var to_read = Number(raw_length)
|
|
325
|
+
var dst_ptr = Number(raw_dst_ptr)
|
|
326
|
+
|
|
327
|
+
var stream = 0
|
|
328
|
+
for (var i = 0; i < FS.streams.length; i++) {
|
|
329
|
+
if (FS.streams[i].fd === fd) {
|
|
330
|
+
stream = FS.streams[i]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// read block headers
|
|
334
|
+
if (stream.path.includes("/block")) {
|
|
335
|
+
mod.HEAP8.set(stream.node.contents.subarray(0, to_read), dst_ptr)
|
|
336
|
+
return to_read
|
|
337
|
+
}
|
|
338
|
+
// read tx headers
|
|
339
|
+
if (stream.path.includes("/tx")) {
|
|
340
|
+
mod.HEAP8.set(stream.node.contents.subarray(0, to_read), dst_ptr)
|
|
341
|
+
return to_read
|
|
342
|
+
}
|
|
343
|
+
// Satisfy what we can with the cache first
|
|
344
|
+
var bytes_read = this.readFromCache(stream, dst_ptr, to_read)
|
|
345
|
+
stream.position += bytes_read
|
|
346
|
+
stream.lastReadPosition = stream.position
|
|
347
|
+
dst_ptr += bytes_read
|
|
348
|
+
to_read -= bytes_read
|
|
349
|
+
|
|
350
|
+
// Return if we have satisfied the request
|
|
351
|
+
if (to_read === 0) {
|
|
352
|
+
//console.log("WeaveDrive: Satisfied request with cache. Returning...")
|
|
353
|
+
return bytes_read
|
|
354
|
+
}
|
|
355
|
+
//console.log("WeaveDrive: Read from cache: ", bytes_read, " Remaining to read: ", to_read)
|
|
356
|
+
|
|
357
|
+
const chunk_download_sz = Math.max(to_read, CACHE_SZ)
|
|
358
|
+
const to = Math.min(
|
|
359
|
+
stream.node.total_size,
|
|
360
|
+
stream.position + chunk_download_sz,
|
|
361
|
+
)
|
|
362
|
+
//console.log("WeaveDrive: fd: ", fd, " Read length: ", to_read, " Reading ahead:", to - to_read - stream.position)
|
|
363
|
+
|
|
364
|
+
// Fetch with streaming
|
|
365
|
+
/*
|
|
366
|
+
const response = await this.customFetch(`/${stream.node.name}`, {
|
|
367
|
+
method: "GET",
|
|
368
|
+
redirect: "follow",
|
|
369
|
+
headers: { Range: `bytes=${stream.position}-${to}` },
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const reader = response.body.getReader()
|
|
373
|
+
*/
|
|
374
|
+
const data = new TextEncoder().encode(
|
|
375
|
+
(await ar.data(stream.node.name)) ?? "",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Extract the Range header to determine the start and end of the requested chunk
|
|
379
|
+
const start = 0
|
|
380
|
+
const end = data.length
|
|
381
|
+
|
|
382
|
+
// Create a ReadableStream for the requested chunk
|
|
383
|
+
const chunk = data.subarray(start, end)
|
|
384
|
+
const response = new Response(
|
|
385
|
+
new ReadableStream({
|
|
386
|
+
start(controller) {
|
|
387
|
+
controller.enqueue(chunk) // Push the chunk to the stream
|
|
388
|
+
controller.close() // Close the stream when done
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
{
|
|
392
|
+
headers: { "Content-Length": chunk.length.toString() },
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
const reader = response.body.getReader()
|
|
396
|
+
var bytes_until_cache = CHUNK_SZ
|
|
397
|
+
var bytes_until_notify = NOTIFY_SZ
|
|
398
|
+
var downloaded_bytes = 0
|
|
399
|
+
var cache_chunks = []
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
while (true) {
|
|
403
|
+
const { done, value: chunk_bytes } = await reader.read()
|
|
404
|
+
if (done) break
|
|
405
|
+
// Update the number of downloaded bytes to be _all_, not just the write length
|
|
406
|
+
downloaded_bytes += chunk_bytes.length
|
|
407
|
+
bytes_until_cache -= chunk_bytes.length
|
|
408
|
+
bytes_until_notify -= chunk_bytes.length
|
|
409
|
+
|
|
410
|
+
// Write bytes from the chunk and update the pointer if necessary
|
|
411
|
+
const write_length = Math.min(chunk_bytes.length, to_read)
|
|
412
|
+
if (write_length > 0) {
|
|
413
|
+
//console.log("WeaveDrive: Writing: ", write_length, " bytes to: ", dst_ptr)
|
|
414
|
+
mod.HEAP8.set(chunk_bytes.subarray(0, write_length), dst_ptr)
|
|
415
|
+
dst_ptr += write_length
|
|
416
|
+
bytes_read += write_length
|
|
417
|
+
stream.position += write_length
|
|
418
|
+
to_read -= write_length
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (to_read == 0) {
|
|
422
|
+
// Add excess bytes to our cache
|
|
423
|
+
const chunk_to_cache = chunk_bytes.subarray(write_length)
|
|
424
|
+
//console.log("WeaveDrive: Cacheing excess: ", chunk_to_cache.length)
|
|
425
|
+
cache_chunks.push(chunk_to_cache)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (bytes_until_cache <= 0) {
|
|
429
|
+
console.log(
|
|
430
|
+
"WeaveDrive: Chunk size reached. Compressing cache...",
|
|
431
|
+
)
|
|
432
|
+
stream.node.cache = this.addChunksToCache(
|
|
433
|
+
stream.node.cache,
|
|
434
|
+
cache_chunks,
|
|
435
|
+
)
|
|
436
|
+
cache_chunks = []
|
|
437
|
+
bytes_until_cache = CHUNK_SZ
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (bytes_until_notify <= 0) {
|
|
441
|
+
console.log(
|
|
442
|
+
"WeaveDrive: Downloaded: ",
|
|
443
|
+
(downloaded_bytes / stream.node.total_size) * 100,
|
|
444
|
+
"%",
|
|
445
|
+
)
|
|
446
|
+
bytes_until_notify = NOTIFY_SZ
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error("WeaveDrive: Error reading the stream: ", error)
|
|
451
|
+
} finally {
|
|
452
|
+
reader.releaseLock()
|
|
453
|
+
}
|
|
454
|
+
// If we have no cache, or we have not satisfied the full request, we need to download the rest
|
|
455
|
+
// Rebuild the cache from the new cache chunks
|
|
456
|
+
stream.node.cache = this.addChunksToCache(
|
|
457
|
+
stream.node.cache,
|
|
458
|
+
cache_chunks,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
// Update the last read position
|
|
462
|
+
stream.lastReadPosition = stream.position
|
|
463
|
+
return bytes_read
|
|
464
|
+
},
|
|
465
|
+
close(fd) {
|
|
466
|
+
var stream = 0
|
|
467
|
+
for (var i = 0; i < FS.streams.length; i++) {
|
|
468
|
+
if (FS.streams[i].fd === fd) {
|
|
469
|
+
stream = FS.streams[i]
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
FS.close(stream)
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
// Readahead cache functions
|
|
476
|
+
readFromCache(stream, dst_ptr, length) {
|
|
477
|
+
// Check if the cache has been invalidated by a seek
|
|
478
|
+
if (stream.lastReadPosition !== stream.position) {
|
|
479
|
+
//console.log("WeaveDrive: Invalidating cache for fd: ", stream.fd, " Current pos: ", stream.position, " Last read pos: ", stream.lastReadPosition)
|
|
480
|
+
stream.node.cache = new Uint8Array(0)
|
|
481
|
+
return 0
|
|
482
|
+
}
|
|
483
|
+
// Calculate the bytes of the request that can be satisfied with the cache
|
|
484
|
+
var cache_part_length = Math.min(length, stream.node.cache.length)
|
|
485
|
+
var cache_part = stream.node.cache.subarray(0, cache_part_length)
|
|
486
|
+
mod.HEAP8.set(cache_part, dst_ptr)
|
|
487
|
+
// Set the new cache to the remainder of the unused cache and update pointers
|
|
488
|
+
stream.node.cache = stream.node.cache.subarray(cache_part_length)
|
|
489
|
+
|
|
490
|
+
return cache_part_length
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
addChunksToCache(old_cache, chunks) {
|
|
494
|
+
// Make a new cache array of the old cache length + the sum of the chunk lengths, capped by the max cache size
|
|
495
|
+
var new_cache_length = Math.min(
|
|
496
|
+
old_cache.length +
|
|
497
|
+
chunks.reduce((acc, chunk) => acc + chunk.length, 0),
|
|
498
|
+
CACHE_SZ,
|
|
499
|
+
)
|
|
500
|
+
var new_cache = new Uint8Array(new_cache_length)
|
|
501
|
+
// Copy the old cache to the new cache
|
|
502
|
+
new_cache.set(old_cache, 0)
|
|
503
|
+
// Load the cache chunks into the new cache
|
|
504
|
+
var current_offset = old_cache.length
|
|
505
|
+
for (let chunk of chunks) {
|
|
506
|
+
if (current_offset < new_cache_length) {
|
|
507
|
+
new_cache.set(
|
|
508
|
+
chunk.subarray(0, new_cache_length - current_offset),
|
|
509
|
+
current_offset,
|
|
510
|
+
)
|
|
511
|
+
current_offset += chunk.length
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return new_cache
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// General helpder functions
|
|
518
|
+
async checkAdmissible(ID) {
|
|
519
|
+
if (mod.mode && mod.mode == "test") {
|
|
520
|
+
// CAUTION: If the module is initiated with `mode = test` we don't check availability.
|
|
521
|
+
return true
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check if we are attempting to load the On-Boot id, if so allow it
|
|
525
|
+
// this was added for AOP 6 Boot loader See: https://github.com/permaweb/aos/issues/342
|
|
526
|
+
const bootTag = this.getTagValue("On-Boot", mod.spawn.tags)
|
|
527
|
+
if (bootTag && bootTag === ID) return true
|
|
528
|
+
|
|
529
|
+
// Check that this module or process set the WeaveDrive tag on spawn
|
|
530
|
+
const blockHeight = mod.blockHeight
|
|
531
|
+
const moduleExtensions = this.getTagValues(
|
|
532
|
+
"Extension",
|
|
533
|
+
mod.module.tags,
|
|
534
|
+
)
|
|
535
|
+
const moduleHasWeaveDrive = moduleExtensions.includes("WeaveDrive")
|
|
536
|
+
const processExtensions = this.getTagValues(
|
|
537
|
+
"Extension",
|
|
538
|
+
mod.spawn.tags,
|
|
539
|
+
)
|
|
540
|
+
const processHasWeaveDrive =
|
|
541
|
+
moduleHasWeaveDrive || processExtensions.includes("WeaveDrive")
|
|
542
|
+
|
|
543
|
+
if (!processHasWeaveDrive) {
|
|
544
|
+
console.log(
|
|
545
|
+
"WeaveDrive: Process tried to call WeaveDrive, but extension not set!",
|
|
546
|
+
)
|
|
547
|
+
return false
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const modes = ["Assignments", "Individual", "Library"]
|
|
551
|
+
// Get the Availability-Type from the spawned process's Module or Process item
|
|
552
|
+
// First check the module for its defaults
|
|
553
|
+
const moduleAvailabilityType = this.getTagValue(
|
|
554
|
+
"Availability-Type",
|
|
555
|
+
mod.module.tags,
|
|
556
|
+
)
|
|
557
|
+
const moduleMode = moduleAvailabilityType
|
|
558
|
+
? moduleAvailabilityType
|
|
559
|
+
: "Assignments" // Default to assignments
|
|
560
|
+
|
|
561
|
+
// Now check the process's spawn item. These settings override Module item settings.
|
|
562
|
+
const processAvailabilityType = this.getTagValue(
|
|
563
|
+
"Availability-Type",
|
|
564
|
+
mod.spawn.tags,
|
|
565
|
+
)
|
|
566
|
+
const processMode = processAvailabilityType
|
|
567
|
+
? processAvailabilityType
|
|
568
|
+
: moduleMode
|
|
569
|
+
|
|
570
|
+
if (!modes.includes(processMode)) {
|
|
571
|
+
throw `Unsupported WeaveDrive mode: ${processMode}`
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const attestors = this.serializeStringArr(
|
|
575
|
+
[
|
|
576
|
+
this.getTagValue("Scheduler", mod.spawn.tags),
|
|
577
|
+
...this.getTagValues("Attestor", mod.spawn.tags),
|
|
578
|
+
].filter(t => !!t),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
// Init a set of GraphQL queries to run in order to find a valid attestation
|
|
582
|
+
// Every WeaveDrive process has at least the "Assignments" availability check form.
|
|
583
|
+
const assignmentsHaveID = await this.queryHasResult(
|
|
584
|
+
`query {
|
|
585
|
+
transactions(
|
|
586
|
+
owners: ${attestors},
|
|
587
|
+
block: {min: 0, max: ${blockHeight}},
|
|
588
|
+
tags: [
|
|
589
|
+
{ name: "Type", values: ["Attestation"] },
|
|
590
|
+
{ name: "Message", values: ["${ID}"]}
|
|
591
|
+
{ name: "Data-Protocol", values: ["ao"] },
|
|
592
|
+
]
|
|
593
|
+
)
|
|
594
|
+
{
|
|
595
|
+
edges {
|
|
596
|
+
node {
|
|
597
|
+
tags {
|
|
598
|
+
name
|
|
599
|
+
value
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}`,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if (assignmentsHaveID) {
|
|
608
|
+
return true
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (processMode == "Individual") {
|
|
612
|
+
const individualsHaveID = await this.queryHasResult(
|
|
613
|
+
`query {
|
|
614
|
+
transactions(
|
|
615
|
+
owners: ${attestors},
|
|
616
|
+
block: {min: 0, max: ${blockHeight}},
|
|
617
|
+
tags: [
|
|
618
|
+
{ name: "Type", values: ["Available"]},
|
|
619
|
+
{ name: "ID", values: ["${ID}"]}
|
|
620
|
+
{ name: "Data-Protocol", values: ["WeaveDrive"] },
|
|
621
|
+
]
|
|
622
|
+
)
|
|
623
|
+
{
|
|
624
|
+
edges {
|
|
625
|
+
node {
|
|
626
|
+
tags {
|
|
627
|
+
name
|
|
628
|
+
value
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}`,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if (individualsHaveID) {
|
|
637
|
+
return true
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Halt message processing if the process requires Library mode.
|
|
642
|
+
// This should signal 'Cannot Process' to the CU, not that the message itself is
|
|
643
|
+
// invalid. Subsequently, the CU should not be slashable for saying that the process
|
|
644
|
+
// execution failed on this message. The CU must also not continue to execute further
|
|
645
|
+
// messages on this process. Attesting to them would be slashable, as the state would
|
|
646
|
+
// be incorrect.
|
|
647
|
+
if (processMode == "Library") {
|
|
648
|
+
throw "This WeaveDrive implementation does not support Library attestations yet!"
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return false
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
serializeStringArr(arr = []) {
|
|
655
|
+
return `[${arr.map(s => `"${s}"`).join(", ")}]`
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
getTagValues(key, tags) {
|
|
659
|
+
var values = []
|
|
660
|
+
for (i = 0; i < tags.length; i++) {
|
|
661
|
+
if (tags[i].name == key) {
|
|
662
|
+
values.push(tags[i].value)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return values
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
getTagValue(key, tags) {
|
|
669
|
+
const values = this.getTagValues(key, tags)
|
|
670
|
+
return values.pop()
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
async queryHasResult(query, variables) {
|
|
674
|
+
const json = await this.gqlQuery(query, variables).then(res =>
|
|
675
|
+
res.json(),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
return !!json?.data?.transactions?.edges?.length
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
async gqlExists() {
|
|
682
|
+
const query = `query {
|
|
683
|
+
transactions(
|
|
684
|
+
first: 1
|
|
685
|
+
) {
|
|
686
|
+
pageInfo {
|
|
687
|
+
hasNextPage
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
`
|
|
692
|
+
|
|
693
|
+
const gqlExists = await this.gqlQuery(query, {}).then(res => res.ok)
|
|
694
|
+
return gqlExists
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
async gqlQuery(query, variables) {
|
|
698
|
+
const options = {
|
|
699
|
+
method: "POST",
|
|
700
|
+
body: JSON.stringify({ query, variables }),
|
|
701
|
+
headers: { "Content-Type": "application/json" },
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return this.customFetch("graphql", options)
|
|
705
|
+
},
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|