Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add io module #65

Merged
merged 7 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
branches: [ "main" ]

env:
VERSION: 0.2.1
VERSION: 0.3.0

jobs:
node:
Expand Down
8 changes: 5 additions & 3 deletions cli.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env node

import index from './index.mjs'
const { get, asyncFileProvider, fetchProvider } = index
import ioNode from './io/node.mjs'
const { getLocal, getRemote } = index
const { node } = ioNode

var args = process.argv.slice(2)

Expand All @@ -12,5 +14,5 @@ if (args.length < 2) {
}

const hostName = args[2]
const provider = hostName === undefined ? asyncFileProvider : fetchProvider(hostName)
get(provider)([args[0], args[1]])
const getFunc = hostName === undefined ? getLocal : getRemote(hostName)
getFunc(node)([args[0], args[1]])
13 changes: 1 addition & 12 deletions get.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,6 @@ const { tailToDigest } = digest256
* }} 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++) {
Expand Down Expand Up @@ -138,10 +132,6 @@ const nextState = state => {
}
}

/** @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} */
Expand Down Expand Up @@ -194,6 +184,5 @@ const get = ({ read, write }) => async (root) => {
}

export default {
get,
fetchRead
get
}
183 changes: 30 additions & 153 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -1,183 +1,60 @@
import fs from 'node:fs'
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
* @typedef {import('./subtree.mjs').Nullable<T>} Nullable
*/
const { toAddress } = base32
const { push: pushTree, end: endTree, partialEnd: partialEndTree, pushDigest } = tree
const { tailToDigest } = digest256
const { get, fetchRead } = getModule
/** @typedef {import('./get.mjs').Address} Address */
/** @typedef {import('./io/io.mjs').IO} IO */

/**
* 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: (path: string) => (buffer: Uint8Array) => Promise<void>,
* }} Provider
*/
const { get } = getModule

/** @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]}`]
}
/** @type {(hostName: string) => (io: IO) => (address: Address) => Promise<Uint8Array>} */
const fetchRead = hostName => ({ fetch }) => address => fetch(`https://${hostName}/${getPath(address)}`)
.then(async (resp) => resp.arrayBuffer().then(buffer => new Uint8Array(buffer)))

for (let i = childAddresses.length - 1; i >= 0; i--) {
state.push([childAddresses[i], null])
}
}
/** @type {(io: IO) => (root: [string, string]) => Promise<number>} */
const getLocal = io => async ([root, file]) => {
const tempFile = `_temp_${root}`
await io.write(tempFile, new Uint8Array())
/** @type {(address: Address) => Promise<Uint8Array>} */
const read = address => io.read(getPath(address))
/** @type {(buffer: Uint8Array) => Promise<void>} */
const write = buffer => io.append(tempFile, buffer)
const error = await get({ read, write })(root)
if (error !== null) {
console.error(error)
return -1
}
await io.rename(tempFile, file)
return 0
}

/** @type {(hostName: string) => Provider} */
const fetchProvider = hostName => ({
read: fetchRead(hostName),
write: path => buffer => fsPromises.appendFile(path, buffer)
})

/** @type {Provider} */
const asyncFileProvider = {
read: address => fsPromises.readFile(getPath(address)),
write: path => buffer => fsPromises.appendFile(path, buffer)
}

/** @type {Provider} */
const syncFileProvider = {
read: address => Promise.resolve(fs.readFileSync(getPath(address))),
write: path => buffer => Promise.resolve(fs.appendFileSync(path, buffer))
}

/** @type {(provider: Provider) => (root: [string, string]) => Promise<number>} */
const getLocal = ({ read, write }) => async ([root, file]) => {
/** @type {(host: string) => (io: IO) => (root: [string, string]) => Promise<number>} */
const getRemote = host => io => async ([root, file]) => {
const tempFile = `_temp_${root}`
await fsPromises.writeFile(tempFile, new Uint8Array())
await io.write(tempFile, new Uint8Array())
/** @type {(address: Address) => Promise<Uint8Array>} */
const read = fetchRead(host)(io)
/** @type {(buffer: Uint8Array) => Promise<void>} */
const write = buffer => fsPromises.appendFile(tempFile, buffer)
const write = buffer => io.append(tempFile, buffer)
const error = await get({ read, write })(root)
if (error !== null) {
console.error(error)
return -1
}
await fsPromises.rename(tempFile, file)
await io.rename(tempFile, file)
return 0
}

export default {
get: getLocal,
syncFileProvider,
asyncFileProvider,
fetchProvider
getLocal,
getRemote,
fetchRead
}
13 changes: 13 additions & 0 deletions io/io.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @typedef {{
* readonly read: (path: string) => Promise<Uint8Array>,
* readonly append: (path: string, buffer: Uint8Array) => Promise<void>,
* readonly write: (path: string, buffer: Uint8Array) => Promise<void>,
* readonly rename: (oldPath: string, newPath: string) => Promise<void>
* readonly fetch: (url: string) => Promise<Response>
* readonly document: Document | undefined
* }} IO
*/

export default {
}
28 changes: 28 additions & 0 deletions io/node.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
/** @typedef {import('./io.mjs').IO} IO */

/** @type {IO} */
const node = {
read: fsPromises.readFile,
append: fsPromises.appendFile,
write: fsPromises.writeFile,
rename: fsPromises.rename,
fetch,
document: undefined
}

/** @type {IO} */
const nodeSync = {
read: async(path) => fs.readFileSync(path),
append: async(path, buffer) => fs.appendFileSync(path, buffer),
write: async(path, buffer) => fs.writeFileSync(path, buffer),
rename: async(oldPath, newPath) => fs.renameSync(oldPath, newPath),
fetch,
document: undefined
}

export default {
node,
nodeSync
}
17 changes: 17 additions & 0 deletions io/web.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** @typedef {import('./io.mjs').IO} IO */

const notImplemented = () => { throw 'not implemented' }

/** @type {IO} */
const web = {
read: notImplemented,
append: notImplemented,
write: notImplemented,
rename: notImplemented,
fetch,
document
}

export default {
web
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "blockset-js",
"version": "0.2.1",
"version": "0.3.0",
"description": "BLOCKSET on JavaScript",
"keywords": ["blockset", "content-addressable", "storage", "cdt", "content-dependent-tree", "hash"],
"main": "index.mjs",
Expand Down
Loading