Skip to content

Commit

Permalink
add get module (#55)
Browse files Browse the repository at this point in the history
* add `get` module

* add index.html

* import get module

* call get

* print exit code

* print buffer

* fix

* log read/write

* fix
  • Loading branch information
Trinidadec authored Nov 8, 2023
1 parent 5dbf8bf commit ba8bd68
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 61 deletions.
199 changes: 199 additions & 0 deletions get.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import base32 from './base32.mjs'
import tree from './tree.mjs'
import digest256 from './digest256.mjs'
/** @typedef {import('./tree.mjs').State} StateTree */
/**
* @template T
* @typedef {import('./subtree.mjs').Nullable<T>} Nullable
*/
const { toAddress } = base32
const { push: pushTree, end: endTree, partialEnd: partialEndTree, pushDigest } = tree
const { tailToDigest } = digest256

/**
* second element is root flag
* @typedef {readonly [string, boolean]} Address
*/

/**
* @typedef {[Address, Uint8Array]} Block
*/

/**
* @template T
* @typedef {readonly['ok', T]} Ok
*/

/**
* @template E
* @typedef {readonly['error', E]} Error
*/

/**
* @template T
* @template E
* @typedef {Ok<T>|Error<E>} Result
*/

/**
* @typedef {readonly Uint8Array[]} OkOutput
*/

/**
* @typedef { Result<OkOutput,string> } Output
*/

/**
* @typedef { Uint8Array } ReadonlyUint8Array
*/

/**
* @typedef {[Address, Nullable<ReadonlyUint8Array>]} BlockState
*/

/**
* @typedef { BlockState[] } State
*/

/**
* @typedef {{
* readonly read: (address: Address) => Promise<Uint8Array>,
* readonly write: (buffer: Uint8Array) => Promise<void>,
* }} Provider
*/

/** @type {(address: Address) => string} */
const getPath = ([address, isRoot]) => {
const dir = isRoot ? 'roots' : 'parts'
return `cdt0/${dir}/${address.substring(0, 2)}/${address.substring(2, 4)}/${address.substring(4)}`
}

/** @type {(state: State) => (block: Block) => void} */
const insertBlock = state => block => {
for (let i = 0; i < state.length; i++) {
if (state[i][0][0] === block[0][0]) {
state[i][1] = block[1]
}
}
}

/** @type {(state: State) => Output} */
const nextState = state => {
/** @type {Uint8Array[]} */
let resultBuffer = []

while (true) {
const blockLast = state.at(-1)
if (blockLast === undefined) {
return ['ok', resultBuffer]
}

const blockData = blockLast[1]
if (blockData === null) {
return ['ok', resultBuffer]
}

state.pop()

if (blockLast[0][0] === '') {
resultBuffer.push(blockData)
continue
}

/** @type {StateTree} */
let verificationTree = []
const tailLength = blockData[0]
if (tailLength === 32) {
const data = blockData.subarray(1)
for (let byte of data) {
pushTree(verificationTree)(byte)
}
resultBuffer.push(data)
} else {
const tail = blockData.subarray(1, tailLength + 1)
if (tail.length !== 0) {
state.push([['', false], tail])
}
/** @type {Address[]} */
let childAddresses = []
for (let i = tailLength + 1; i < blockData.length; i += 28) {
let hash = 0n
for (let j = 0; j < 28; j++) {
hash += BigInt(blockData[i + j]) << BigInt(8 * j)
}
pushDigest(verificationTree)(hash | (0xffff_ffffn << 224n))
const childAddress = toAddress(hash)
childAddresses.push([childAddress, false])
}
pushDigest(verificationTree)(tailToDigest(tail))
const digest = blockLast[0][1] ? endTree(verificationTree) : partialEndTree(verificationTree)
if (digest === null || toAddress(digest) !== blockLast[0][0]) {
return ['error', `verification failed ${blockLast[0][0]}`]
}

for (let i = childAddresses.length - 1; i >= 0; i--) {
state.push([childAddresses[i], null])
}
}
}
}

/** @type {(hostName: string) => (address: Address) => Promise<Uint8Array>} */
const fetchRead = hostName => address => fetch(`https://${hostName}/${getPath(address)}`)
.then(async (resp) => resp.arrayBuffer().then(buffer => new Uint8Array(buffer)))

/** @type {(provider: Provider) => (root: string) => Promise<string | null>} */
const get = ({ read, write }) => async (root) => {
/** @type {State} */
let state = [[[root, true], null]]
/** @type {[Address, Promise<Uint8Array>] | null} */
let readPromise = null
/** @type {Promise<void> | null} */
let writePromise = null
try {
while (true) {
const blockLast = state.at(-1)
if (blockLast === undefined) {
if (writePromise === null) {
return 'unexpected behaviour'
}
await writePromise
return null
}

if (readPromise !== null) {
const data = await readPromise[1]
insertBlock(state)([readPromise[0], data])
}

for (let i = state.length - 1; i >= 0; i--) {
const blockLastI = state[i]
if (blockLastI[1] === null) {
const address = blockLastI[0]
readPromise = [address, read(address)]
break
}
}

const next = nextState(state)
if (next[0] === 'error') {
return `${next[1]}`
}

const writeData = next[1]
for (let buffer of writeData) {
if (writePromise === null) {
writePromise = Promise.resolve()
}
writePromise = writePromise.then(() => write(buffer))
}
}
} catch (err) {
return `${err}`
}
}

export default {
get,
fetchRead
}
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="main.css">
</head>
<body>
Hash: <input id="input"/><button id="download">Download</button>
<pre id="log"></pre>
<pre id="output"></pre>
</body>
<script type="module" src="web-test.mjs"></script>
</html>
75 changes: 14 additions & 61 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fsPromises from 'node:fs/promises'
import base32 from './base32.mjs'
import tree from './tree.mjs'
import digest256 from './digest256.mjs'
import getModule from './get.mjs'
/** @typedef {import('./tree.mjs').State} StateTree */
/**
* @template T
Expand All @@ -11,6 +12,7 @@ import digest256 from './digest256.mjs'
const { toAddress } = base32
const { push: pushTree, end: endTree, partialEnd: partialEndTree, pushDigest } = tree
const { tailToDigest } = digest256
const { get, fetchRead } = getModule

/**
* second element is root flag
Expand Down Expand Up @@ -142,9 +144,8 @@ const nextState = state => {

/** @type {(hostName: string) => Provider} */
const fetchProvider = hostName => ({
read: address => fetch(`https://${hostName}/${getPath(address)}`)
.then(async (resp) => resp.arrayBuffer().then(buffer => new Uint8Array(buffer))),
write: path => buffer => fsPromises.appendFile(path, buffer)
read: fetchRead(hostName),
write: path => buffer => fsPromises.appendFile(path, buffer)
})

/** @type {Provider} */
Expand All @@ -159,71 +160,23 @@ const syncFileProvider = {
write: path => buffer => Promise.resolve(fs.appendFileSync(path, buffer))
}

// /** @type {Provider} */
// const getReadPromise2 = address => {
// const path = getPath(address)
// return fsPromises.access(path)
// .then(() => fsPromises.readFile(path))
// .catch(() => fetchProvider(path))
// }

/** @type {(provider: Provider) => (root: [string, string]) => Promise<number>} */
const get = ({ read, write }) => async ([root, file]) => {
const getLocal = ({ read, write }) => async ([root, file]) => {
const tempFile = `_temp_${root}`
/** @type {State} */
let state = [[[root, true], null]]
/** @type {[Address, Promise<Uint8Array>] | null} */
let readPromise = null
/** @type {Promise<void> | null} */
let writePromise = null
try {
while (true) {
const blockLast = state.at(-1)
if (blockLast === undefined) {
if (writePromise === null) {
return -1
}
await writePromise
await fsPromises.rename(tempFile, file)
return 0
}

if (readPromise !== null) {
const data = await readPromise[1]
insertBlock(state)([readPromise[0], data])
}

for (let i = state.length - 1; i >= 0; i--) {
const blockLastI = state[i]
if (blockLastI[1] === null) {
const address = blockLastI[0]
readPromise = [address, read(address)]
break
}
}

const next = nextState(state)
if (next[0] === 'error') {
console.error(`${next[1]}`)
return -1
}

const writeData = next[1]
for (let buffer of writeData) {
if (writePromise === null) {
writePromise = fsPromises.writeFile(tempFile, new Uint8Array())
}
writePromise = writePromise.then(() => write(tempFile)(buffer))
}
}
} catch (err) {
console.error(err);
await fsPromises.writeFile(tempFile, new Uint8Array())
/** @type {(buffer: Uint8Array) => Promise<void>} */
const write = buffer => fsPromises.appendFile(tempFile, buffer)
const error = await get({ read, write })(root)
if (error !== null) {
console.error(error)
return -1
}
await fsPromises.rename(tempFile, file)
return 0
}

export default {
get,
get: getLocal,
syncFileProvider,
asyncFileProvider,
fetchProvider
Expand Down
3 changes: 3 additions & 0 deletions main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* {
font-family: monospace;
}
28 changes: 28 additions & 0 deletions web-test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import getModule from './get.mjs'
/** @typedef {import('./get.mjs').Address} Address */
const { get, fetchRead } = getModule

// @ts-ignore
document.getElementById('download').addEventListener('click', () => {
// @ts-ignore
const hash = document.getElementById('input').value
let buffer = new Uint8Array()
const fRead = fetchRead('410f5a49.blockset-js-test.pages.dev')
/** @type {(address: Address) => Promise<Uint8Array>} */
const read = address => {
// @ts-ignore
document.getElementById('log').innerText += `read from ${address}\n`
return fRead(address)
}
/** @type {(b: Uint8Array) => Promise<void>} */
const write = async (b) => {
// @ts-ignore
document.getElementById('log').innerText += `write ${b.length}\n`
buffer = new Uint8Array([...buffer, ...b])
}
get({ read, write })(hash).then(exitCode => {
const innerText = exitCode === null ? new TextDecoder().decode(buffer) : `error exit code = ${exitCode}`
// @ts-ignore
document.getElementById('output').innerText = innerText
})
});

0 comments on commit ba8bd68

Please sign in to comment.