[WIP] Start tinkering with IMAP

This commit is contained in:
2024-12-30 21:36:12 -05:00
parent 7791baff20
commit 063809e446
15 changed files with 3651 additions and 2 deletions

View File

@@ -0,0 +1,33 @@
import { Iterable } from './Iterable'
import {collect, Collection} from './Collection'
/**
* A basic Iterable implementation that uses an array as a backend.
* @extends Iterable
*/
export class ArrayIterable<T> extends Iterable<T> {
constructor(
/**
* Items to use for this iterable.
*/
protected items: T[],
) {
super()
}
async at(i: number): Promise<T | undefined> {
return this.items[i]
}
async range(start: number, end: number): Promise<Collection<T>> {
return collect(this.items.slice(start, end + 1))
}
async count(): Promise<number> {
return this.items.length
}
clone(): ArrayIterable<T> {
return new ArrayIterable([...this.items])
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
import {type ChunkCallback, Iterable, type MaybeIterationItem} from "./Iterable.ts";
import {collect, type Collection} from "./Collection.ts";
import {type Either, isLeft, isRight, left, type Maybe, right, unright} from "../types.ts";
export class AsyncGeneratorIterable<T> extends Iterable<T> {
private sourceIndex: number = -1
private cacheStartIndex: number = 0
private cache: T[] = []
private source?: AsyncGenerator<T, unknown, unknown>
constructor(
private sourceFactory: () => AsyncGenerator<T, unknown, unknown>,
private maxCacheSize: number = 100,
) {
super()
}
private computeCacheIndex(realIndex: number): Either<null, number> {
let i = realIndex - this.cacheStartIndex
console.log('agi cci', { i, realIndex, cSI: this.cacheStartIndex, cl: this.cache.length})
if ( i >= this.cache.length ) {
return left(null)
}
return right(i)
}
private async advanceIndexInCache(realIndex: number): Promise<void> {
if ( isRight(this.computeCacheIndex(realIndex)) ) {
return
}
console.log('aIIC needs advance')
if ( realIndex < this.cacheStartIndex ) {
this.source = undefined
}
if ( !this.source ) {
this.source = this.sourceFactory()
this.sourceIndex = -1
}
for await ( const item of this.source ) {
console.log('aIIC source item', item, this.sourceIndex, realIndex)
this.sourceIndex += 1
this.cache.push(item)
if ( this.cache.length >= this.maxCacheSize ) {
this.cache.shift()
this.cacheStartIndex += 1
}
if ( this.sourceIndex >= realIndex ) {
console.log('aIIC break')
break
}
}
}
async at(i: number): Promise<Maybe<T>> {
await this.advanceIndexInCache(i)
const cacheIndex = this.computeCacheIndex(i)
console.log('agi at', { i, cacheIndex })
if ( isRight(cacheIndex) ) {
return this.cache[unright(cacheIndex)]
}
return undefined
}
clone(): AsyncGeneratorIterable<T> {
return new AsyncGeneratorIterable(this.sourceFactory, this.maxCacheSize)
}
count(): Promise<number> {
throw new Error('cannot count!')
}
async range(start: number, end: number): Promise<Collection<T>> {
const c: Collection<T> = collect()
for ( let i = start; i <= end; i += 1 ) {
const item = await this.at(i)
if ( !item ) {
break
}
c.push(item)
}
return c
}
async all(): Promise<Collection<T>> {
const c: Collection<T> = collect()
let i = -1
while ( true ) {
i += 1
const item = await this.at(i)
if ( !item ) {
break
}
c.push(item)
}
return c
}
async next(): Promise<MaybeIterationItem<T>> {
const value = await this.at(this.index)
if ( !value ) {
return { done: true }
}
this.index += 1
return { done: false, value }
}
async seek(index: number): Promise<void> {
if ( index < 0 ) {
throw new TypeError('Cannot seek to negative index.')
}
await this.advanceIndexInCache(index)
const cacheIndex = this.computeCacheIndex(index)
if ( isLeft(cacheIndex) ) {
throw new TypeError('Cannot seek past last item.')
}
this.index = index
}
async peek(): Promise<Maybe<T>> {
return this.at(this.index + 1)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,135 @@
import {Collection} from './Collection'
export type MaybeIterationItem<T> = { done: boolean, value?: T }
export type ChunkCallback<T> = (items: Collection<T>) => any
export class StopIteration extends Error {}
/**
* Abstract class representing an iterable, lazy-loaded dataset.
* @abstract
*/
export abstract class Iterable<T> {
/**
* The current index of the iterable.
* @type number
*/
protected index = 0
/**
* Get the item of this iterable at the given index, if one exists.
* @param {number} i
* @return Promise<any|undefined>
*/
abstract at(i: number): Promise<T | undefined>
/**
* Get the collection of items in the given range of this iterable.
* @param {number} start
* @param {number} end
* @return Promise<Collection>
*/
abstract range(start: number, end: number): Promise<Collection<T>>
/**
* Count the number of items in this collection.
* @return Promise<number>
*/
abstract count(): Promise<number>
/**
* Get a copy of this iterable.
* @return Iterable
*/
abstract clone(): Iterable<T>
/**
* Return a collection of all items in this iterable.
* @return Promise<Collection>
*/
public async all(): Promise<Collection<T>> {
return this.range(0, (await this.count()) + 1)
}
/**
* Advance to the next value of this iterable.
* @return Promise<MaybeIterationItem>
*/
public async next(): Promise<MaybeIterationItem<T>> {
const i = this.index
if ( i >= await this.count() ) {
return { done: true }
}
this.index = i + 1
return { done: false,
value: await this.at(i) }
}
/**
* Chunk the iterable into the given size and call the callback passing the chunk along.
* @param {number} size
* @param {ChunkCallback} callback
* @return Promise<void>
*/
public async chunk(size: number, callback: ChunkCallback<T>): Promise<void> {
if ( size < 1 ) {
throw new Error('Chunk size must be at least 1')
}
while ( true ) {
const items = await this.range(this.index, this.index + size - 1)
this.index += items.count()
try {
await callback(items)
} catch ( error ) {
if ( error instanceof StopIteration ) {
break
} else {
throw error
}
}
if ( items.count() < size ) {
// We hit the last chunk, so bail out
break
}
}
}
/**
* Advance the iterable to the given index.
* @param {number} index
* @return Promise<void>
*/
public async seek(index: number): Promise<void> {
if ( index < 0 ) {
throw new TypeError('Cannot seek to negative index.')
} else if ( index >= await this.count() ) {
throw new TypeError('Cannot seek past last item.')
}
this.index = index
}
/**
* Peek at the next value of the iterable, without advancing.
* @return Promise<any|undefined>
*/
public async peek(): Promise<T | undefined> {
if ( this.index + 1 >= await this.count() ) {
return undefined
} else {
return this.at(this.index + 1)
}
}
/**
* Reset the iterable to the first index.
* @return Promise<any>
*/
public async reset(): Promise<void> {
this.index = 0
}
}

View File

@@ -0,0 +1,110 @@
/**
* Type representing a valid where operator.
*/
export type WhereOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' | '%' | '|' | '!' | '~' | '=' | '^'
/**
* Type associating search items with a key.
*/
export type AssociatedSearchItem = { key: any, item: any }
/**
* Type representing the result of a where.
*/
export type WhereResult = any[]
/**
* Returns true if the given item satisfies the given where clause.
* @param {AssociatedSearchItem} item
* @param {WhereOperator} operator
* @param [operand]
* @return boolean
*/
export const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand?: unknown): boolean => {
switch ( operator ) {
case '&':
if ( item.key & Number(operand) ) {
return true
}
break
case '>':
if ( item.key > (operand as any) ) {
return true
}
break
case '>=':
if ( item.key >= (operand as any) ) {
return true
}
break
case '<':
if ( item.key < (operand as any) ) {
return true
}
break
case '<=':
if ( item.key <= (operand as any) ) {
return true
}
break
case '!=':
if ( item.key !== (operand as any) ) {
return true
}
break
case '<=>':
if ( item.key === operand && typeof item.key !== 'undefined' && item.key !== null ) {
return true
}
break
case '%':
if ( item.key % Number(operand) ) {
return true
}
break
case '|':
if ( item.key | Number(operand) ) {
return true
}
break
case '!':
if ( !item.key ) {
return true
}
break
case '~':
if ( ~item.key ) {
return true
}
break
case '=':
if ( item.key === operand ) {
return true
}
break
case '^':
if ( item.key ^ Number(operand) ) {
return true
}
break
}
return false
}
/**
* Apply the given where clause to the items and return those that match.
* @param {Array<AssociatedSearchItem>} items
* @param {WhereOperator} operator
* @param [operand]
*/
export const applyWhere = (items: AssociatedSearchItem[], operator: WhereOperator, operand?: unknown): WhereResult => {
const matches: WhereResult = []
for ( const item of items ) {
if ( whereMatch(item, operator, operand) ) {
matches.push(item.item)
}
}
return matches
}