diff --git a/app/components/Collapsable.vue b/app/components/Collapsable.vue
new file mode 100644
index 0000000..1fdb924
--- /dev/null
+++ b/app/components/Collapsable.vue
@@ -0,0 +1,31 @@
+<script lang="ts" setup>
+import { watchEffect } from 'vue'
+
+const props = defineProps<{
+  default?: boolean
+  label?: string
+}>()
+const isVisible = defineModel<boolean>({ default: false })
+watchEffect(() => {
+  if (props.default != null) {
+    isVisible.value = !!props.default
+  }
+})
+</script>
+
+<template>
+  <div flex="~ col" border="~ gray/25 rounded-lg" divide="y dashed gray/25" max-w-150 of-hidden shadow-sm>
+    <button
+      flex items-center justify-between px2 py1 text-sm
+      @click="isVisible = !isVisible"
+    >
+      <span>
+        <slot name="label">
+          {{ props.label ?? 'Inspect' }}
+        </slot></span> <span op50>{{ isVisible ? '▲' : '▼' }}</span>
+    </button>
+    <div v-if="isVisible">
+      <slot />
+    </div>
+  </div>
+</template>
diff --git a/app/components/Generate.vue b/app/components/Generate.vue
index 6a4d539..9b476d2 100644
--- a/app/components/Generate.vue
+++ b/app/components/Generate.vue
@@ -1,50 +1,53 @@
 <script lang="ts" setup>
-import { encode, renderSVG } from 'uqr'
+import type { EncodedBlock } from '~~/utils/lt-code'
+import { blockToBinary, createEncoder } from '~~/utils/lt-code'
+import { fromUint8Array } from 'js-base64'
+import { renderSVG } from 'uqr'
 
 const props = withDefaults(defineProps<{
-  data: string[]
+  data: Uint8Array
   speed: number
 }>(), {
   speed: 250,
 })
 
-const ecc = 'L' as const
-const minVersion = computed(() => encode(props.data[0]! || '', { ecc }).version)
-const svgList = computed(() => props.data.map(content => renderSVG(content, {
-  border: 1,
-  ecc,
-  minVersion: minVersion.value,
-})))
-const activeIndex = ref(0)
-watch(() => props.data, () => activeIndex.value = 0)
+const count = ref(0)
+const encoder = createEncoder(props.data, 1000)
+const svg = ref<string>()
+const block = shallowRef<EncodedBlock>()
 
-let intervalId: any
-function initInterval() {
-  intervalId = setInterval(() => {
-    activeIndex.value = (activeIndex.value + 1) % svgList.value.length
-  }, props.speed)
-}
-watch(() => props.speed, () => {
-  intervalId && clearInterval(intervalId)
-  initInterval()
-}, { immediate: true })
-onUnmounted(() => intervalId && clearInterval(intervalId))
+const renderTime = ref(0)
+const framePerSecond = computed(() => 1000 / renderTime.value)
+
+onMounted(() => {
+  let frame = performance.now()
+
+  useIntervalFn(() => {
+    count.value++
+    const data = encoder.fountain().next().value
+    block.value = data
+    const binary = blockToBinary(data)
+    const str = fromUint8Array(binary)
+    svg.value = renderSVG(str, { border: 1 })
+    const now = performance.now()
+    renderTime.value = now - frame
+    frame = now
+  }, () => props.speed)
+})
 </script>
 
 <template>
-  <div flex flex-col items-center>
-    <p mb-4>
-      {{ activeIndex }}/{{ svgList.length }}
+  <div flex flex-col items-center pb-20>
+    <p mb-4 w-full of-x-auto ws-nowrap font-mono>
+      Indices: {{ block?.indices }}<br>
+      Total: {{ block?.k }}<br>
+      Bytes: {{ ((block?.bytes || 0) / 1024).toFixed(2) }} KB<br>
+      Bitrate: {{ ((block?.bytes || 0) / 1024 * framePerSecond).toFixed(2) }} Kbps<br>
+      Frame Count: {{ count }}<br>
+      FPS: {{ framePerSecond.toFixed(2) }}
     </p>
     <div class="relative h-full w-full">
       <div
-        class="arc aspect-square" absolute inset-0
-        :style="{ '--deg': `${(activeIndex + 1) * 360 / svgList.length}deg` }"
-      />
-      <div
-        v-for="svg, idx of svgList"
-        :key="idx"
-        :class="{ hidden: idx !== activeIndex }"
         class="aspect-square [&>svg]:h-full [&>svg]:w-full"
         h-full w-full
         v-html="svg"
diff --git a/app/components/Scan.vue b/app/components/Scan.vue
index cf72e0e..67a81bc 100644
--- a/app/components/Scan.vue
+++ b/app/components/Scan.vue
@@ -1,6 +1,8 @@
 <script lang="ts" setup>
-import { merge, type SliceData } from '~~/utils/slicing'
+import { binaryToBlock, createDecoder } from '~~/utils/lt-code'
+import { toUint8Array } from 'js-base64'
 import { scan } from 'qr-scanner-wechat'
+import { useBytesRate } from '~/composables/timeseries'
 
 const props = withDefaults(defineProps<{
   speed?: number
@@ -8,12 +10,33 @@ const props = withDefaults(defineProps<{
   height?: number
 }>(), {
   speed: 34,
-  width: 512,
-  height: 512,
+  width: 1080,
+  height: 1080,
 })
 
-const { bytesReceived: validBytesReceivedInLastSecond, currentFormatted: currentValidBandwidthFormatted } = useBandwidth()
-const { bytesReceived: bytesReceivedInLastSecond, currentFormatted: currentBandwidthFormatted } = useBandwidth()
+enum CameraSignalStatus {
+  Waiting,
+  Ready,
+}
+
+const bytesReceived = ref(0)
+const totalValidBytesReceived = ref(0)
+
+const { formatted: currentValidBytesSpeedFormatted } = useBytesRate(totalValidBytesReceived, {
+  interval: 250,
+  timeWindow: 1000,
+  type: 'counter',
+  sampleRate: 50,
+  maxDataPoints: 100,
+})
+
+const { formatted: currentBytesFormatted } = useBytesRate(bytesReceived, {
+  interval: 250,
+  timeWindow: 1000,
+  type: 'counter',
+  sampleRate: 50,
+  maxDataPoints: 100,
+})
 
 const { devices } = useDevicesList({
   requestPermissions: true,
@@ -23,6 +46,7 @@ const { devices } = useDevicesList({
   },
 })
 
+const cameraSignalStatus = ref(CameraSignalStatus.Waiting)
 const cameras = computed(() => devices.value.filter(i => i.kind === 'videoinput'))
 const selectedCamera = ref(cameras.value[0]?.deviceId)
 
@@ -31,7 +55,7 @@ watchEffect(() => {
     selectedCamera.value = cameras.value[0]?.deviceId
 })
 
-const results = defineModel<Set<string>>('results', { default: new Set() })
+// const results = defineModel<Set<string>>('results', { default: new Set() })
 
 let stream: MediaStream | undefined
 
@@ -54,7 +78,14 @@ onMounted(async () => {
   }, { immediate: true })
 
   useIntervalFn(
-    () => scanFrame(),
+    async () => {
+      try {
+        await scanFrame()
+      }
+      catch (e) {
+        error.value = e
+      }
+    },
     () => props.speed,
   )
 })
@@ -83,13 +114,38 @@ async function connectCamera() {
   }
 }
 
-const chunks: SliceData[] = reactive([])
+const decoder = ref(createDecoder())
+const k = ref(0)
+const bytes = ref(0)
+const checksum = ref(0)
+const cached = new Set<string>()
+const startTime = ref(0)
+const endTime = ref(0)
 
-const length = computed(() => chunks.find(i => i?.[1])?.[1] || 0)
-const id = computed(() => chunks.find(i => i?.[0])?.[0] || 0)
-const picked = computed(() => Array.from({ length: length.value }, (_, idx) => chunks[idx]))
 const dataUrl = ref<string>()
 const dots = useTemplateRef<HTMLDivElement[]>('dots')
+const status = ref<number[]>([])
+const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
+const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
+
+function getStatus() {
+  const array = Array.from({ length: k.value }, () => 0)
+  for (let i = 0; i < k.value; i++) {
+    if (decoder.value.decodedData[i] != null)
+      array[i] = 1
+  }
+  for (const block of decoder.value.encodedBlocks) {
+    for (const i of block.indices) {
+      if (array[i] === 0 || array[i]! > block.indices.length) {
+        array[i] = block.indices.length
+      }
+      else {
+        console.warn(`Unexpected block #${i} status: ${array[i]}`)
+      }
+    }
+  }
+  return array
+}
 
 function pluse(index: number) {
   const el = dots.value?.[index]
@@ -97,7 +153,7 @@ function pluse(index: number) {
     return
   el.style.transition = 'none'
   el.style.transform = 'scale(1.3)'
-  el.style.filter = 'hue-rotate(90deg)'
+  el.style.filter = 'hue-rotate(-90deg)'
   // // force reflow
   void el.offsetWidth
   el.style.transition = 'transform 0.3s, filter 0.3s'
@@ -110,53 +166,84 @@ async function scanFrame() {
   const canvas = document.createElement('canvas')
   canvas.width = video.value!.videoWidth
   canvas.height = video.value!.videoHeight
+  if (video.value!.videoWidth === 0 || video.value!.videoHeight === 0) {
+    cameraSignalStatus.value = CameraSignalStatus.Waiting
+    return
+  }
+
+  cameraSignalStatus.value = CameraSignalStatus.Ready
   const ctx = canvas.getContext('2d')!
   ctx.drawImage(video.value!, 0, 0, canvas.width, canvas.height)
-  const result = await scan(canvas)
 
-  if (result?.text) {
-    setFps()
-    results.value.add(result.text)
-    const data = JSON.parse(result.text) as SliceData
-    if (Array.isArray(data)) {
-      if (data[0] !== id.value) {
-        chunks.length = 0
-        dataUrl.value = undefined
-      }
+  const result = await scan(canvas)
+  if (!result.text)
+    return
 
-      // Bandwidth calculation
-      {
-        const chunkSize = data[4].length
+  setFps()
+  bytesReceived.value += result.text.length
+  totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)
 
-        if (!chunks[data[2]]) {
-          validBytesReceivedInLastSecond.value += chunkSize
-        }
+  // Do not process the same QR code twice
+  if (cached.has(result.text))
+    return
 
-        bytesReceivedInLastSecond.value += chunkSize
-      }
+  error.value = undefined
+  const binary = toUint8Array(result.text)
+  const data = binaryToBlock(binary)
+  // Data set changed, reset decoder
+  if (checksum.value !== data.checksum) {
+    decoder.value = createDecoder()
+    checksum.value = data.checksum
+    bytes.value = data.bytes
+    k.value = data.k
+    startTime.value = performance.now()
+    endTime.value = 0
+    cached.clear()
+  }
+  // The previous data set is already decoded, skip for any new blocks
+  else if (endTime.value) {
+    return
+  }
 
-      chunks[data[2]] = data
-      pluse(data[2])
-
-      if (!length.value)
-        return
-      if (picked.value.every(i => !!i)) {
-        try {
-          const merged = merge(picked.value as SliceData[])
-          dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
-        }
-        catch (e) {
-          error.value = e
-        }
-      }
-    }
+  cached.add(result.text)
+  k.value = data.k
+  data.indices.map(i => pluse(i))
+  const success = decoder.value.addBlock(data)
+  status.value = getStatus()
+  if (success) {
+    endTime.value = performance.now()
+    const merged = decoder.value.getDecoded()!
+    dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
   }
+  // console.log({ data })
+  // if (Array.isArray(data)) {
+  //   if (data[0] !== id.value) {
+  //     chunks.length = 0
+  //     dataUrl.value = undefined
+  //   }
+
+  //
+
+  //   chunks[data[2]] = data
+  //   pluse(data[2])
+
+  //   if (!length.value)
+  //     return
+  //   if (picked.value.every(i => !!i)) {
+  //     try {
+  //       const merged = merge(picked.value as SliceData[])
+  //       dataUrl.value = URL.createObjectURL(new Blob([merged], { type: 'application/octet-stream' }))
+  //     }
+  //     catch (e) {
+  //       error.value = e
+  //     }
+  //   }
+  // }
 }
 
-watch(() => results.value.size, (size) => {
-  if (!size)
-    chunks.length = 0
-})
+function now() {
+  return performance.now()
+}
 </script>
 
 <template>
@@ -166,51 +253,102 @@ watch(() => results.value.size, (size) => {
         v-for="item of cameras" :key="item.deviceId" :class="{
           'text-blue': selectedCamera === item.deviceId,
         }"
-        class="border rounded-md px2 py1 text-sm shadow-sm"
+        px2 py1 text-sm shadow-sm
+        border="~ gray/25 rounded-lg"
         @click="selectedCamera = item.deviceId"
       >
         {{ item.label }}
       </button>
     </div>
 
-    <pre v-if="error" text-red v-text="error" />
-    <div border="~ gray/25 rounded-lg" flex="~ col gap-2" mb--4 max-w-150 p2>
-      <div flex="~ gap-0.4 wrap">
-        <div
-          v-for="x, idx in picked"
-          :key="idx"
-          ref="dots"
-          h-4
-          w-4
-          border="~ gray rounded"
-          :class="x ? 'bg-green border-green4' : 'bg-gray:50'"
-        />
+    <pre v-if="error" overflow-x-auto text-red v-text="error" />
+
+    <Collapsable>
+      <p w-full of-x-auto ws-nowrap px2 py1 font-mono :class="endTime ? 'text-green' : ''">
+        <span>Checksum: {{ checksum }}</span><br>
+        <span>Indices: {{ k }}</span><br>
+        <span>Decoded: {{ decodedBlocks }}</span><br>
+        <span>Received blocks: {{ decoder.encodedCount }}</span><br>
+        <span>Expected bytes: {{ (bytes / 1024).toFixed(2) }} KB</span><br>
+        <span>Received bytes: {{ (receivedBytes / 1024).toFixed(2) }} KB ({{ bytes === 0 ? 0 : (receivedBytes / bytes * 100).toFixed(2) }}%)</span><br>
+        <span>Timepassed: {{ (((endTime || now()) - startTime) / 1000).toFixed(2) }} s</span><br>
+        <span>Average bitrate: {{ (receivedBytes / 1024 / ((endTime || now()) - startTime) * 1000).toFixed(2) }} Kbps</span><br>
+      </p>
+    </Collapsable>
+
+    <Collapsable v-if="k" label="Packets" :default="true">
+      <div flex="~ col gap-2" max-w-150 p2>
+        <div flex="~ gap-0.4 wrap">
+          <div
+            v-for="x, idx of status"
+            :key="idx"
+            ref="dots"
+
+            flex="~ items-center justify-center"
+            h-4 w-4 overflow-hidden text-8px
+            border="~ rounded"
+            :class="x === 1 ? 'bg-green:100 border-green4!' : x > 1 ? 'bg-amber border-amber4 text-amber-900 dark:text-amber-200' : 'bg-gray:50 border-gray'"
+            :style="{ '--un-bg-opacity': Math.max(0.5, (11 - x) / 10) }"
+          >
+            {{ ([0, 1, 2].includes(x)) ? '' : x }}
+          </div>
+        </div>
+      </div>
+    </Collapsable>
+
+    <Collapsable v-if="dataUrl" label="Download" :default="true">
+      <div flex="~ col gap-2" max-w-150 p2>
+        <img :src="dataUrl">
+        <a
+          class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
+          :href="dataUrl"
+          download="foo.png"
+        >Download</a>
       </div>
-      <a
-        v-if="dataUrl"
-        class="w-max border border-gray:50 rounded-md px2 py1 text-sm hover:bg-gray:10"
-        :href="dataUrl"
-        download="foo.png"
-      >Download</a>
+    </Collapsable>
+
+    <!-- This is a progress bar that is not accurate but feels comfortable. -->
+    <div v-if="k" relative h-2 max-w-150 rounded-lg bg-black:75 text-white font-mono shadow>
+      <div
+        absolute inset-y-0 h-full bg-green border="~ green4 rounded-lg"
+        :style="{ width: `${decodedBlocks === k ? 100 : (Math.min(1, receivedBytes / bytes * 0.66) * 100).toFixed(2)}%` }"
+      />
     </div>
 
-    <div relative h-full max-h-150 max-w-150 w-full>
+    <div relative h-full max-h-150 max-w-150 w-full text="10px md:sm">
       <video
         ref="video"
         autoplay muted playsinline :controls="false"
         aspect-square h-full w-full rounded-lg
       />
-      <div absolute left-1 top-1 border border-gray:50 rounded-md bg-black:75 px2 py1 text-sm text-white font-mono shadow>
-        <template v-if="length">
-          {{ picked.filter(p => !!p).length }} / {{ length }}
+
+      <div absolute left-1 top-1 border="~ gray:50 rounded-md" bg-black:75 px2 py1 text-white font-mono shadow>
+        <template v-if="k">
+          {{ (receivedBytes / 1024).toFixed(2) }} / {{ (bytes / 1024).toFixed(2) }} KB <span text-neutral-400>({{ (receivedBytes / bytes * 100).toFixed(2) }}%)</span>
         </template>
         <template v-else>
           No Data
         </template>
       </div>
-      <p absolute right-1 top-1 border border-gray:50 rounded-md bg-black:75 px2 py1 text-sm text-white font-mono shadow>
-        {{ shutterCount }} | {{ fps.toFixed(0) }} hz | {{ currentValidBandwidthFormatted }} <span text-neutral-400>({{ currentBandwidthFormatted }})</span>
+      <div
+        v-if="cameraSignalStatus === CameraSignalStatus.Waiting"
+        top="50%" left="[calc(50%-4.5ch)]" text="neutral-500" absolute flex flex-col items-center gap-2 font-mono
+      >
+        <div i-carbon:circle-dash animate-spin animate-duration-5000 text-3xl />
+        <p>No Signal</p>
+      </div>
+      <p absolute right-1 top-1 border="~ gray:50 rounded-md" bg-black:75 px2 py1 text-white font-mono shadow>
+        {{ fps.toFixed(0) }} hz | {{ currentValidBytesSpeedFormatted }} <span text-neutral-400>({{ currentBytesFormatted }})</span>
       </p>
     </div>
+
+    <div flex="~ gap-1 wrap" max-w-150 text-xs>
+      <div v-for="i, idx of decoder.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
+        <template v-for="x, idy of i.indices" :key="x">
+          <span v-if="idy !== 0" op25>, </span>
+          <span :style="{ color: `hsl(${x * 40}, 40%, 60%)` }">{{ x }}</span>
+        </template>
+      </div>
+    </div>
   </div>
 </template>
diff --git a/app/composables/bandwidth.ts b/app/composables/bandwidth.ts
deleted file mode 100644
index e25adc9..0000000
--- a/app/composables/bandwidth.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-export function useBandwidth() {
-  const kiloBytesFormatter = new Intl.NumberFormat('en-US', {
-    style: 'unit',
-    unit: 'kilobyte-per-second',
-    unitDisplay: 'short',
-  })
-
-  const bytesReceived = ref(0)
-  const current = ref(0)
-  const animationFrameId = ref<number | null>(null)
-  const lastUpdateTime = ref(0)
-
-  const currentFormatted = computed(() => {
-    return kiloBytesFormatter.format(current.value)
-  })
-
-  function updateBandwidth(timestamp: number) {
-    const now = timestamp
-    const elapsedTime = now - lastUpdateTime.value
-
-    if (elapsedTime >= 1000) {
-      // Calculate bandwidth for the last second
-      current.value = Number.parseFloat(
-        (
-          (bytesReceived.value / 1024)
-          / (elapsedTime / 1000)
-        ).toFixed(2),
-      )
-
-      // Reset for the next second
-      bytesReceived.value = 0
-
-      lastUpdateTime.value = now
-    }
-
-    requestAnimationFrame(updateBandwidth)
-  }
-
-  onMounted(() => {
-    animationFrameId.value = requestAnimationFrame(updateBandwidth)
-  })
-
-  onUnmounted(() => {
-    if (animationFrameId.value !== null) {
-      cancelAnimationFrame(animationFrameId.value)
-    }
-  })
-
-  return {
-    bytesReceived,
-    current,
-    currentFormatted,
-  }
-}
diff --git a/app/composables/timeseries.ts b/app/composables/timeseries.ts
new file mode 100644
index 0000000..07c2ab2
--- /dev/null
+++ b/app/composables/timeseries.ts
@@ -0,0 +1,163 @@
+import type { ComputedRef, Ref } from 'vue'
+import { computed, ref } from 'vue'
+
+interface TimeSeriesOptions {
+  type: 'counter' | 'gauge'
+  interval: number
+  maxDataPoints: number
+}
+
+interface DataPoint {
+  time: number
+  value: number
+}
+
+function timeSeriesOptions(options?: Partial<TimeSeriesOptions>): TimeSeriesOptions {
+  if (!options) {
+    return {
+      type: 'gauge',
+      interval: 250,
+      maxDataPoints: 100,
+    }
+  }
+
+  return {
+    type: options?.type ?? 'gauge',
+    interval: options?.interval ?? 250,
+    maxDataPoints: options?.maxDataPoints ?? 100,
+  }
+}
+
+export function useTimeSeries(
+  input: Ref<number> | ComputedRef<number>,
+  opts?: Partial<TimeSeriesOptions>,
+) {
+  const options = timeSeriesOptions(opts)
+
+  const timeSeries = ref<DataPoint[]>([])
+  const lastValue = ref(0)
+  const lastSampleTime = ref(0)
+
+  onAnimationFrame((now) => {
+    if (now - lastSampleTime.value < options.interval) {
+      return
+    }
+
+    const currentValue = input.value
+
+    switch (options.type) {
+      case 'counter':
+        // eslint-disable-next-line no-case-declarations
+        const diff = Math.max(0, currentValue - lastValue.value)
+
+        timeSeries.value.push({ time: now, value: diff })
+        lastValue.value = currentValue
+        break
+      case 'gauge':
+        timeSeries.value.push({ time: now, value: currentValue })
+        break
+    }
+
+    // GC
+    if (timeSeries.value.length > options.maxDataPoints) {
+      const excessPoints = timeSeries.value.length - options.maxDataPoints
+      timeSeries.value.splice(0, excessPoints)
+    }
+
+    lastSampleTime.value = now
+  })
+
+  return timeSeries
+}
+
+interface BytesRateOption {
+  sampleRate: number
+  timeWindow: number
+}
+
+function bytesRateOptions(options?: Partial<BytesRateOption>): BytesRateOption {
+  if (!options) {
+    return {
+      sampleRate: 1000,
+      timeWindow: 10000,
+    }
+  }
+
+  return {
+    sampleRate: options?.sampleRate ?? 1000,
+    timeWindow: options?.timeWindow ?? 10000,
+  }
+}
+
+export function useBytesRate(
+  input: Ref<number> | ComputedRef<number>,
+  opts?: Partial<BytesRateOption & TimeSeriesOptions>,
+) {
+  const options = bytesRateOptions(opts)
+
+  const timeSeriesOpts = timeSeriesOptions(opts)
+  const timeSeries = useTimeSeries(input, timeSeriesOpts)
+
+  const bytesRate = ref(0)
+  const lastUpdateTime = ref(0)
+
+  onAnimationFrame((now) => {
+    if (now - lastUpdateTime.value < options.sampleRate) {
+      return
+    }
+
+    const cutoffTime = now - options.timeWindow
+    const relevantPoints = timeSeries.value.filter(point => point.time > cutoffTime)
+
+    if (relevantPoints.length < 2) {
+      bytesRate.value = 0
+    }
+
+    else {
+      const oldestPoint = relevantPoints[0]!
+      const newestPoint = relevantPoints[relevantPoints.length - 1]!
+      const timeDiff = newestPoint.time - oldestPoint.time
+      const valueDiff = relevantPoints.reduce((sum, point) => sum + point.value, 0)
+
+      // bytes per second
+      bytesRate.value = timeDiff > 0 ? (valueDiff / timeDiff) * 1000 : 0
+    }
+
+    lastUpdateTime.value = now
+  })
+
+  const kiloBytesFormatter = new Intl.NumberFormat('en-US', {
+    style: 'unit',
+    unit: 'kilobyte-per-second',
+    unitDisplay: 'short',
+  })
+
+  const formatted = computed(() => {
+    const kiloBytesPerSecond = bytesRate.value / 1024
+    return kiloBytesFormatter.format(Number.parseFloat(kiloBytesPerSecond.toFixed(2)))
+  })
+
+  return {
+    bytesRate,
+    formatted,
+  }
+}
+
+export function onAnimationFrame(callback: (timestamp: number) => void) {
+  const animationFrameId = ref<number | null>(null)
+
+  function onAnimationFrame(timestamp: number) {
+    callback(timestamp)
+    animationFrameId.value = requestAnimationFrame(onAnimationFrame)
+  }
+
+  onMounted(() => {
+    animationFrameId.value = requestAnimationFrame(onAnimationFrame)
+  })
+
+  onUnmounted(() => {
+    if (animationFrameId.value !== null) {
+      cancelAnimationFrame(animationFrameId.value)
+    }
+  })
+}
diff --git a/app/pages/index.vue b/app/pages/index.vue
index ef33f30..a546b92 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,6 +1,4 @@
 <script lang="ts" setup>
-import { slice } from '~~/utils/slicing'
-
 enum ReadPhase {
   Idle,
   Reading,
@@ -8,31 +6,29 @@ enum ReadPhase {
   Ready,
 }
 
-const count = ref(10)
-const demoData = ref(false)
-const speed = ref(50)
+const error = ref<any>()
+const speed = ref(100)
 const readPhase = ref<ReadPhase>(ReadPhase.Idle)
-const data = ref<Array<any>>([])
-
-if (demoData.value) {
-  data.value = Array.from({ length: count.value }, (_, i) => `hello world ${i}`)
-  readPhase.value = ReadPhase.Ready
-}
+const data = ref<Uint8Array | null>(null)
 
 async function onFileChange(file?: File) {
   if (!file) {
     readPhase.value = ReadPhase.Idle
-    data.value = []
-
+    data.value = null
     return
   }
 
-  readPhase.value = ReadPhase.Reading
-  const content = await file.arrayBuffer()
-  readPhase.value = ReadPhase.Chunking
-  const chunks = slice(content, 900)
-  readPhase.value = ReadPhase.Ready
-  data.value = chunks.map(i => JSON.stringify(i))
+  try {
+    readPhase.value = ReadPhase.Reading
+    const buffer = await file.arrayBuffer()
+    data.value = new Uint8Array(buffer)
+    readPhase.value = ReadPhase.Ready
+  }
+  catch (e) {
+    error.value = e
+    readPhase.value = ReadPhase.Idle
+    data.value = null
+  }
 }
 </script>
 
@@ -61,8 +57,13 @@ async function onFileChange(file?: File) {
         />
       </div>
     </div>
-    <div v-if="readPhase === ReadPhase.Ready" h-full w-full flex justify-center>
-      <Generate :speed="speed" :data="data" min-h="[calc(100vh-250px)]" max-w="[calc(100vh-250px)]" h-full w-full />
+    <div v-if="readPhase === ReadPhase.Ready && data" h-full w-full flex justify-center>
+      <Generate
+        :speed="speed" :data="data"
+        min-h="[calc(100vh-250px)]"
+        max-w="[calc(100vh-250px)]"
+        h-full w-full
+      />
     </div>
     <InputFile
       v-else
diff --git a/app/pages/scan.vue b/app/pages/scan.vue
index c10f07e..eb8cba7 100644
--- a/app/pages/scan.vue
+++ b/app/pages/scan.vue
@@ -1,12 +1,10 @@
 <script lang="ts" setup>
 const speed = ref(16)
-
-const results = ref(new Set<string>())
 </script>
 
 <template>
   <div px-4 pt-4 space-y-10>
-    <Scan v-model:results="results" :speed="speed" />
+    <Scan :speed="speed" />
     <div max-w-150 w-full inline-flex flex-row items-center>
       <span min-w-40>
         <span pr-2 text-zinc-400>Speed</span>
@@ -20,18 +18,18 @@ const results = ref(new Set<string>())
         w-full flex-1
       />
     </div>
-    <h2 flex items-center gap-4 text-3xl>
+    <!-- <h2 flex items-center gap-4 text-3xl>
       Results: {{ results.size }}
       <button class="flex items-center gap2 border rounded-md px2 py1 text-base shadow" @click="results = new Set()">
         <span i-carbon:clean inline-block />
         Clear
       </button>
-    </h2>
-    <div>
+    </h2> -->
+    <!-- <div>
       <div v-for="(item, index) of results" :key="index" font-mono>
         ID:{{ index }}:<br>
         {{ item }}
       </div>
-    </div>
+    </div> -->
   </div>
 </template>
diff --git a/test/lt-blocks.test.ts b/test/lt-blocks.test.ts
new file mode 100644
index 0000000..aaef2c1
--- /dev/null
+++ b/test/lt-blocks.test.ts
@@ -0,0 +1,53 @@
+import { expect, it } from 'vitest'
+import { createDecoder, createEncoder } from '../utils/lt-code'
+
+function randomBuffer(length: number) {
+  return new Uint8Array(length).map(() => Math.floor(Math.random() * 256))
+}
+
+function createEncoderWithIndices(count: number) {
+  const buffer = randomBuffer(count * 100 - 5)
+  const encoder = createEncoder(buffer, 100)
+  expect(encoder.k).toBe(count)
+
+  return {
+    buffer,
+    encoder,
+  }
+}
+
+it('cross-blocks resolving 1', () => {
+  const { buffer, encoder } = createEncoderWithIndices(3)
+
+  const decoder = createDecoder()
+  decoder.addBlock(encoder.createBlock([0, 1, 2]))
+  decoder.addBlock(encoder.createBlock([1, 2]))
+  // Here we know [0]
+  decoder.addBlock(encoder.createBlock([0, 2]))
+
+  const data = decoder.getDecoded()
+  expect(data).toBeDefined()
+  expect(data).toEqual(buffer)
+})
+
+// TODO: get it work
+it.skip('cross-blocks resolving 2', () => {
+  const { buffer, encoder } = createEncoderWithIndices(5)
+
+  const decoder = createDecoder()
+  decoder.addBlock(encoder.createBlock([0, 1, 2, 3, 4]))
+  decoder.addBlock(encoder.createBlock([0, 1]))
+  expect(decoder.decodedCount).toBe(0)
+  // Here we know [0, 1] and [2, 3, 4]
+  decoder.addBlock(encoder.createBlock([2, 3]))
+  // Here we can have [4]
+  expect(decoder.decodedCount).toBe(1)
+  decoder.addBlock(encoder.createBlock([1]))
+  // Here we can have [0] and [1]
+  expect(decoder.decodedCount).toBe(3)
+  decoder.addBlock(encoder.createBlock([4]))
+
+  const data = decoder.getDecoded()
+  expect(data).toBeDefined()
+  expect(data).toEqual(buffer)
+})
diff --git a/test/lt.test.ts b/test/lt.test.ts
index ea97019..5236916 100644
--- a/test/lt.test.ts
+++ b/test/lt.test.ts
@@ -1,73 +1,81 @@
 import fs from 'node:fs/promises'
 import { join } from 'node:path'
 
-import { describe, expect, it } from 'vitest'
-import { binaryToBlock, blockToBinary, createDecoder, encodeFountain } from '../utils/lt-codes'
+import { fromUint8Array, toUint8Array } from 'js-base64'
+import { expect, it } from 'vitest'
+import { binaryToBlock, blockToBinary, createDecoder, createEncoder } from '../utils/lt-code'
 
-describe('lt-codes', () => {
-  it('slice binary', async () => {
-    const input = (await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer
-    const data = new Uint32Array(input)
+const list: {
+  name: string
+  data: Uint8Array
+  only?: boolean
+  repeats?: number
+  size?: number
+}[] = [
+  {
+    name: 'generated-1',
+    data: new Uint8Array(1).fill(23),
+    // only: true,
+    // repeats: 0,
+  },
+  {
+    name: 'generated-2',
+    data: new Uint8Array(1000).fill(1),
+  },
+  {
+    name: 'generated-3',
+    data: new Uint8Array(1031).fill(1),
+  },
+  {
+    name: 'sample-jpg',
+    data: new Uint8Array((await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer),
+    size: 1200,
+  },
+]
 
-    const decoder = createDecoder()
-    let count = 0
-    for (const block of encodeFountain(data, 1000)) {
-      count += 1
-      if (count > 1000)
-        throw new Error('Too many blocks')
-      const binary = blockToBinary(block)
-      // Use the binary to transfer
-      const back = binaryToBlock(binary)
-      const result = decoder.addBlock([back])
-      if (result)
-        break
-    }
-
-    const result = decoder.getDecoded()!
-    expect(result).toBeDefined()
-    expect(result).toBeInstanceOf(Uint32Array)
-    expect(result.length).toBe(data.length)
-  })
-
-  it(`allow loss and disorder`, async () => {
-    const input = (await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer
-    const data = new Uint32Array(input)
+for (const item of list) {
+  let test = it
+  if (item.only)
+    test = (it as any).only
+  const {
+    data,
+    name,
+    repeats = 10,
+    size = 1000,
+  } = item
 
+  test(`slice binary: ${name} (size: ${size})`, { repeats }, async () => {
+    const encoder = createEncoder(data, size)
     const decoder = createDecoder()
-    let count = 0
 
-    const packets: Uint32Array[] = []
-
-    // Encode the data
-    for (const block of encodeFountain(data, 1000)) {
-      count += 1
-      if (count > 1000)
-        break
-      packets.push(blockToBinary(block))
-    }
-
-    // Dissupt the order of packets
-    packets.sort(() => Math.random() - 0.5)
-    // Simulate 50% of packet loss
-    packets.length = Math.floor(packets.length * 0.5)
+    let count = 0
 
-    count = 0
-    // Decode the data
-    for (const packet of packets) {
+    for (const block of encoder.fountain()) {
       count += 1
-      if (count > 500)
-        throw new Error('Too many blocks')
+      const rate = count / encoder.k
+      if (rate > 10) {
+        throw new Error('Too many blocks, aborting')
+      }
+      const binary = blockToBinary(block)
+      const str = fromUint8Array(binary)
+      // Use the str to transfer
 
-      // Use the binary to transfer
-      const back = binaryToBlock(packet)
-      const result = decoder.addBlock([back])
+      const b2 = toUint8Array(str)
+      const back = binaryToBlock(b2)
+      const result = decoder.addBlock(back)
       if (result)
         break
     }
 
     const result = decoder.getDecoded()!
     expect(result).toBeDefined()
-    expect(result).toBeInstanceOf(Uint32Array)
+    expect(result).toBeInstanceOf(Uint8Array)
     expect(result.length).toBe(data.length)
+
+    expect(
+      +(count / encoder.k * 100).toFixed(2),
+      'Data rate should be less than 200%',
+    )
+      .toBeLessThan(250) // TODO: target 180%
   })
-})
+}
diff --git a/utils/lt-code/checksum.ts b/utils/lt-code/checksum.ts
new file mode 100644
index 0000000..70cfaa8
--- /dev/null
+++ b/utils/lt-code/checksum.ts
@@ -0,0 +1,30 @@
+function generateCRCTable() {
+  const crcTable = new Uint32Array(256)
+  for (let i = 0; i < 256; i++) {
+    let crc = i
+    for (let j = 8; j > 0; j--) {
+      if (crc & 1) {
+        crc = (crc >>> 1) ^ 0xEDB88320 // Polynomial used in CRC-32
+      }
+      else {
+        crc = crc >>> 1
+      }
+    }
+    crcTable[i] = crc >>> 0
+  }
+  return crcTable
+}
+const crcTable = /* @__PURE__ */ generateCRCTable()
+/**
+ * Get checksum of the data using CRC32 and XOR with k to ensure uniqueness for different chunking sizes
+ */
+
+export function getChecksum(uint8Array: Uint8Array, k: number): number {
+  let crc = 0xFFFFFFFF // Initial value
+  for (let i = 0; i < uint8Array.length; i++) {
+    const byte = uint8Array[i]!
+    crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xFF]!
+  }
+
+  return (crc ^ k ^ 0xFFFFFFFF) >>> 0 // Final XOR value and ensure 32-bit unsigned
+}
diff --git a/utils/lt-code/decoder.ts b/utils/lt-code/decoder.ts
new file mode 100644
index 0000000..95c0cbf
--- /dev/null
+++ b/utils/lt-code/decoder.ts
@@ -0,0 +1,123 @@
+import type { EncodedBlock } from './shared'
+import { getChecksum } from './checksum'
+import { xorUint8Array } from './shared'
+
+export function createDecoder(blocks?: EncodedBlock[]) {
+  return new LtDecoder(blocks)
+}
+
+export class LtDecoder {
+  public decodedData: (Uint8Array | undefined)[] = []
+  public decodedCount = 0
+  public encodedCount = 0
+  public encodedBlocks: Set<EncodedBlock> = new Set()
+  public meta: EncodedBlock = undefined!
+
+  constructor(blocks?: EncodedBlock[]) {
+    if (blocks) {
+      for (const block of blocks) {
+        this.addBlock(block)
+      }
+    }
+  }
+
+  // Add block and decode them on the fly
+  addBlock(block: EncodedBlock): boolean {
+    if (!this.meta) {
+      this.meta = block
+      this.decodedData = Array.from({ length: this.meta.k })
+    }
+
+    if (block.checksum !== this.meta.checksum) {
+      throw new Error('Adding block with different checksum')
+    }
+    this.encodedBlocks.add(block)
+    this.encodedCount += 1
+
+    this.propagateDecoded()
+
+    return this.decodedCount === this.meta.k
+  }
+
+  propagateDecoded() {
+    let changed = false
+    for (const block of this.encodedBlocks) {
+      let { data, indices } = block
+
+      // We already have all the data from this block
+      if (indices.every(index => this.decodedData[index] != null)) {
+        this.encodedBlocks.delete(block)
+        continue
+      }
+
+      // XOR the data
+      for (const index of indices) {
+        if (this.decodedData[index] != null) {
+          block.data = data = xorUint8Array(data, this.decodedData[index]!)
+          block.indices = indices = indices.filter(i => i !== index)
+          changed = true
+        }
+      }
+
+      if (indices.length === 1 && this.decodedData[indices[0]!] == null) {
+        this.decodedData[indices[0]!] = block.data
+        this.decodedCount++
+        this.encodedBlocks.delete(block)
+        changed = true
+      }
+    }
+
+    for (const block of this.encodedBlocks) {
+      const { data, indices } = block
+
+      // Use 1x2x3 XOR 2x3 to get 1
+      if (indices.length >= 3) {
+        const lowerBlocks = Array.from(this.encodedBlocks).filter(i => i.indices.length === indices.length - 1)
+        for (const lower of lowerBlocks) {
+          const extraIndices = indices.filter(i => !lower.indices.includes(i))
+          if (extraIndices.length === 1 && this.decodedData[extraIndices[0]!] == null) {
+            const extraData = xorUint8Array(data, lower.data)
+            const extraIndex = extraIndices[0]!
+            this.decodedData[extraIndex] = extraData
+            this.decodedCount++
+            this.encodedBlocks.delete(lower)
+            changed = true
+          }
+        }
+      }
+    }
+
+    // If making some progress, continue
+    if (changed) {
+      this.propagateDecoded()
+    }
+  }
+
+  getDecoded(): Uint8Array | undefined {
+    if (this.decodedCount !== this.meta.k) {
+      return
+    }
+    if (this.decodedData.some(block => block == null)) {
+      return
+    }
+    const indiceSize = this.meta.data.length
+    const blocks = this.decodedData as Uint8Array[]
+    const decodedData = new Uint8Array(this.meta.bytes)
+    blocks.forEach((block, i) => {
+      const start = i * indiceSize
+      if (start + indiceSize > decodedData.length) {
+        for (let j = 0; j < decodedData.length - start; j++) {
+          decodedData[start + j] = block[j]!
+        }
+      }
+      else {
+        decodedData.set(block, i * indiceSize)
+      }
+    })
+    const checksum = getChecksum(decodedData, this.meta.k)
+    if (checksum !== this.meta.checksum) {
+      throw new Error('Checksum mismatch')
+    }
+    return decodedData
+  }
+}
diff --git a/utils/lt-code/encoder.ts b/utils/lt-code/encoder.ts
new file mode 100644
index 0000000..bd6ba56
--- /dev/null
+++ b/utils/lt-code/encoder.ts
@@ -0,0 +1,99 @@
+import type { EncodedBlock } from './shared'
+import { getChecksum } from './checksum'
+
+export function createEncoder(data: Uint8Array, indiceSize: number) {
+  return new LtEncoder(data, indiceSize)
+}
+
+export class LtEncoder {
+  public readonly k: number
+  public readonly indices: Uint8Array[]
+  public readonly checksum: number
+  public readonly bytes: number
+
+  constructor(
+    public readonly data: Uint8Array,
+    public readonly indiceSize: number,
+  ) {
+    this.indices = sliceData(data, indiceSize)
+    this.k = this.indices.length
+    this.checksum = getChecksum(data, this.k)
+    this.bytes = data.length
+  }
+
+  createBlock(indices: number[]): EncodedBlock {
+    const data = new Uint8Array(this.indiceSize)
+    for (const index of indices) {
+      const indice = this.indices[index]!
+      for (let i = 0; i < this.indiceSize; i++) {
+        data[i] = data[i]! ^ indice[i]!
+      }
+    }
+
+    return {
+      k: this.k,
+      bytes: this.bytes,
+      checksum: this.checksum,
+      indices,
+      data,
+    }
+  }
+
+  /**
+   * Generate random encoded blocks that **never** ends
+   */
+  *fountain(): Generator<EncodedBlock> {
+    while (true) {
+      const degree = getRandomDegree(this.k)
+      const selectedIndices = getRandomIndices(this.k, degree)
+      yield this.createBlock(selectedIndices)
+    }
+  }
+}
+
+function sliceData(data: Uint8Array, blockSize: number): Uint8Array[] {
+  const blocks: Uint8Array[] = []
+  for (let i = 0; i < data.length; i += blockSize) {
+    const block = new Uint8Array(blockSize)
+    block.set(data.slice(i, i + blockSize))
+    blocks.push(block)
+  }
+  return blocks
+}
+
+// Use Ideal Soliton Distribution to select degree
+function getRandomDegree(k: number): number {
+  const probabilities: number[] = Array.from({ length: k }, () => 0)
+
+  // Calculate the probabilities of the Ideal Soliton Distribution
+  probabilities[0] = 1 / k // P(1) = 1/k
+  for (let d = 2; d <= k; d++) {
+    probabilities[d - 1] = 1 / (d * (d - 1))
+  }
+
+  // Accumulate the probabilities to generate the cumulative distribution
+  const cumulativeProbabilities: number[] = probabilities.reduce((acc, p, index) => {
+    acc.push(p + (acc[index - 1] || 0))
+    return acc
+  }, [] as number[])
+
+  // Generate a random number between [0,1] and select the corresponding degree in the cumulative probabilities
+  const randomValue = Math.random()
+  for (let i = 0; i < cumulativeProbabilities.length; i++) {
+    if (randomValue < cumulativeProbabilities[i]!) {
+      return i + 1
+    }
+  }
+
+  return k // Theoretically, this line should never be reached
+}
+
+// Randomly select indices of degree number of original data blocks
+function getRandomIndices(k: number, degree: number): number[] {
+  const indices: Set<number> = new Set()
+  while (indices.size < degree) {
+    const randomIndex = Math.floor(Math.random() * k)
+    indices.add(randomIndex)
+  }
+  return Array.from(indices)
+}
diff --git a/utils/lt-code/index.ts b/utils/lt-code/index.ts
new file mode 100644
index 0000000..c54e34b
--- /dev/null
+++ b/utils/lt-code/index.ts
@@ -0,0 +1,3 @@
+export * from './decoder'
+export * from './encoder'
+export * from './shared'
diff --git a/utils/lt-code/shared.ts b/utils/lt-code/shared.ts
new file mode 100644
index 0000000..a33691d
--- /dev/null
+++ b/utils/lt-code/shared.ts
@@ -0,0 +1,71 @@
+export interface EncodedHeader {
+  /**
+   * Number of original data blocks
+   */
+  k: number
+  /**
+   * Data length for Uint8Array data
+   */
+  bytes: number
+  /**
+   * Checksum, CRC32 and XOR of k
+   */
+  checksum: number
+}
+
+export interface EncodedBlock extends EncodedHeader {
+  indices: number[]
+  data: Uint8Array
+}
+
+export function blockToBinary(block: EncodedBlock): Uint8Array {
+  const { k, bytes, checksum, indices, data } = block
+  const header = new Uint32Array([
+    indices.length,
+    ...indices,
+    k,
+    bytes,
+    checksum,
+  ])
+  const binary = new Uint8Array(header.length * 4 + data.length)
+  let offset = 0
+  binary.set(new Uint8Array(header.buffer), offset)
+  offset += header.length * 4
+  binary.set(data, offset)
+  return binary
+}
+
+export function binaryToBlock(binary: Uint8Array): EncodedBlock {
+  const degree = new Uint32Array(binary.buffer, 0, 4)[0]!
+  const headerRest = Array.from(new Uint32Array(binary.buffer, 4, degree + 3))
+  const indices = headerRest.slice(0, degree)
+  const [
+    k,
+    bytes,
+    checksum,
+  ] = headerRest.slice(degree) as [number, number, number]
+  const data = binary.slice(4 * (degree + 4))
+  return {
+    k,
+    bytes,
+    checksum,
+    indices,
+    data,
+  }
+}
+
+export function xorUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array {
+  const result = new Uint8Array(a.length)
+  for (let i = 0; i < a.length; i++) {
+    result[i] = a[i]! ^ b[i]!
+  }
+  return result
+}
+
+export function stringToUint8Array(str: string): Uint8Array {
+  const data = new Uint8Array(str.length)
+  for (let i = 0; i < str.length; i++) {
+    data[i] = str.charCodeAt(i)
+  }
+  return data
+}
diff --git a/utils/lt-codes.ts b/utils/lt-codes.ts
deleted file mode 100644
index 89c563f..0000000
--- a/utils/lt-codes.ts
+++ /dev/null
@@ -1,225 +0,0 @@
-interface EncodingMeta {
-  /**
-   * Number of original data blocks
-   */
-  k: number
-  /**
-   * Data length
-   */
-  length: number
-  /**
-   * Checksum
-   */
-  sum: number
-}
-
-interface EncodingBlock extends EncodingMeta {
-  indices: number[]
-  data: Uint32Array
-}
-
-export function blockToBinary(block: EncodingBlock): Uint32Array {
-  const { k, length, sum, indices, data } = block
-  const header = [
-    indices.length,
-    ...indices,
-    k,
-    length,
-    sum,
-  ]
-  const binary = new Uint32Array(header.length + data.length)
-  binary.set(header)
-  binary.set(data, header.length)
-  return binary
-}
-
-export function binaryToBlock(binary: Uint32Array): EncodingBlock {
-  const degree = binary[0]!
-  const indices = Array.from(binary.slice(1, degree + 1))
-  const [
-    k,
-    length,
-    sum,
-  ] = Array.from(binary.slice(degree + 1)) as [number, number, number]
-  const data = binary.slice(degree + 1 + 3)
-  return {
-    k,
-    length,
-    sum,
-    indices,
-    data,
-  }
-}
-
-// CRC32 checksum
-function checksum(data: Uint32Array): number {
-  let crc = 0xFFFFFFFF
-  for (let i = 0; i < data.length; i++) {
-    crc = crc ^ data[i]!
-    for (let j = 0; j < 8; j++) {
-      crc = crc & 1 ? (crc >>> 1) ^ 0xEDB88320 : crc >>> 1
-    }
-  }
-  return crc ^ 0xFFFFFFFF
-}
-
-// Use Ideal Soliton Distribution to select degree
-function getRandomDegree(k: number): number {
-  const probability = Math.random()
-  let degree = 1
-
-  for (let d = 2; d <= k; d++) {
-    const prob = 1 / (d * (d - 1))
-    if (probability < prob) {
-      degree = d
-      break
-    }
-  }
-
-  return degree
-}
-
-// Randomly select indices of degree number of original data blocks
-function getRandomIndices(k: number, degree: number): number[] {
-  const indices: Set<number> = new Set()
-  while (indices.size < degree) {
-    const randomIndex = Math.floor(Math.random() * k)
-    indices.add(randomIndex)
-  }
-  return Array.from(indices)
-}
-
-function xorUint32Array(a: Uint32Array, b: Uint32Array): Uint32Array {
-  const result = new Uint32Array(a.length)
-  for (let i = 0; i < a.length; i++) {
-    result[i] = a[i]! ^ b[i]!
-  }
-  return result
-}
-
-function sliceData(data: Uint32Array, blockSize: number): Uint32Array[] {
-  const blocks: Uint32Array[] = []
-  for (let i = 0; i < data.length; i += blockSize) {
-    const block = new Uint32Array(blockSize)
-    block.set(data.slice(i, i + blockSize))
-    blocks.push(block)
-  }
-  return blocks
-}
-
-export function *encodeFountain(data: Uint32Array, indiceSize: number): Generator<EncodingBlock> {
-  const sum = checksum(data)
-  const indices = sliceData(data, indiceSize)
-  const k = indices.length
-  const meta: EncodingMeta = {
-    k,
-    length: data.length,
-    sum,
-  }
-
-  while (true) {
-    const degree = getRandomDegree(k)
-    const selectedIndices = getRandomIndices(k, degree)
-    let encodedData = new Uint32Array(indiceSize)
-
-    for (const index of selectedIndices) {
-      encodedData = xorUint32Array(encodedData, indices[index]!)
-    }
-
-    yield {
-      ...meta,
-      indices: selectedIndices,
-      data: encodedData,
-    }
-  }
-}
-
-export function createDecoder(blocks?: EncodingBlock[]) {
-  return new LtDecoder(blocks)
-}
-
-export class LtDecoder {
-  public decodedData: (Uint32Array | undefined)[] = []
-  public decodedCount = 0
-  public encodedBlocks: Set<EncodingBlock> = new Set()
-  public meta: EncodingBlock = undefined!
-
-  constructor(blocks?: EncodingBlock[]) {
-    if (blocks) {
-      this.addBlock(blocks)
-    }
-  }
-
-  // Add blocks and decode them on the fly
-  addBlock(blocks: EncodingBlock[]): boolean {
-    if (!blocks.length) {
-      return false
-    }
-
-    if (!this.meta) {
-      this.meta = blocks[0]!
-    }
-
-    for (const block of blocks) {
-      this.encodedBlocks.add(block)
-    }
-
-    for (const block of this.encodedBlocks) {
-      let { data, indices } = block
-
-      for (const index of indices) {
-        if (this.decodedData[index] != null) {
-          data = xorUint32Array(data, this.decodedData[index]!)
-          indices = indices.filter(i => i !== index)
-        }
-      }
-
-      block.data = data
-      block.indices = indices
-
-      if (indices.length === 1) {
-        this.decodedData[indices[0]!] = data
-        this.decodedCount++
-        this.encodedBlocks.delete(block)
-      }
-    }
-
-    return this.decodedCount === this.meta.k
-  }
-
-  getDecoded(): Uint32Array | undefined {
-    if (this.decodedCount !== this.meta.k) {
-      return
-    }
-    if (this.decodedData.some(block => block == null)) {
-      return
-    }
-    const indiceSize = this.meta.data.length
-    const blocks = this.decodedData as Uint32Array[]
-    const decodedData = new Uint32Array(this.meta.length)
-    blocks.forEach((block, i) => {
-      const start = i * indiceSize
-      if (start + indiceSize > decodedData.length) {
-        for (let j = 0; j < decodedData.length - start; j++) {
-          decodedData[start + j] = block[j]!
-        }
-      }
-      else {
-        decodedData.set(block, i * indiceSize)
-      }
-    })
-    const sum = checksum(decodedData)
-    if (sum !== this.meta.sum) {
-      throw new Error('Checksum mismatch')
-    }
-    return decodedData
-  }
-}
-
-export function tringToUint32Array(str: string): Uint32Array {
-  const data = new Uint32Array(str.length)
-  for (let i = 0; i < str.length; i++) {
-    data[i] = str.charCodeAt(i)
-  }
-  return data
-}