working on fixing/reimplementing save command now that the mutations logic is more complete
This commit is contained in:
parent
6ed9c74862
commit
992bb77d2f
12 changed files with 239 additions and 58 deletions
|
@ -30,4 +30,9 @@ export default defineConfig({
|
|||
compact: true,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
prerender: {
|
||||
crawlLinks: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,5 +26,15 @@
|
|||
"sizes": "2092x1295",
|
||||
"form_factor": "wide"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/edit",
|
||||
"accept": {
|
||||
"text/*": [
|
||||
".json"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -78,4 +78,12 @@ h1 {
|
|||
|
||||
p {
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding-inline: var(--padding-s);
|
||||
background-color: var(--surface-3);
|
||||
border: 1px solid var(--surface-5);
|
||||
border-radius: var(--radii-m);
|
||||
}
|
|
@ -55,7 +55,7 @@ interface TreeContextType {
|
|||
const TreeContext = createContext<TreeContextType>();
|
||||
|
||||
export const Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element], open?: TreeContextType['open'] }> = (props) => {
|
||||
const [selection, setSelection] = createSignal<object[]>([]);
|
||||
const [, setSelection] = createSignal<object[]>([]);
|
||||
|
||||
const context = {
|
||||
open: props.open ?? (() => { }),
|
||||
|
|
34
src/components/prompt.module.css
Normal file
34
src/components/prompt.module.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
.prompt {
|
||||
display: grid;
|
||||
gap: var(--padding-m);
|
||||
padding: var(--padding-m);
|
||||
background-color: var(--surface-1);
|
||||
color: var(--text-2);
|
||||
border: 1px solid var(--surface-5);
|
||||
border-radius: var(--radii-m);
|
||||
|
||||
&:not(&[open]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[open]::backdrop {
|
||||
background-color: color(from var(--surface-1) xyz x y z / .3);
|
||||
backdrop-filter: blur(.25em);
|
||||
}
|
||||
|
||||
& > form {
|
||||
display: contents;
|
||||
|
||||
& > header > .title {
|
||||
font-size: var(--text-l);
|
||||
color: var(--text-1);
|
||||
}
|
||||
|
||||
& > footer {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: end;
|
||||
gap: var(--padding-m);
|
||||
}
|
||||
}
|
||||
}
|
75
src/components/prompt.tsx
Normal file
75
src/components/prompt.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { createEffect, createSignal, createUniqueId, JSX, onMount, ParentComponent, Show } from "solid-js";
|
||||
import css from './prompt.module.css';
|
||||
|
||||
export interface PromptApi {
|
||||
showModal(): Promise<FormData | undefined>;
|
||||
};
|
||||
|
||||
class PromptCanceledError extends Error { }
|
||||
|
||||
export const Prompt: ParentComponent<{ api: (api: PromptApi) => any, title?: string, description?: string | JSX.Element }> = (props) => {
|
||||
const [dialog, setDialog] = createSignal<HTMLDialogElement>();
|
||||
const [form, setForm] = createSignal<HTMLFormElement>();
|
||||
const [resolvers, setResolvers] = createSignal<[(...args: any[]) => any, (...args: any[]) => any]>();
|
||||
const submitId = createUniqueId();
|
||||
const cancelId = createUniqueId();
|
||||
|
||||
const api = {
|
||||
async showModal(): Promise<FormData | undefined> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
|
||||
setResolvers([resolve, reject]);
|
||||
|
||||
dialog()!.showModal();
|
||||
|
||||
try {
|
||||
await promise;
|
||||
|
||||
return new FormData(form());
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof PromptCanceledError)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
dialog()!.close();
|
||||
setResolvers(undefined);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const onSubmit = (e: SubmitEvent) => {
|
||||
resolvers()?.[0]();
|
||||
};
|
||||
|
||||
const onCancel = (e: Event) => {
|
||||
resolvers()?.[1](new PromptCanceledError());
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
props.api(api);
|
||||
});
|
||||
|
||||
return <dialog class={css.prompt} ref={setDialog} onsubmit={onSubmit} onCancel={onCancel} onReset={onCancel}>
|
||||
<form method="dialog" ref={setForm}>
|
||||
<Show when={props.title || props.description}>
|
||||
<header>
|
||||
<Show when={props.title}>{
|
||||
title => <b class={css.title}>{title()}</b>
|
||||
}</Show>
|
||||
|
||||
<Show when={props.description}>{
|
||||
description => <p>{description()}</p>
|
||||
}</Show>
|
||||
</header>
|
||||
</Show>
|
||||
|
||||
<main>{props.children}</main>
|
||||
|
||||
<footer>
|
||||
<button id={submitId} type="submit">Ok</button>
|
||||
<button id={cancelId} type="reset">Cancel</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>;
|
||||
};
|
|
@ -10,7 +10,7 @@ export default createHandler(() => (
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { createStore, produce, reconcile, unwrap } from "solid-js/store";
|
||||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { createStore, produce, unwrap } from "solid-js/store";
|
||||
import { SelectionProvider, useSelection, selectable } from "../selectable";
|
||||
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||
import css from './grid.module.css';
|
||||
import diff from "microdiff";
|
||||
|
||||
selectable // prevents removal of import
|
||||
|
||||
|
@ -37,7 +36,7 @@ const GridContext = createContext<GridContextType>();
|
|||
const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some(v => typeof v === 'string');
|
||||
const useGrid = () => useContext(GridContext)!;
|
||||
|
||||
const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||
export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => {
|
||||
const [selection, setSelection] = createSignal<SelectionItem[]>([]);
|
||||
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, snapshot: Rows, numberOfRows: number }>({
|
||||
rows: {},
|
||||
|
@ -45,7 +44,12 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
|||
numberOfRows: 0,
|
||||
});
|
||||
|
||||
const mutations = createMemo(() => deepDiff(state.snapshot, state.rows).toArray());
|
||||
const mutations = createMemo(() => {
|
||||
// enumerate all values to make sure the memo is recalculated on any change
|
||||
Object.values(state.rows).map(entry => Object.values(entry));
|
||||
|
||||
return deepDiff(state.snapshot, state.rows).toArray();
|
||||
});
|
||||
const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const)));
|
||||
|
||||
createEffect(() => {
|
||||
|
@ -57,10 +61,6 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
|||
setState('numberOfRows', Object.keys(state.rows).length);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(mutations());
|
||||
});
|
||||
|
||||
const ctx: GridContextType = {
|
||||
rows,
|
||||
mutations,
|
||||
|
@ -82,7 +82,7 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
|||
|
||||
insert(prop: string) {
|
||||
setState('rows', produce(rows => {
|
||||
rows[prop] = { en: '' };
|
||||
rows[prop] = Object.fromEntries(props.columns.slice(1).map(lang => [lang, '']));
|
||||
|
||||
return rows
|
||||
}))
|
||||
|
@ -91,15 +91,16 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
|||
|
||||
return <GridContext.Provider value={ctx}>
|
||||
<SelectionProvider selection={setSelection} multiSelect>
|
||||
{props.children}
|
||||
<Api api={props.api} />
|
||||
|
||||
<_Grid class={props.class} columns={props.columns} rows={rows()} />
|
||||
</SelectionProvider>
|
||||
</GridContext.Provider>;
|
||||
};
|
||||
|
||||
export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => {
|
||||
const _Grid: Component<{ class?: string, columns: string[], rows: Record<string, Record<string, string>> }> = (props) => {
|
||||
const columnCount = createMemo(() => props.columns.length - 1);
|
||||
const root = createMemo<Entry>(() => props.rows
|
||||
?.entries()
|
||||
const root = createMemo<Entry>(() => Object.entries(props.rows)
|
||||
.reduce((aggregate, [key, value]) => {
|
||||
let obj: any = aggregate;
|
||||
const parts = key.split('.');
|
||||
|
@ -121,15 +122,11 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap
|
|||
}, {}));
|
||||
|
||||
return <section class={`${css.table} ${props.class}`} style={{ '--columns': columnCount() }}>
|
||||
<GridProvider rows={props.rows}>
|
||||
<Api api={props.api} />
|
||||
<Head headers={props.columns} />
|
||||
|
||||
<Head headers={props.columns} />
|
||||
|
||||
<main class={css.main}>
|
||||
<Row entry={root()} />
|
||||
</main>
|
||||
</GridProvider>
|
||||
<main class={css.main}>
|
||||
<Row entry={root()} />
|
||||
</main>
|
||||
</section>
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ interface InternalFilesContextType {
|
|||
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any): void;
|
||||
set(key: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
||||
get(key: string): Promise<FileSystemDirectoryHandle | undefined>;
|
||||
remove(key: string): Promise<void>;
|
||||
remove(...keys: string[]): Promise<void>;
|
||||
keys(): Promise<string[]>;
|
||||
entries(): Promise<FileEntity[]>;
|
||||
list(): Promise<FileSystemDirectoryHandle[]>;
|
||||
|
@ -65,8 +65,8 @@ const clientContext = (): InternalFilesContextType => {
|
|||
async get(key: string) {
|
||||
return (await db.files.get(key))?.handle;
|
||||
},
|
||||
async remove(key: string) {
|
||||
return (await db.files.delete(key));
|
||||
async remove(...keys: string[]) {
|
||||
await Promise.all(keys.map(key => db.files.delete(key)));
|
||||
},
|
||||
async keys() {
|
||||
return (await db.files.where('key').notEqual(ROOT).toArray()).map(f => f.key);
|
||||
|
@ -92,7 +92,7 @@ const serverContext = (): InternalFilesContextType => ({
|
|||
get(key: string) {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
remove(key: string) {
|
||||
remove(...keys: string[]) {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
keys() {
|
||||
|
@ -131,9 +131,12 @@ export const FilesProvider: ParentComponent = (props) => {
|
|||
files: createMemo(() => state.openedFiles),
|
||||
root: createMemo(() => state.root),
|
||||
|
||||
open(directory: FileSystemDirectoryHandle) {
|
||||
async open(directory: FileSystemDirectoryHandle) {
|
||||
await internal.remove(...(await internal.keys()));
|
||||
|
||||
setState('root', directory);
|
||||
internal.set(ROOT, directory);
|
||||
|
||||
await internal.set(ROOT, directory);
|
||||
},
|
||||
|
||||
get(key: string): Accessor<FileSystemDirectoryHandle | undefined> {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, createEffect, createMemo, createSignal, For, ParentProps, Setter, Show } from "solid-js";
|
||||
import { Component, createEffect, createMemo, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js";
|
||||
import { filter, MutarionKind, Mutation, splitAt } from "~/utilities";
|
||||
import { Sidebar } from "~/components/sidebar";
|
||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
||||
|
@ -9,6 +9,7 @@ import { GridApi } from "~/features/file/grid";
|
|||
import { Tab, Tabs } from "~/components/tabs";
|
||||
import css from "./edit.module.css";
|
||||
import { isServer } from "solid-js/web";
|
||||
import { Prompt, PromptApi } from "~/components/prompt";
|
||||
|
||||
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
|
||||
|
||||
|
@ -60,8 +61,18 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => {
|
||||
const [api, setApi] = createSignal<GridApi>();
|
||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||
const [files, setFiles] = createSignal<Map<string, { key: string, handle: FileSystemFileHandle }>>(new Map());
|
||||
|
||||
return ({ key, handle, api, setApi, entries, setEntries });
|
||||
(async () => {
|
||||
const files = await Array.fromAsync(
|
||||
filter(handle.values(), entry => entry.kind === 'file'),
|
||||
async file => [file.name.split('.').at(0)!, { handle: file, key: await file.getUniqueId() }] as const
|
||||
);
|
||||
|
||||
setFiles(new Map(files));
|
||||
})();
|
||||
|
||||
return ({ key, handle, api, setApi, entries, setEntries, files });
|
||||
}));
|
||||
const [active, setActive] = createSignal<string>();
|
||||
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
||||
|
@ -74,14 +85,27 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
const api = createMemo(() => tab()?.api());
|
||||
const mutations = createMemo<(Mutation & { file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => {
|
||||
const entries = tab.entries();
|
||||
const files = tab.files();
|
||||
const mutations = tab.api()?.mutations() ?? [];
|
||||
|
||||
return mutations.map(m => {
|
||||
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.'));
|
||||
return mutations.flatMap(m => {
|
||||
switch (m.kind) {
|
||||
case MutarionKind.Update: {
|
||||
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.'));
|
||||
|
||||
console.log(m.key, key, lang, entries);
|
||||
return { kind: MutarionKind.Update, key, file: entries.get(key)?.[lang] };
|
||||
}
|
||||
|
||||
return { ...m, key, file: entries.get(key)?.[lang] };
|
||||
case MutarionKind.Create: {
|
||||
return Object.entries(m.value).map(([lang, value]) => ({ kind: MutarionKind.Create, key: m.key, file: files.get(lang)!, value }));
|
||||
}
|
||||
|
||||
case MutarionKind.Delete: {
|
||||
return files.values().map(file => ({ kind: MutarionKind.Delete, key: m.key, file })).toArray();
|
||||
}
|
||||
|
||||
default: throw new Error('unreachable code');
|
||||
}
|
||||
});
|
||||
}));
|
||||
const mutatedFiles = createMemo(() =>
|
||||
|
@ -149,10 +173,21 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(mutations());
|
||||
console.log(mutatedFiles());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log(mutatedData());
|
||||
});
|
||||
|
||||
const [prompt, setPrompt] = createSignal<PromptApi>();
|
||||
|
||||
const commands = {
|
||||
open: createCommand('open folder', async () => {
|
||||
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
|
||||
await filesContext.open(directory);
|
||||
}, { key: 'o', modifier: Modifier.Control }),
|
||||
close: createCommand('close folder', async () => {
|
||||
filesContext.remove('__root__');
|
||||
}),
|
||||
|
@ -197,8 +232,15 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
|
||||
remove(Object.keys(selection()));
|
||||
}, { key: 'delete', modifier: Modifier.None }),
|
||||
inserNewKey: createCommand('insert new key', () => {
|
||||
api()?.insert('this.is.some.key');
|
||||
inserNewKey: createCommand('insert new key', async () => {
|
||||
const formData = await prompt()?.showModal();
|
||||
const key = formData?.get('key')?.toString();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
api()?.insert(key);
|
||||
}),
|
||||
inserNewLanguage: noop.withLabel('insert new language'),
|
||||
} as const;
|
||||
|
@ -214,10 +256,6 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
<Menu.Item label="file">
|
||||
<Menu.Item command={commands.open} />
|
||||
|
||||
<Menu.Item command={commands.close} />
|
||||
|
||||
<Menu.Separator />
|
||||
|
||||
<Menu.Item command={commands.save} />
|
||||
</Menu.Item>
|
||||
|
||||
|
@ -240,6 +278,10 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
|||
<Menu.Item command={noop.withLabel('view')} />
|
||||
</Menu.Root>
|
||||
|
||||
<Prompt api={setPrompt} title="Which key do you want to create?" description={<>hint: use <code>.</code> to denote nested keys,<br /> i.e. <code>this.is.some.key</code> would be a key that is four levels deep</>}>
|
||||
<input name="key" value="this.is.an.awesome.key" placeholder="name of new key ()" />
|
||||
</Prompt>
|
||||
|
||||
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
||||
<Tree entries={tree().entries}>{[
|
||||
folder => {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-auto-flow: column;
|
||||
justify-content: start;
|
||||
justify-items: start;
|
||||
place-items: center start;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ export const splitAt = (subject: string, index: number): readonly [string, strin
|
|||
return [subject.slice(0, index), subject.slice(index + 1)] as const;
|
||||
};
|
||||
|
||||
export const debounce = <T extends (...args: any[]) => void>(callback: T, delay: number): T => {
|
||||
export const debounce = <T extends (...args: any[]) => void>(callback: T, delay: number): ((...args: Parameters<T>) => void) => {
|
||||
let handle: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
return (...args: any[]) => {
|
||||
return (...args: Parameters<T>) => {
|
||||
if (handle) {
|
||||
clearTimeout(handle);
|
||||
}
|
||||
|
@ -58,10 +58,7 @@ export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, pa
|
|||
return;
|
||||
}
|
||||
|
||||
for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b)).take(10)) {
|
||||
// console.log('deepdiff', keyA, valueA, keyB, valueB);
|
||||
// continue;
|
||||
|
||||
for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) {
|
||||
if (!keyA && !keyB) {
|
||||
throw new Error('this code should not be reachable, there is a bug with an unhandled/unknown edge case');
|
||||
}
|
||||
|
@ -125,9 +122,6 @@ const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable
|
|||
// 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) {
|
||||
yield [iterA.consume(), iterB.consume()];
|
||||
|
||||
iterA.advance();
|
||||
iterB.advance();
|
||||
}
|
||||
|
||||
// key of a aligns with last key in buffer b
|
||||
|
@ -140,8 +134,6 @@ const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable
|
|||
}
|
||||
|
||||
yield [a, iterB.consume()];
|
||||
|
||||
iterB.advance();
|
||||
}
|
||||
|
||||
// the reverse case, key of b is aligns with the last key in buffer a
|
||||
|
@ -154,8 +146,14 @@ const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable
|
|||
}
|
||||
|
||||
yield [iterA.consume(), b];
|
||||
}
|
||||
|
||||
iterA.advance();
|
||||
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.
|
||||
|
@ -196,8 +194,11 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
|||
|
||||
consume() {
|
||||
cursor = 0;
|
||||
const value = buffer.shift()!;
|
||||
|
||||
return buffer.shift()!;
|
||||
this.advance();
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
flush(): T[] {
|
||||
|
@ -213,7 +214,7 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
|||
},
|
||||
|
||||
get done() {
|
||||
return done && Math.max(0, buffer.length - 1) === cursor;
|
||||
return done && buffer.length === 0;
|
||||
},
|
||||
|
||||
get top() {
|
||||
|
@ -247,3 +248,9 @@ export const filter = async function*<T, S extends T>(subject: AsyncIterableIter
|
|||
}
|
||||
};
|
||||
|
||||
export const map = async function*<TIn, TResult>(subject: AsyncIterableIterator<TIn>, predicate: (value: TIn) => TResult): AsyncGenerator<TResult, void, unknown> {
|
||||
for await (const value of subject) {
|
||||
yield predicate(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue