Skip to content
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
12 changes: 10 additions & 2 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ export default function CellView({ source, row, col, config }: CellProps) {
setProgress(0.75)
const df = parquetDataFrame(from, metadata)
const asyncRows = df.rows({ start: row, end: row + 1 })
if (asyncRows.length !== 1) {
if (asyncRows.length > 1 || !(0 in asyncRows)) {
throw new Error(`Expected 1 row, got ${asyncRows.length}`)
}
const asyncRow = asyncRows[0]
// Await cell data
const text = await asyncRow.cells[df.header[col]].then(stringify)
const columnName = df.header[col]
if (columnName === undefined) {
throw new Error(`Column name missing at index col=${col}`)
}
const asyncCell = asyncRow.cells[columnName]
if (asyncCell === undefined) {
throw new Error(`Cell missing at column ${columnName}`)
}
const text = await asyncCell.then(stringify)
setText(text)
setError(undefined)
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function Folder({ source, config }: FolderProps) {
setSearchQuery('')
} else if (e.key === 'Enter') {
// if there is only one result, view it
if (filtered?.length === 1) {
if (filtered?.length === 1 && 0 in filtered) {
const key = join(source.prefix, filtered[0].name)
if (key.endsWith('/')) {
// clear search because we're about to change folder
Expand Down
88 changes: 64 additions & 24 deletions src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function parseMarkdown(text: string): Token[] {
const line = lines[i]

// Skip blank lines
if (line.trim() === '') {
if (line === undefined || line.trim() === '') {
i++
continue
}
Expand All @@ -36,9 +36,13 @@ function parseMarkdown(text: string): Token[] {
if (line.startsWith('```')) {
const language = line.slice(3).trim() || undefined
i++
const codeLines = []
while (i < lines.length && !lines[i].startsWith('```')) {
codeLines.push(lines[i])
const codeLines: string[] = []
while (i < lines.length && !lines[i]?.startsWith('```')) {
const currentLine = lines[i]
if (currentLine === undefined) {
throw new Error(`Line is undefined at index ${i}.`)
}
codeLines.push(currentLine)
i++
}
i++ // skip the closing ```
Expand All @@ -48,7 +52,10 @@ function parseMarkdown(text: string): Token[] {

// Heading
const headingMatch = /^(#{1,6})\s+(.*)/.exec(line)
if (headingMatch) {
if (headingMatch !== null) {
if (!(1 in headingMatch) || !(2 in headingMatch)) {
throw new Error('Missing entries in regex matches')
}
const level = headingMatch[1].length
tokens.push({
type: 'heading',
Expand All @@ -61,7 +68,10 @@ function parseMarkdown(text: string): Token[] {

// List (ordered or unordered)
const listMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line)
if (listMatch) {
if (listMatch !== null) {
if (!(1 in listMatch) || !(2 in listMatch)) {
throw new Error('Missing entries in regex matches')
}
const baseIndent = listMatch[1].length
const ordered = /^\d+\./.test(listMatch[2])
const [items, newIndex] = parseList(lines, i, baseIndent)
Expand All @@ -72,9 +82,13 @@ function parseMarkdown(text: string): Token[] {

// Blockquote
if (line.startsWith('>')) {
const quoteLines = []
while (i < lines.length && lines[i].startsWith('>')) {
quoteLines.push(lines[i].replace(/^>\s?/, ''))
const quoteLines: string[] = []
while (i < lines.length && lines[i]?.startsWith('>')) {
const line = lines[i]
if (line === undefined) {
throw new Error(`Index ${i} not found in lines`)
}
quoteLines.push(line.replace(/^>\s?/, ''))
i++
}
tokens.push({
Expand All @@ -85,9 +99,13 @@ function parseMarkdown(text: string): Token[] {
}

// Paragraph
const paraLines = []
while (i < lines.length && lines[i].trim() !== '') {
paraLines.push(lines[i])
const paraLines: string[] = []
while (i < lines.length && lines[i]?.trim() !== '') {
const line = lines[i]
if (line === undefined) {
throw new Error(`Index ${i} not found in lines`)
}
paraLines.push(line)
i++
}
tokens.push({
Expand All @@ -104,16 +122,18 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
let i = start

while (i < lines.length) {
const line = lines[i]

// End of list if blank line or no more lines
if (lines[i].trim() === '') {
if (line === undefined || line.trim() === '') {
i++
continue
}

// This matches a new top-level bullet/number for the list
const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(lines[i])
const match = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(line)
// If we don't find a bullet/number at the same indent, break out
if (!match || match[1].length !== baseIndent) {
if (match === null || !(1 in match) || match[1].length !== baseIndent || !(3 in match)) {
break
}

Expand All @@ -130,7 +150,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
// Now parse subsequent indented lines as sub-items or sub-blocks
while (i < lines.length) {
const subline = lines[i]
if (subline.trim() === '') {
if (subline === undefined || subline.trim() === '') {
i++
continue
}
Expand All @@ -141,9 +161,13 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]
// If it’s a fenced code block, parse until closing fence
const language = trimmed.slice(3).trim() || undefined
i++
const codeLines = []
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
codeLines.push(lines[i])
const codeLines: string[] = []
while (i < lines.length && !lines[i]?.trimStart().startsWith('```')) {
const line = lines[i]
if (line === undefined) {
throw new Error(`Line is undefined at index ${i}`)
}
codeLines.push(line)
i++
}
i++ // skip the closing ```
Expand All @@ -157,7 +181,7 @@ function parseList(lines: string[], start: number, baseIndent: number): [Token[]

// Check for nested list
const sublistMatch = /^(\s*)([-*+]|\d+\.)\s+(.*)/.exec(subline)
if (sublistMatch && sublistMatch[1].length > baseIndent) {
if (sublistMatch && 1 in sublistMatch && sublistMatch[1].length > baseIndent && 2 in sublistMatch) {
const newBaseIndent = sublistMatch[1].length
const ordered = /^\d+\./.test(sublistMatch[2])
const [subItems, newIndex] = parseList(lines, i, newBaseIndent)
Expand Down Expand Up @@ -253,7 +277,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
const [linkTextTokens, consumed] = parseInlineRecursive(text.slice(i), ']')
i += consumed
if (i >= text.length || text[i] !== ']') {
tokens.push({ type: 'text', content: text[start] })
const startText = text[start]
if (startText === undefined) {
throw new Error(`Text is undefined at index ${start}`)
}
tokens.push({ type: 'text', content: startText })
continue
}
i++ // skip ']'
Expand Down Expand Up @@ -285,7 +313,11 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
i++
let code = ''
while (i < text.length && text[i] !== '`') {
code += text[i]
const character = text[i]
if (character === undefined) {
throw new Error(`Character is undefined at index ${i}`)
}
code += character
i++
}
i++
Expand All @@ -311,10 +343,18 @@ function parseInlineRecursive(text: string, stop?: string): [Token[], number] {
if (delimiter === '*') {
let j = i - 1
while (j >= 0 && text[j] === ' ') j--
const prevIsDigit = j >= 0 && /\d/.test(text[j])
const characterAtJ = text[j]
if (characterAtJ === undefined) {
throw new Error(`Character at index ${j} is undefined`)
}
const prevIsDigit = j >= 0 && /\d/.test(characterAtJ)
let k = i + 1
while (k < text.length && text[k] === ' ') k++
const nextIsDigit = k < text.length && /\d/.test(text[k])
const characterAtK = text[k]
if (characterAtK === undefined) {
throw new Error(`Character at index ${j} is undefined`)
}
const nextIsDigit = k < text.length && /\d/.test(characterAtK)
if (prevIsDigit && nextIsDigit) {
tokens.push({ type: 'text', content: delimiter })
i++
Expand Down
13 changes: 11 additions & 2 deletions src/components/viewers/CellPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ export default function CellPanel({ df, row, col, setProgress, setError, onClose
try {
setProgress(0.5)
const asyncRows = df.rows({ start: row, end: row + 1 })
if (asyncRows.length !== 1) {
if (asyncRows.length > 1 || !(0 in asyncRows)) {
throw new Error(`Expected 1 row, got ${asyncRows.length}`)
}
const asyncRow = asyncRows[0]
// Await cell data
const text = await asyncRow.cells[df.header[col]].then(stringify)
const columnName = df.header[col]
if (columnName === undefined) {
throw new Error(`Column name missing at index col=${col}`)
}
const asyncCell = asyncRow.cells[columnName]
if (asyncCell === undefined) {
throw new Error(`Cell missing at column ${columnName}`)
}
/* TODO(SL): use the same implementation of stringify, here and in Cell.tsx */
const text = await asyncCell.then(cell => stringify(cell as unknown) ?? '{}')
setText(text)
} catch (error) {
setError(error as Error)
Expand Down
6 changes: 3 additions & 3 deletions src/components/viewers/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export default function ImageView({ source, setError }: ViewerProps) {
function arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = ''
const bytes = new Uint8Array(buffer)
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
for (const byte of bytes) {
binary += String.fromCharCode(byte)
}
return btoa(binary)
}

function contentType(filename: string): string {
const ext = filename.split('.').pop() ?? ''
return contentTypes[ext] || 'image/png'
return contentTypes[ext] ?? 'image/png'
}
53 changes: 41 additions & 12 deletions src/lib/tableProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,33 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):

function fetchRowGroup(groupIndex: number) {
if (!groups[groupIndex]) {
const rowStart = groupEnds[groupIndex - 1] || 0
const rowStart = groupEnds[groupIndex - 1] ?? 0
const rowEnd = groupEnds[groupIndex]
if (rowEnd === undefined) {
throw new Error(`Missing groupEnd for groupIndex: ${groupIndex}`)
}
// Initialize with resolvable promises
for (let i = rowStart; i < rowEnd; i++) {
data[i] = resolvableRow(header)
}
parquetQueryWorker({ from, metadata, rowStart, rowEnd })
.then((groupData) => {
for (let i = rowStart; i < rowEnd; i++) {
data[i]?.index.resolve(i)
for (const [key, value] of Object.entries(
groupData[i - rowStart]
)) {
data[i]?.cells[key].resolve(value)
const dataRow = data[i]
if (dataRow === undefined) {
throw new Error(`Missing data row for index ${i}`)
}
dataRow.index.resolve(i)
const row = groupData[i - rowStart]
if (row === undefined) {
throw new Error(`Missing row in groupData for index: ${i - rowStart}`)
}
for (const [key, value] of Object.entries(row)) {
const cell = dataRow.cells[key]
if (cell === undefined) {
throw new Error(`Missing column in dataRow for column ${key}`)
}
cell.resolve(value)
}
}
})
Expand Down Expand Up @@ -68,19 +81,31 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):
// Re-assemble data in sorted order into wrapped
for (let i = start; i < end; i++) {
const index = indices[i]
if (index === undefined) {
throw new Error(`index ${i} not found in indices`)
}
const row = data[index]
if (row === undefined) {
throw new Error('Row not fetched')
}
const { cells } = row
wrapped[i - start].index.resolve(index)
const wrappedRow = wrapped[i - start]
if (wrappedRow === undefined) {
throw new Error(`Wrapped row missing at index ${i - start}`)
}
wrappedRow.index.resolve(index)
for (const key of header) {
if (key in cells) {
const cell = cells[key]
if (cell) {
// TODO(SL): should we remove this check? It makes sense only if header change
// but if so, I guess we will have more issues
cells[key]
cell
.then((value: unknown) => {
wrapped[i - start]?.cells[key].resolve(value)
const wrappedCell = wrappedRow.cells[key]
if (wrappedCell === undefined) {
throw new Error(`Wrapped cell not found for column ${key}`)
}
wrappedCell.resolve(value)
})
.catch((error: unknown) => {
console.error('Error resolving sorted row', error)
Expand All @@ -98,8 +123,12 @@ export function parquetDataFrame(from: AsyncBufferFrom, metadata: FileMetaData):
return wrapped
} else {
for (let i = 0; i < groups.length; i++) {
const groupStart = groupEnds[i - 1] || 0
if (start < groupEnds[i] && end > groupStart) {
const groupStart = groupEnds[i - 1] ?? 0
const groupEnd = groupEnds[i]
if (groupEnd === undefined) {
throw new Error(`Missing group end at index ${i}`)
}
if (start < groupEnd && end > groupStart) {
fetchRowGroup(i)
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 b'
const i = Math.floor(Math.log2(bytes) / 10)
if (i === 0) return bytes.toLocaleString('en-US') + ' b'
const size = sizes[i]
if (size === undefined) {
throw new Error(`Size not found at index ${i}`)
}
const base = bytes / Math.pow(1024, i)
return (
(base < 10 ? base.toFixed(1) : Math.round(base)).toLocaleString('en-US') +
' ' +
sizes[i]
size
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/workers/parquetWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ self.onmessage = async ({ data }: {
throw new Error('sortIndex requires all rows')
const sortColumn = await parquetQuery({ metadata, file, columns: [orderBy], compressors })
const indices = Array.from(sortColumn, (_, index) => index).sort((a, b) =>
compare<unknown>(sortColumn[a][orderBy], sortColumn[b][orderBy])
compare<unknown>(sortColumn[a]?.[orderBy], sortColumn[b]?.[orderBy])
)
postIndicesMessage({ indices, queryId })
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"noUncheckedIndexedAccess": true,

"rootDir": "src",
"outDir": "lib",
Expand Down