diff --git a/get.mjs b/get.mjs new file mode 100644 index 0000000..b8500a2 --- /dev/null +++ b/get.mjs @@ -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} 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|Error} Result + */ + +/** + * @typedef {readonly Uint8Array[]} OkOutput + */ + +/** + * @typedef { Result } Output +*/ + +/** + * @typedef { Uint8Array } ReadonlyUint8Array + */ + +/** + * @typedef {[Address, Nullable]} BlockState + */ + +/** + * @typedef { BlockState[] } State +*/ + +/** + * @typedef {{ + * readonly read: (address: Address) => Promise, + * readonly write: (buffer: Uint8Array) => Promise, + * }} 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} */ +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} */ +const get = ({ read, write }) => async (root) => { + /** @type {State} */ + let state = [[[root, true], null]] + /** @type {[Address, Promise] | null} */ + let readPromise = null + /** @type {Promise | 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 +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..73ea4bc --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + +Hash: +

+    

+  
+  
+
diff --git a/index.mjs b/index.mjs
index ae78eac..e6be857 100644
--- a/index.mjs
+++ b/index.mjs
@@ -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
@@ -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
@@ -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} */
@@ -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} */
-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] | null} */
-  let readPromise = null
-  /** @type {Promise | 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} */
+  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
diff --git a/main.css b/main.css
new file mode 100644
index 0000000..5c97c27
--- /dev/null
+++ b/main.css
@@ -0,0 +1,3 @@
+* {
+  font-family: monospace;
+}
diff --git a/web-test.mjs b/web-test.mjs
new file mode 100644
index 0000000..f1fa3d1
--- /dev/null
+++ b/web-test.mjs
@@ -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} */
+  const read = address => {
+    // @ts-ignore
+    document.getElementById('log').innerText += `read from ${address}\n`
+    return fRead(address)
+  }
+  /** @type {(b: Uint8Array) => Promise} */
+  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
+  })
+});
\ No newline at end of file