export const splitAt = (subject: string, index: number): readonly [string, string] => { if (index < 0) { return [subject, '']; } if (index > subject.length) { return [subject, '']; } return [subject.slice(0, index), subject.slice(index + 1)]; }; export function* gen__split_by_filter(subject: string, filter: string): Generator { let lastIndex = 0; for (const { 0: match, index } of subject.matchAll(new RegExp(filter, 'gmi'))) { const end = index + match.length; yield [false, subject.slice(lastIndex, index)]; yield [true, subject.slice(index, end)]; lastIndex = end; } yield [false, subject.slice(lastIndex, subject.length)]; } export function split_by_filter(subject: string, filter: string): (readonly [boolean, string])[] { return Array.from(gen__split_by_filter(subject, filter)); } type Hex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'; type EncodedChar = 't' | 'b' | 'n' | 'r' | 'f' | '\'' | '"' | `u${Hex}${Hex | ''}${Hex | ''}${Hex | ''}` const decodeRegex = /(? ({ t: '\t', b: '\b', n: '\n', r: '\r', f: '\f', "'": '\'', '"': '\"', u: String.fromCharCode(Number.parseInt(`0x${char.slice(1)}`)), }[char.charAt(0) as ('t' | 'b' | 'n' | 'r' | 'f' | '\'' | '"' | 'u')]); export const decode = (subject: string): string => subject.replace(decodeRegex, decodeReplacer); export const deepCopy = (original: T): T => { if (typeof original !== 'object' || original === null || original === undefined) { return original; } if (original instanceof Date) { return new Date(original.getTime()) as T; } if (original instanceof Array) { return original.map(item => deepCopy(item)) as T; } if (original instanceof Set) { return new Set(original.values().map(item => deepCopy(item))) as T; } if (original instanceof Map) { return new Map(original.entries().map(([key, value]) => [key, deepCopy(value)])) as T; } return Object.assign( Object.create(Object.getPrototypeOf(original)), Object.fromEntries(Object.entries(original).map(([key, value]) => [key, deepCopy(value)])) ) as T; } export enum MutarionKind { Create = 'created', Update = 'updated', Delete = 'deleted', } export type Created = { kind: MutarionKind.Create, key: string, value: any }; export type Updated = { kind: MutarionKind.Update, key: string, value: any, original: any }; export type Deleted = { kind: MutarionKind.Delete, key: string, original: any }; export type Mutation = Created | Updated | Deleted; export function* deepDiff(a: T1, b: T2, path: string[] = []): Generator { if (!isIterable(a) || !isIterable(b)) { console.error('Edge cases', a, b); return; } for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) { if (keyA === undefined && keyB !== undefined) { yield { key: path.concat(keyB.toString()).join('.'), kind: MutarionKind.Create, value: valueB }; continue; } if (keyA !== undefined && keyB === undefined) { yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete, original: valueA }; continue; } if (typeof valueA == 'object' && typeof valueB == 'object') { yield* deepDiff(valueA, valueB, path.concat(keyA!.toString())); continue; } if (valueA === valueB) { continue; } const key = path.concat(keyA!.toString()).join('.'); yield ((): Mutation => { if (valueA === null || valueA === undefined) { return { key, kind: MutarionKind.Create, value: valueB }; } if (valueB === null || valueB === undefined) { return { key, kind: MutarionKind.Delete, original: valueA }; } return { key, kind: MutarionKind.Update, value: valueB, original: valueA }; })(); } }; const isIterable = (subject: object): subject is Iterable => ['boolean', 'undefined', 'null', 'number'].includes(typeof subject) === false; const entriesOf = (subject: object): Iterable => { if (subject instanceof Array) { return subject.entries(); } if (subject instanceof Map) { return subject.entries(); } if (subject instanceof Set) { return subject.entries(); } if (subject === null || subject === undefined) { return []; } return Object.entries(subject); }; type ZippedPair = | readonly [readonly [string | number, any], readonly [string | number, any]] | readonly [readonly [undefined, undefined], readonly [string | number, any]] | readonly [readonly [string | number, any], readonly [undefined, undefined]] ; const zip = function* (a: Iterable, b: Iterable): Generator { const iterA = bufferredIterator(a); const iterB = bufferredIterator(b); const EMPTY = [undefined, undefined] as const; while (!iterA.done || !iterB.done) { // if we have a match on the keys of a and b we can simply consume and yield if (iterA.current.key === iterB.current.key) { // When we match keys it could have happened that // there are as many keys added as there are deleted, // therefor we can now flush both a and b because we are aligned yield* iterA.flush().map(entry => [entry, EMPTY] as const); yield* iterB.flush().map(entry => [EMPTY, entry] as const); yield [iterA.consume(), iterB.consume()]; } // key of a aligns with last key in buffer b // conclusion: a has key(s) that b does not else if (iterA.current.key === iterB.top.key) { yield* iterA.flush().map(entry => [entry, EMPTY] as const); yield [iterA.consume(), iterB.consume()]; } // the reverse case, key of b is aligns with the last key in buffer a // conclusion: a is missing key(s) the b does have else if (iterB.current.key === iterA.top.key) { yield* iterB.flush().map(entry => [EMPTY, entry] as const); yield [iterA.consume(), iterB.consume()]; } else if (iterA.done && !iterB.done) { yield [EMPTY, iterB.consume()]; } else if (!iterA.done && iterB.done) { yield [iterA.consume(), EMPTY]; } // Neiter of the above cases are hit. // conclusion: there still is no alignment. else { iterA.advance(); iterB.advance(); } } }; const bufferredIterator = (subject: Iterable) => { const iterator = Iterator.from(subject); const buffer: T[] = []; let done = false; const next = () => { const res = iterator.next(); done = res.done!; if (!done) { buffer.push(res.value); } }; next(); return { advance() { next(); }, consume() { const value = buffer.shift()!; this.advance(); return value; }, flush(): T[] { const entries = buffer.splice(0, buffer.length - 1); return entries; }, get done() { return done && buffer.length === 0; }, get top() { const [key = undefined, value = undefined] = buffer.at(0) ?? []; return { key, value }; }, get current() { const [key = undefined, value = undefined] = buffer.at(-1) ?? []; return { key, value }; }, }; }; export interface filter { (subject: AsyncIterableIterator, predicate: (value: T) => value is S): AsyncGenerator; (subject: AsyncIterableIterator, predicate: (value: T) => unknown): AsyncGenerator; } export const filter = async function*(subject: AsyncIterableIterator, predicate: (value: T) => value is S): AsyncGenerator { for await (const value of subject) { if (predicate(value)) { yield value; } } }; export const map = async function*(subject: AsyncIterableIterator, predicate: (value: TIn) => TResult): AsyncGenerator { for await (const value of subject) { yield predicate(value); } };