Mixing async and signals

I have a -probably stipid- question: can I create a createAsyncMemo function? and how would I go about that? I am looking for a memo that can handle async browser api's. I thought that I could just use createAsync for this. however since it seems to be tied to the page's state I get a flickering page because the source signal I use has an internal use of createPolled. So in that train of thought I think the next logical step is to basically make a createAsync that is not tied to the router. But I am not having much luck, likely due to my limited knowledge in regards to signals. For some more concrete context, I have a function that I feed an Accessor<FileSystemDirectoryHandle> and this returns me a signal of all the .json files in that directory. Interally I also poll the files' lastModified so that my output signal is updated every time a file is CRUD'ed outside of my app. Next, and this is my problem code, I want to consume this file-list-signal. The file API's are however all async, and I often have trouble mixing async and signals. So guessing this is all due to a lack of knowledge on my part, how would I go about mixing async and signals best? this is that functions that polls a directory
8 Replies
Chris P Bacon
Chris P BaconOP4d ago
type Files = Record<string, { handle: FileSystemFileHandle, file: File }>;
const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Files> => {
const tick = createPolled<number>(i => i + 1, 1000, 0);

return createAsync<Files>(async (prev) => {
tick();

prev ??= {};

const next = Object.fromEntries(await Array.fromAsync(
filter(directory().values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')),
async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }]
));

const keysPrev = Object.keys(prev);
const keysNext = Object.keys(next);

if (keysPrev.length !== keysNext.length) {
return next;
}

if (keysPrev.some(prev => keysNext.includes(prev) === false)) {
return next;
}

if (Object.entries(prev).every(([id, { file }]) => next[id].file.lastModified === file.lastModified) === false) {
return next;
}

return prev;
}, { initialValue: {} });
};
type Files = Record<string, { handle: FileSystemFileHandle, file: File }>;
const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Files> => {
const tick = createPolled<number>(i => i + 1, 1000, 0);

return createAsync<Files>(async (prev) => {
tick();

prev ??= {};

const next = Object.fromEntries(await Array.fromAsync(
filter(directory().values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')),
async handle => [await handle.getUniqueId(), { file: await handle.getFile(), handle }]
));

const keysPrev = Object.keys(prev);
const keysNext = Object.keys(next);

if (keysPrev.length !== keysNext.length) {
return next;
}

if (keysPrev.some(prev => keysNext.includes(prev) === false)) {
return next;
}

if (Object.entries(prev).every(([id, { file }]) => next[id].file.lastModified === file.lastModified) === false) {
return next;
}

return prev;
}, { initialValue: {} });
};
also, for more context, this is my inital attempt at a createAsyncMemo, but it goes infinite as soon as I consume the getter
function createAsyncMemo<Next extends Prev, Init extends Next, Prev>(fn: (prev: Prev | Init) => (Promise<Next | Prev> | Next | Prev), value: Init): Accessor<Next> {
const [get, set] = createSignal<Next>(value);

createEffect(() => {
const v = get() ?? value;

(async () => {
const next = await fn(v);

console.log('next', next);

set(() => next as Next);
})();
});

createEffect(() => {
// console.log('infinite???', get());
});

return createMemo(() => [] as Next);
}
function createAsyncMemo<Next extends Prev, Init extends Next, Prev>(fn: (prev: Prev | Init) => (Promise<Next | Prev> | Next | Prev), value: Init): Accessor<Next> {
const [get, set] = createSignal<Next>(value);

createEffect(() => {
const v = get() ?? value;

(async () => {
const next = await fn(v);

console.log('next', next);

set(() => next as Next);
})();
});

createEffect(() => {
// console.log('infinite???', get());
});

return createMemo(() => [] as Next);
}
Brendonovich
Brendonovich4d ago
The flickering sounds like the createAsync is re-executing and showing a suspense fallback - that's to be expected. Try using createResource and its .latest, which won't suspend at all since you're using initialValue createAsync is effectively an async memo haha
Chris P Bacon
Chris P BaconOP4d ago
hmm. I get the same flickering when using createResource. And also not sure how .latest is supposed to help here. I still do want it to be a signal. It seems that the eventual consuming 'scope' is still triggered by the underlying refetch even though the signal's output itself is clean. Is this perhaps where a well-placed untrack might help?
Brendonovich
Brendonovich4d ago
And also not sure how .latest is supposed to help here
Assuming that the flicker is the result of a suspense fallback showing, resource.latest will avoid suspending as opposed to doing resource()
Chris P Bacon
Chris P BaconOP4d ago
the new implementation
const Content: Component<{ directory: FileSystemDirectoryHandle }> = (props) => {
const files = readFiles(() => props.directory);
const [contents] = createResource(() => Promise.all(Object.entries(files()).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await load(file)) }))));

createEffect(() => {
console.log('contents', contents.latest);
});

return <></>;
}


type Files = Record<string, { handle: FileSystemFileHandle, file: File }>;
const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Files> => {
const [value, { refetch }] = createResource<Files>(async (_, { value: prev }) => {
... // omitted for brevity
}, { initialValue: {} })

const interval = setInterval(() => {
refetch();
}, 1000);

onCleanup(() => {
clearInterval(interval);
});

return value;
};
const Content: Component<{ directory: FileSystemDirectoryHandle }> = (props) => {
const files = readFiles(() => props.directory);
const [contents] = createResource(() => Promise.all(Object.entries(files()).map(async ([id, { file, handle }]) => ({ id, handle, lang: file.name.split('.').at(0)!, entries: (await load(file)) }))));

createEffect(() => {
console.log('contents', contents.latest);
});

return <></>;
}


type Files = Record<string, { handle: FileSystemFileHandle, file: File }>;
const readFiles = (directory: Accessor<FileSystemDirectoryHandle>): Accessor<Files> => {
const [value, { refetch }] = createResource<Files>(async (_, { value: prev }) => {
... // omitted for brevity
}, { initialValue: {} })

const interval = setInterval(() => {
refetch();
}, 1000);

onCleanup(() => {
clearInterval(interval);
});

return value;
};
ok, understood, but then I lose my reactivity right? and I do want a signal
Brendonovich
Brendonovich4d ago
resource.latest is a signal, you just don't call it same as accessing a store property, it's still reactive
Chris P Bacon
Chris P BaconOP4d ago
oooooh, it's one of those getters. not an accessor that is returned gotcha woooo, works like a charm!!! thanks!! @Brendonovich just a quick sanity check. does it make sense to have that onCleanup in order to remove the interval? or should I use a different mechanism?
Brendonovich
Brendonovich4d ago
yeah that's the right approach

Did you find this page helpful?