Last active
February 6, 2026 11:39
-
-
Save laphilosophia/57c4eb41a08cea40d79179c327607af4 to your computer and use it in GitHub Desktop.
SafeList - Defensive collection for untrusted user code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @fileoverview SafeList v4.0 - Production-Grade Immutable Chunked List | |
| * @description Zero-cast implementation, strict type safety, power-of-2 optimizations | |
| */ | |
| // ============================================================================= | |
| // BRANDED TYPES & CONFIGURATION | |
| // ============================================================================= | |
| declare const ListIdBrand: unique symbol | |
| export type ListId = string & { readonly [ListIdBrand]: true } | |
| declare const TimestampBrand: unique symbol | |
| type Timestamp = number & { readonly [TimestampBrand]: true } | |
| export type ErrorCode = | |
| | 'CAPACITY_EXCEEDED' | |
| | 'CIRCULAR_REFERENCE' | |
| | 'IMMUTABLE_MODIFICATION' | |
| | 'OPERATION_TIMEOUT' | |
| | 'VALIDATION_FAILURE' | |
| | 'INDEX_OUT_OF_BOUNDS' | |
| | 'MEMORY_PRESSURE' | |
| export interface SafeListConfig { | |
| readonly id?: ListId | |
| readonly capacity?: number | |
| /** Must be power of 2, otherwise rounded to nearest lower power of 2 */ | |
| readonly chunkSize?: number | |
| readonly timeoutMs?: number | |
| readonly allowNull?: boolean | |
| readonly strictMode?: boolean | |
| readonly enableCache?: boolean | |
| } | |
| interface NormalizedConfig { | |
| readonly id: ListId | |
| readonly capacity: number | |
| readonly chunkSize: number | |
| readonly chunkShift: number // log2(chunkSize) for bitwise operations | |
| readonly timeoutMs: number | |
| readonly allowNull: boolean | |
| readonly strictMode: boolean | |
| readonly enableCache: boolean | |
| } | |
| // ============================================================================= | |
| // ERROR HANDLING - Rich Context | |
| // ============================================================================= | |
| export class SafeListError extends Error { | |
| constructor( | |
| message: string, | |
| public readonly code: ErrorCode, | |
| public readonly context?: Readonly<{ | |
| listId?: ListId | |
| operation?: string | |
| index?: number | |
| actualIndex?: number | |
| chunkIdx?: number | |
| offset?: number | |
| chunkLength?: number | |
| expected?: number | |
| actual?: number | |
| cause?: unknown | |
| }>, | |
| ) { | |
| super(`[SafeList:${code}] ${message}`) | |
| this.name = 'SafeListError' | |
| // Satır 68-69 FIX: Runtime check ekle | |
| if (typeof (Error as any).captureStackTrace === 'function') { | |
| ;(Error as any).captureStackTrace(this, SafeListError) | |
| } | |
| } | |
| toJSON() { | |
| return { | |
| name: this.name, | |
| code: this.code, | |
| message: this.message, | |
| context: this.context, | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // RESULT MONAD - Enhanced Functional API | |
| // ============================================================================= | |
| export class Result<T, E = SafeListError> { | |
| private constructor( | |
| private readonly _ok: boolean, | |
| private readonly _value?: T, | |
| private readonly _error?: E, | |
| public readonly meta?: OperationMeta, | |
| ) {} | |
| static ok<T>(value: T, meta?: OperationMeta): Result<T, never> { | |
| return new Result(true, value, undefined, meta) as Result<T, never> | |
| } | |
| static err<E = SafeListError>(error: E, meta?: OperationMeta): Result<never, E> { | |
| return new Result(false, undefined, error, meta) as Result<never, E> | |
| } | |
| get isOk(): boolean { | |
| return this._ok | |
| } | |
| get isErr(): boolean { | |
| return !this._ok | |
| } | |
| value(): T | undefined { | |
| return this._value | |
| } | |
| error(): E | undefined { | |
| return this._error | |
| } | |
| unwrap(): T { | |
| if (!this._ok) { | |
| throw this._error instanceof Error ? this._error : new Error(String(this._error)) | |
| } | |
| return this._value! | |
| } | |
| expect(msg: string): T { | |
| if (!this._ok) { | |
| throw new SafeListError(msg, 'VALIDATION_FAILURE', { | |
| cause: this._error, | |
| }) | |
| } | |
| return this._value! | |
| } | |
| map<U>(fn: (v: T) => U): Result<U, E> { | |
| if (!this._ok) return Result.err(this._error!, this.meta) | |
| try { | |
| return Result.ok(fn(this._value!), this.meta) | |
| } catch (e) { | |
| return Result.err( | |
| new SafeListError('Map transformation failed', 'VALIDATION_FAILURE', { | |
| cause: e, | |
| }) as unknown as E, | |
| this.meta, | |
| ) | |
| } | |
| } | |
| flatMap<U>(fn: (v: T) => Result<U, E>): Result<U, E> { | |
| if (!this._ok) return Result.err(this._error!, this.meta) | |
| return fn(this._value!) | |
| } | |
| tap(fn: (v: T) => void): Result<T, E> { | |
| if (this._ok) { | |
| try { | |
| fn(this._value!) | |
| } catch { | |
| /* ignore tap errors */ | |
| } | |
| } | |
| return this | |
| } | |
| orElse(fallback: T): T { | |
| return this._ok ? this._value! : fallback | |
| } | |
| match<U>(onOk: (v: T) => U, onErr: (e: E) => U): U { | |
| return this._ok ? onOk(this._value!) : onErr(this._error!) | |
| } | |
| /** Convert to Promise for async workflows */ | |
| toPromise(): Promise<T> { | |
| return this._ok ? Promise.resolve(this._value!) : Promise.reject(this._error!) | |
| } | |
| } | |
| export interface OperationMeta { | |
| readonly durationMs: number | |
| readonly processedCount: number | |
| readonly memoryDelta?: number | |
| readonly errors?: ReadonlyArray<{ index: number; message: string }> | |
| } | |
| // ============================================================================= | |
| // INTERNAL DATA STRUCTURES - Zero Cast Design | |
| // ============================================================================= | |
| /** Internal mutable view for efficient operations */ | |
| interface ChunkNode<T> { | |
| readonly chunks: ReadonlyArray<ReadonlyArray<T>> | |
| readonly size: number | |
| readonly owner: ListId | |
| readonly frozen: boolean | |
| readonly config: NormalizedConfig | |
| } | |
| /** Mutable chunk for internal transformations */ | |
| type MutableChunk<T> = T[] | |
| /** Generator-friendly iterator state */ | |
| interface IteratorState<T> { | |
| chunks: ReadonlyArray<ReadonlyArray<T>> | |
| chunkIdx: number | |
| itemIdx: number | |
| } | |
| // ============================================================================= | |
| // UTILITY FUNCTIONS | |
| // ============================================================================= | |
| const isPowerOf2 = (n: number): boolean => n > 0 && (n & (n - 1)) === 0 | |
| const nextPowerOf2 = (n: number): number => { | |
| if (isPowerOf2(n)) return n | |
| return 1 << (32 - Math.clz32(n - 1)) | |
| } | |
| const log2 = (n: number): number => Math.log2(n) | |
| /** Secure ID generation without global counter */ | |
| const generateId = (): ListId => { | |
| const random = | |
| typeof crypto !== 'undefined' && crypto.randomUUID | |
| ? crypto.randomUUID() | |
| : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}` | |
| return `list:${random}` as ListId | |
| } | |
| const normalizeConfig = (cfg: SafeListConfig): NormalizedConfig => { | |
| const rawChunkSize = cfg.chunkSize ?? 1024 | |
| const chunkSize = Math.min(nextPowerOf2(rawChunkSize), 32768) // Max 32k chunks | |
| return { | |
| id: cfg.id ?? generateId(), | |
| capacity: Math.min(cfg.capacity ?? 100_000, 1_000_000), // Hard limit 1M | |
| chunkSize, | |
| chunkShift: log2(chunkSize), | |
| timeoutMs: cfg.timeoutMs ?? 5000, | |
| allowNull: cfg.allowNull ?? false, | |
| strictMode: cfg.strictMode ?? false, | |
| enableCache: cfg.enableCache ?? false, | |
| } | |
| } | |
| // ============================================================================= | |
| // MAIN CLASS - SafeList v4.0 | |
| // ============================================================================= | |
| export class SafeList<out T> { | |
| private readonly root: ChunkNode<T> | |
| private cachedArray: ReadonlyArray<T> | null = null | |
| private constructor(root: ChunkNode<T>) { | |
| this.root = root | |
| } | |
| // =========================================================================== | |
| // FACTORY METHODS | |
| // =========================================================================== | |
| static create<T>(cfg: SafeListConfig = {}): SafeList<T> { | |
| const config = normalizeConfig(cfg) | |
| if (config.capacity <= 0) { | |
| throw new SafeListError('Capacity must be positive', 'CAPACITY_EXCEEDED') | |
| } | |
| return new SafeList({ | |
| chunks: Object.freeze([]), | |
| size: 0, | |
| owner: config.id, | |
| frozen: false, | |
| config, | |
| }) | |
| } | |
| static fromArray<T>(items: readonly T[], cfg: SafeListConfig = {}): Result<SafeList<T>> { | |
| const startTime = performance.now() | |
| const config = normalizeConfig(cfg) | |
| if (items.length > config.capacity) { | |
| return Result.err( | |
| new SafeListError( | |
| `Batch size ${items.length} exceeds capacity ${config.capacity}`, | |
| 'CAPACITY_EXCEEDED', | |
| { expected: config.capacity, actual: items.length }, | |
| ), | |
| ) | |
| } | |
| const chunks: MutableChunk<T>[] = [] | |
| const { chunkSize, id } = config | |
| // Pre-allocate chunks for better memory locality | |
| const numChunks = Math.ceil(items.length / chunkSize) | |
| chunks.length = numChunks | |
| for (let i = 0; i < numChunks; i++) { | |
| const start = i * chunkSize | |
| const end = Math.min(start + chunkSize, items.length) | |
| chunks[i] = items.slice(start, end) | |
| } | |
| const duration = performance.now() - startTime | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(chunks.map((c) => Object.freeze(c))), | |
| size: items.length, | |
| owner: id, | |
| frozen: false, | |
| config, | |
| }), | |
| { | |
| durationMs: duration, | |
| processedCount: items.length, | |
| }, | |
| ) | |
| } | |
| static of<T>(...items: T[]): Result<SafeList<T>> { | |
| return SafeList.fromArray(items) | |
| } | |
| static empty<T>(): SafeList<T> { | |
| return SafeList.create<T>() | |
| } | |
| // =========================================================================== | |
| // ACCESSORS & PROPERTIES | |
| // =========================================================================== | |
| get size(): number { | |
| return this.root.size | |
| } | |
| get id(): ListId { | |
| return this.root.owner | |
| } | |
| get isFrozen(): boolean { | |
| return this.root.frozen | |
| } | |
| get capacity(): number { | |
| return this.root.config.capacity | |
| } | |
| /** Bitwise optimized index calculation */ | |
| private locate(index: number): { chunkIdx: number; offset: number } { | |
| return { | |
| chunkIdx: index >>> this.root.config.chunkShift, | |
| offset: index & (this.root.config.chunkSize - 1), // Faster than modulo for power-of-2 | |
| } | |
| } | |
| // =========================================================================== | |
| // SAFE ACCESS | |
| // =========================================================================== | |
| at(index: number): Result<T> { | |
| if (!Number.isFinite(index)) { | |
| return Result.err( | |
| new SafeListError('Index must be finite number', 'VALIDATION_FAILURE', { actual: index }), | |
| ) | |
| } | |
| const len = this.root.size | |
| const actualIndex = index < 0 ? len + index : index | |
| if (actualIndex < 0 || actualIndex >= len) { | |
| return Result.err( | |
| new SafeListError(`Index ${index} out of bounds`, 'INDEX_OUT_OF_BOUNDS', { | |
| index, | |
| actualIndex, | |
| expected: len, | |
| listId: this.root.owner, | |
| }), | |
| ) | |
| } | |
| const { chunkIdx, offset } = this.locate(actualIndex) | |
| const chunk = this.root.chunks[chunkIdx] | |
| if (!chunk || offset >= chunk.length) { | |
| return Result.err( | |
| new SafeListError('Internal chunk structure corrupted', 'VALIDATION_FAILURE', { | |
| chunkIdx, | |
| offset, | |
| chunkLength: chunk?.length, | |
| }), | |
| ) | |
| } | |
| return Result.ok(chunk[offset]) | |
| } | |
| /** Unsafe but fast access - returns undefined on bounds violation */ | |
| get(index: number): T | undefined { | |
| const len = this.root.size | |
| const i = index < 0 ? len + index : index | |
| if (i < 0 || i >= len) return undefined | |
| const { chunkIdx, offset } = this.locate(i) | |
| return this.root.chunks[chunkIdx]?.[offset] | |
| } | |
| first(): T | undefined { | |
| return this.get(0) | |
| } | |
| last(): T | undefined { | |
| return this.get(this.root.size - 1) | |
| } | |
| // =========================================================================== | |
| // MODIFIERS (Immutable) | |
| // =========================================================================== | |
| add(item: T): Result<SafeList<T>> { | |
| if (this.root.frozen) { | |
| return Result.err( | |
| new SafeListError('Cannot modify frozen list', 'IMMUTABLE_MODIFICATION', { | |
| listId: this.root.owner, | |
| }), | |
| ) | |
| } | |
| if (!this.root.config.allowNull && item == null) { | |
| return Result.err( | |
| new SafeListError('Null values not allowed', 'VALIDATION_FAILURE', { | |
| listId: this.root.owner, | |
| }), | |
| ) | |
| } | |
| if (this.root.size >= this.root.config.capacity) { | |
| return Result.err( | |
| new SafeListError(`Capacity ${this.root.config.capacity} exceeded`, 'CAPACITY_EXCEEDED', { | |
| actual: this.root.size + 1, | |
| expected: this.root.config.capacity, | |
| }), | |
| ) | |
| } | |
| const startTime = performance.now() | |
| const { chunks, size, config } = this.root | |
| const lastChunk = chunks[chunks.length - 1] as MutableChunk<T> | undefined | |
| let newChunks: MutableChunk<T>[] | |
| if (!lastChunk || lastChunk.length >= config.chunkSize) { | |
| newChunks = [...(chunks as MutableChunk<T>[]), [item]] | |
| } else { | |
| // Copy-on-write: Mutate only the last chunk | |
| newChunks = chunks.slice(0, -1) as MutableChunk<T>[] | |
| newChunks.push([...lastChunk, item]) | |
| } | |
| const duration = performance.now() - startTime | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size: size + 1, | |
| owner: config.id, | |
| frozen: false, | |
| config, | |
| }), | |
| { durationMs: duration, processedCount: 1 }, | |
| ) | |
| } | |
| addMany(items: readonly T[], timeoutMs?: number): Result<SafeList<T>> { | |
| if (this.root.frozen) { | |
| return Result.err(new SafeListError('Cannot modify frozen list', 'IMMUTABLE_MODIFICATION')) | |
| } | |
| const startTime = performance.now() | |
| const deadline = startTime + (timeoutMs ?? this.root.config.timeoutMs) | |
| const { capacity, allowNull, strictMode, chunkSize, id } = this.root.config | |
| if (items.length === 0) { | |
| return Result.ok(this, { durationMs: 0, processedCount: 0 }) | |
| } | |
| const remainingCapacity = capacity - this.root.size | |
| if (items.length > remainingCapacity) { | |
| return Result.err( | |
| new SafeListError( | |
| `Cannot add ${items.length} items, remaining capacity: ${remainingCapacity}`, | |
| 'CAPACITY_EXCEEDED', | |
| { actual: items.length, expected: remainingCapacity }, | |
| ), | |
| ) | |
| } | |
| // Validation pass with timeout checks | |
| const validItems: T[] = [] | |
| const errors: Array<{ index: number; message: string }> = [] | |
| for (let i = 0; i < items.length; i++) { | |
| // Check timeout every 128 items (bitwise & 127) | |
| if ((i & 127) === 0 && performance.now() > deadline) { | |
| return Result.err(new SafeListError('Batch validation timeout', 'OPERATION_TIMEOUT'), { | |
| durationMs: timeoutMs ?? this.root.config.timeoutMs, | |
| processedCount: i, | |
| }) | |
| } | |
| const item = items[i] | |
| if (!allowNull && item == null) { | |
| const error = { index: i, message: 'Null value rejected' } | |
| if (strictMode) { | |
| return Result.err( | |
| new SafeListError( | |
| `Null value at index ${i} rejected in strict mode`, | |
| 'VALIDATION_FAILURE', | |
| ), | |
| { durationMs: performance.now() - startTime, processedCount: i, errors: [error] }, | |
| ) | |
| } | |
| errors.push(error) | |
| continue | |
| } | |
| validItems.push(item) | |
| } | |
| if (validItems.length === 0) { | |
| return Result.ok(this, { | |
| durationMs: performance.now() - startTime, | |
| processedCount: 0, | |
| errors: errors.length ? errors : undefined, | |
| }) | |
| } | |
| // Efficient chunk construction | |
| const currentChunks = this.root.chunks as MutableChunk<T>[] | |
| const newChunks: MutableChunk<T>[] = [...currentChunks] | |
| let currentChunk: MutableChunk<T> | undefined = newChunks[newChunks.length - 1] | |
| if (!currentChunk || currentChunk.length >= chunkSize) { | |
| currentChunk = [] | |
| newChunks.push(currentChunk) | |
| } | |
| for (const item of validItems) { | |
| if (currentChunk.length >= chunkSize) { | |
| currentChunk = [] | |
| newChunks.push(currentChunk) | |
| } | |
| currentChunk.push(item) | |
| } | |
| const duration = performance.now() - startTime | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size: this.root.size + validItems.length, | |
| owner: id, | |
| frozen: false, | |
| config: this.root.config, | |
| }), | |
| { | |
| durationMs: duration, | |
| processedCount: validItems.length, | |
| errors: errors.length ? errors : undefined, | |
| }, | |
| ) | |
| } | |
| // =========================================================================== | |
| // TRANSFORMATIONS | |
| // =========================================================================== | |
| map<U>(fn: (item: T, index: number) => U, timeoutMs?: number): Result<SafeList<U>> { | |
| const startTime = performance.now() | |
| const limit = timeoutMs ?? this.root.config.timeoutMs | |
| const deadline = startTime + limit | |
| const { chunks, config, size } = this.root | |
| const { chunkSize, chunkShift } = config | |
| const newChunks: MutableChunk<U>[] = [] | |
| let processed = 0 | |
| for (let c = 0; c < chunks.length; c++) { | |
| const chunk = chunks[c] | |
| const baseIndex = c << chunkShift // c * chunkSize via bitwise | |
| const newChunk: MutableChunk<U> = new Array(chunk.length) | |
| for (let i = 0; i < chunk.length; i++) { | |
| // Check timeout every 64 items | |
| if ((processed & 63) === 0 && performance.now() > deadline) { | |
| return Result.err( | |
| new SafeListError( | |
| `Map timeout at chunk ${c}, index ${baseIndex + i}`, | |
| 'OPERATION_TIMEOUT', | |
| ), | |
| { durationMs: limit, processedCount: processed }, | |
| ) | |
| } | |
| try { | |
| newChunk[i] = fn(chunk[i], baseIndex + i) | |
| } catch (e) { | |
| return Result.err( | |
| new SafeListError( | |
| `Map failed at index ${baseIndex + i}: ${e instanceof Error ? e.message : 'Unknown'}`, | |
| 'VALIDATION_FAILURE', | |
| { index: baseIndex + i, cause: e }, | |
| ), | |
| { durationMs: performance.now() - startTime, processedCount: processed }, | |
| ) | |
| } | |
| processed++ | |
| } | |
| newChunks.push(newChunk) | |
| } | |
| const newId = generateId() | |
| const duration = performance.now() - startTime | |
| return Result.ok( | |
| new SafeList<U>({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size, | |
| owner: newId, | |
| frozen: false, | |
| config: { ...config, id: newId }, | |
| }), | |
| { durationMs: duration, processedCount: processed }, | |
| ) | |
| } | |
| filter(predicate: (item: T, index: number) => boolean, timeoutMs?: number): Result<SafeList<T>> { | |
| const startTime = performance.now() | |
| const limit = timeoutMs ?? this.root.config.timeoutMs | |
| const deadline = startTime + limit | |
| const { strictMode, chunkSize, chunkShift } = this.root.config | |
| const newChunks: MutableChunk<T>[] = [] | |
| let currentChunk: MutableChunk<T> = [] | |
| let processed = 0 | |
| let totalSize = 0 | |
| for (let c = 0; c < this.root.chunks.length; c++) { | |
| const chunk = this.root.chunks[c] | |
| const baseIndex = c << chunkShift | |
| for (let i = 0; i < chunk.length; i++) { | |
| if ((processed & 63) === 0 && performance.now() > deadline) { | |
| return Result.err(new SafeListError('Filter operation timeout', 'OPERATION_TIMEOUT'), { | |
| durationMs: limit, | |
| processedCount: processed, | |
| }) | |
| } | |
| try { | |
| if (predicate(chunk[i], baseIndex + i)) { | |
| if (currentChunk.length >= chunkSize) { | |
| newChunks.push(currentChunk) | |
| currentChunk = [] | |
| } | |
| currentChunk.push(chunk[i]) | |
| totalSize++ | |
| } | |
| } catch (e) { | |
| const errorMsg = `Filter predicate error at index ${baseIndex + i}` | |
| if (strictMode) { | |
| return Result.err( | |
| new SafeListError(errorMsg, 'VALIDATION_FAILURE', { index: baseIndex + i, cause: e }), | |
| { durationMs: performance.now() - startTime, processedCount: processed }, | |
| ) | |
| } | |
| console.warn(errorMsg, e) | |
| } | |
| processed++ | |
| } | |
| } | |
| if (currentChunk.length > 0) { | |
| newChunks.push(currentChunk) | |
| } | |
| const newId = generateId() | |
| const duration = performance.now() - startTime | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size: totalSize, | |
| owner: newId, | |
| frozen: false, | |
| config: { ...this.root.config, id: newId }, | |
| }), | |
| { durationMs: duration, processedCount: totalSize }, | |
| ) | |
| } | |
| slice(start?: number, end?: number): Result<SafeList<T>> { | |
| const len = this.root.size | |
| const s = start == null ? 0 : start < 0 ? Math.max(0, len + start) : start | |
| const e = end == null ? len : end < 0 ? Math.max(0, len + end) : Math.min(end, len) | |
| if (s >= e || s >= len) { | |
| return Result.ok(SafeList.create<T>(this.root.config)) | |
| } | |
| const { chunkSize, chunkShift } = this.root.config | |
| const startChunk = s >>> chunkShift | |
| const endChunk = (e - 1) >>> chunkShift | |
| const newChunks: MutableChunk<T>[] = [] | |
| for (let i = startChunk; i <= endChunk; i++) { | |
| const chunk = this.root.chunks[i] as MutableChunk<T> | |
| const isFirst = i === startChunk | |
| const isLast = i === endChunk | |
| const chunkStart = isFirst ? s & (chunkSize - 1) : 0 | |
| const chunkEnd = isLast ? ((e - 1) & (chunkSize - 1)) + 1 : chunk.length | |
| newChunks.push(chunk.slice(chunkStart, chunkEnd)) | |
| } | |
| const newId = generateId() | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size: e - s, | |
| owner: newId, | |
| frozen: this.root.frozen, | |
| config: { ...this.root.config, id: newId }, | |
| }), | |
| { durationMs: 0, processedCount: e - s }, | |
| ) | |
| } | |
| // =========================================================================== | |
| // CONCATENATION - Efficient multi-list operations | |
| // =========================================================================== | |
| concat(other: SafeList<T>): Result<SafeList<T>> { | |
| if (this.root.size + other.root.size > this.root.config.capacity) { | |
| return Result.err( | |
| new SafeListError('Concatenated size exceeds capacity', 'CAPACITY_EXCEEDED', { | |
| actual: this.root.size + other.root.size, | |
| expected: this.root.config.capacity, | |
| }), | |
| ) | |
| } | |
| // Fast path: If either list is empty | |
| if (other.root.size === 0) return Result.ok(this) | |
| if (this.root.size === 0) return Result.ok(other) | |
| const newChunks = [ | |
| ...(this.root.chunks as MutableChunk<T>[]), | |
| ...(other.root.chunks as MutableChunk<T>[]), | |
| ] | |
| // Flatten if last chunk of first and first chunk of second can fit together | |
| const lastThis = newChunks[newChunks.length - 2] | |
| const firstOther = newChunks[newChunks.length - 1] | |
| if ( | |
| lastThis && | |
| firstOther && | |
| lastThis.length + firstOther.length <= this.root.config.chunkSize | |
| ) { | |
| newChunks[newChunks.length - 2] = [...lastThis, ...firstOther] | |
| newChunks.pop() | |
| } | |
| const newId = generateId() | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(newChunks.map((c) => Object.freeze(c))), | |
| size: this.root.size + other.root.size, | |
| owner: newId, | |
| frozen: false, | |
| config: { ...this.root.config, id: newId }, | |
| }), | |
| { durationMs: 0, processedCount: other.root.size }, | |
| ) | |
| } | |
| // =========================================================================== | |
| // QUERY & SEARCH | |
| // =========================================================================== | |
| find(predicate: (item: T) => boolean, timeoutMs?: number): T | undefined { | |
| const limit = timeoutMs ?? this.root.config.timeoutMs | |
| const deadline = performance.now() + limit | |
| let count = 0 | |
| for (const chunk of this.root.chunks) { | |
| for (const item of chunk) { | |
| if ((count++ & 63) === 0 && performance.now() > deadline) { | |
| throw new SafeListError('Find operation timeout', 'OPERATION_TIMEOUT') | |
| } | |
| try { | |
| if (predicate(item)) return item | |
| } catch { | |
| continue | |
| } | |
| } | |
| } | |
| return undefined | |
| } | |
| findIndex(predicate: (item: T) => boolean, timeoutMs?: number): number { | |
| const limit = timeoutMs ?? this.root.config.timeoutMs | |
| const deadline = performance.now() + limit | |
| const { chunkShift } = this.root.config | |
| let globalIdx = 0 | |
| for (let c = 0; c < this.root.chunks.length; c++) { | |
| const chunk = this.root.chunks[c] | |
| for (let i = 0; i < chunk.length; i++) { | |
| if ((globalIdx & 63) === 0 && performance.now() > deadline) { | |
| return -1 // Timeout returns -1 instead of throwing for easier handling | |
| } | |
| try { | |
| if (predicate(chunk[i])) return (c << chunkShift) + i | |
| } catch { | |
| // Continue search | |
| } | |
| globalIdx++ | |
| } | |
| } | |
| return -1 | |
| } | |
| includes(item: T, fromIndex = 0): boolean { | |
| return this.findIndex((x) => Object.is(x, item)) !== -1 | |
| } | |
| some(predicate: (item: T) => boolean, timeoutMs?: number): boolean { | |
| return this.find(predicate, timeoutMs) !== undefined | |
| } | |
| every(predicate: (item: T) => boolean, timeoutMs?: number): boolean { | |
| try { | |
| return !this.find((item) => !predicate(item), timeoutMs) | |
| } catch { | |
| return false | |
| } | |
| } | |
| // =========================================================================== | |
| // ITERATION - Optimized for performance | |
| // =========================================================================== | |
| *[Symbol.iterator](): Iterator<T> { | |
| for (const chunk of this.root.chunks) { | |
| for (const item of chunk) { | |
| yield item | |
| } | |
| } | |
| } | |
| /** Manual iterator for better control than generator */ | |
| iterate(): { next: () => IteratorResult<T>; [Symbol.iterator]: () => IterableIterator<T> } { | |
| const state: IteratorState<T> = { | |
| chunks: this.root.chunks, | |
| chunkIdx: 0, | |
| itemIdx: 0, | |
| } | |
| const next = (): IteratorResult<T> => { | |
| if (state.chunkIdx >= state.chunks.length) { | |
| return { done: true, value: undefined } | |
| } | |
| const chunk = state.chunks[state.chunkIdx] | |
| if (state.itemIdx >= chunk.length) { | |
| state.chunkIdx++ | |
| state.itemIdx = 0 | |
| return next() | |
| } | |
| return { done: false, value: chunk[state.itemIdx++] } | |
| } | |
| return { | |
| next, | |
| [Symbol.iterator]: function* () { | |
| while (true) { | |
| const result = this.next() | |
| if (result.done) break | |
| yield result.value | |
| } | |
| }, | |
| } | |
| } | |
| forEach(fn: (item: T, index: number) => void): void { | |
| let idx = 0 | |
| const { strictMode } = this.root.config | |
| for (const chunk of this.root.chunks) { | |
| for (const item of chunk) { | |
| try { | |
| fn(item, idx++) | |
| } catch (e) { | |
| if (strictMode) { | |
| throw new SafeListError(`forEach error at index ${idx}`, 'VALIDATION_FAILURE', { | |
| index: idx, | |
| cause: e, | |
| }) | |
| } | |
| console.error(`forEach error at ${idx}:`, e) | |
| } | |
| } | |
| } | |
| } | |
| // =========================================================================== | |
| // CONVERSION & SERIALIZATION | |
| // =========================================================================== | |
| toArray(): ReadonlyArray<T> { | |
| // Return cached version if available and frozen | |
| if (this.cachedArray && this.root.frozen) { | |
| return this.cachedArray | |
| } | |
| const result: T[] = new Array(this.root.size) | |
| let idx = 0 | |
| // Fast copy using chunk boundaries | |
| for (const chunk of this.root.chunks) { | |
| for (let i = 0; i < chunk.length; i++) { | |
| result[idx++] = chunk[i] | |
| } | |
| } | |
| const frozen = Object.freeze(result) | |
| // Cache if frozen and caching enabled | |
| if (this.root.frozen && this.root.config.enableCache) { | |
| this.cachedArray = frozen | |
| } | |
| return frozen | |
| } | |
| toJSON(): unknown { | |
| try { | |
| return this.toArray() | |
| } catch (e) { | |
| if (e instanceof Error && e.message.includes('circular')) { | |
| throw new SafeListError( | |
| 'Circular reference detected during serialization', | |
| 'CIRCULAR_REFERENCE', | |
| { listId: this.root.owner }, | |
| ) | |
| } | |
| throw e | |
| } | |
| } | |
| // =========================================================================== | |
| // IMMUTABILITY UTILITIES | |
| // =========================================================================== | |
| freeze(): SafeList<T> { | |
| if (this.root.frozen) return this | |
| const frozenChunks = this.root.chunks.map((chunk) => | |
| Object.isFrozen(chunk) ? chunk : Object.freeze([...chunk]), | |
| ) | |
| return new SafeList({ | |
| ...this.root, | |
| chunks: Object.freeze(frozenChunks), | |
| frozen: true, | |
| }) | |
| } | |
| clone(deep = false): Result<SafeList<T>> { | |
| const newId = generateId() | |
| const startTime = performance.now() | |
| if (!deep) { | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze( | |
| [...(this.root.chunks as MutableChunk<T>[])].map((c) => Object.freeze([...c])), | |
| ), | |
| size: this.root.size, | |
| owner: newId, | |
| frozen: false, | |
| config: { ...this.root.config, id: newId }, | |
| }), | |
| { durationMs: performance.now() - startTime, processedCount: this.root.size }, | |
| ) | |
| } | |
| try { | |
| let clonedChunks: MutableChunk<T>[] | |
| if (typeof structuredClone === 'function') { | |
| clonedChunks = structuredClone(this.root.chunks as MutableChunk<T>[]) | |
| } else { | |
| // Fallback with circular reference protection | |
| const cache = new WeakMap() | |
| clonedChunks = JSON.parse( | |
| JSON.stringify(this.root.chunks, (key, value) => { | |
| if (typeof value === 'object' && value !== null) { | |
| if (cache.has(value)) { | |
| throw new SafeListError('Circular reference detected', 'CIRCULAR_REFERENCE') | |
| } | |
| cache.set(value, true) | |
| } | |
| return value | |
| }), | |
| ) | |
| } | |
| return Result.ok( | |
| new SafeList({ | |
| chunks: Object.freeze(clonedChunks.map((c) => Object.freeze(c))), | |
| size: this.root.size, | |
| owner: newId, | |
| frozen: false, | |
| config: { ...this.root.config, id: newId }, | |
| }), | |
| { durationMs: performance.now() - startTime, processedCount: this.root.size }, | |
| ) | |
| } catch (e) { | |
| if (e instanceof SafeListError) return Result.err(e) | |
| return Result.err(new SafeListError('Deep clone failed', 'CIRCULAR_REFERENCE', { cause: e })) | |
| } | |
| } | |
| // =========================================================================== | |
| // TYPE GUARDS & UTILITIES | |
| // =========================================================================== | |
| static isSafeList<T>(obj: unknown): obj is SafeList<T> { | |
| return obj instanceof SafeList | |
| } | |
| toString(): string { | |
| return `SafeList(id=${this.root.owner}, size=${this.root.size}, frozen=${this.root.frozen})` | |
| } | |
| [Symbol.for('nodejs.util.inspect.custom')](): string { | |
| return this.toString() | |
| } | |
| } | |
| // ============================================================================= | |
| // EXPORT FACTORY | |
| // ============================================================================= | |
| export const createList = SafeList.create | |
| export const listOf = SafeList.of | |
| export const emptyList = SafeList.empty |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How Not To Be A Villainous Developer