Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Last active March 28, 2023 18:39
Show Gist options
  • Save zerkalica/4a5260bf6fcd57492946c463b8cfc0f6 to your computer and use it in GitHub Desktop.
Save zerkalica/4a5260bf6fcd57492946c463b8cfc0f6 to your computer and use it in GitHub Desktop.
batch fetch
type DataError = {
message?: string;
code: string;
}
class BatchError extends Error {
constructor(message: string, readonly code: string) {
super(message)
}
}
class Batcher<Data> {
// На сервер отправляется мапа id: значение
// Если значение null - сущность удалится, если Partial<Data> - пропатчится
// значение undefined - означает загрузить сущность по id в ключе,
// однако id: undefined вырежутся при сериализации в json
// поэтому отправляем id в массиве в _ids
// Сервер может вернуть ошибку на каждый id в соответствии с переданной мапой
// Или полную сущность (при загрузке или патче) или null при удалении
fetch(patch: Record<string, null | undefined | Partial<Data>> & { _ids?: string[] }) {
// Сервер возвращает id-мапу сущностей и id-мапу ошибок, если есть
return {} as { data?: Record<string, Data | null>; errors?: Record<string, DataError> }
}
// values кэшится по ключу timer_id
// Для разных таймеров будут разные timer_id и непересекающиеся наборы id в параметрах фетча (в params_get)
@ $mol_mem_key
values(timer_id: number) {
const patch = this.params_get()
const ids = Object.keys(patch)
const result = this.fetch( { ...patch, _ids: ids.filter(id => patch[id] === undefined) } )
const next = {} as Record<string, Data | Error | null>
for (const id of ids) {
const data = result?.data?.[id]
let error = result?.errors?.[id]
// Сервер не вернул ошибку и забыл отдать данные по какому-то из id, интерпретируем это как ошибку
if (data === undefined && ! error) error = { message: `Unknown error fetching ${id}`, code: 'notfound' }
// Конвертим объект с ошибкой в полноценный Error
// если value вернет ошибку, она автоматически бросится
const err = error ? new BatchError(error.message ?? error.code, error.code) : undefined
next[id] = err ?? data
// Если от конкретного value(id) отписались, то удаляем его из мапы, очищая память
// destructor поддерживается в mol_wire,
// что бы оно не всплывало в Object.keys, делаем его не перечисляемым
if (next[id] && typeof next[id] === 'object') Object.defineProperty(next, 'desctructor', {
value() { delete next[id] },
enumerable: false
})
}
return next
}
protected timer_id = 0
protected patch = {} as Record<string, null | undefined | Partial<Data>>
// В экшене, что бы перезапуски this.values не меняли timer_id и не очищали this.patch
// sleep бросает Promise с setTimeout
// Пока тред спит, все обращения к this.value складывают параметры запроса в this.patch
// По истечении таймера, timer_id увеличивается, this.patch очищается
// а его текущее значение запоминается в this.values, пока он не зафетчит данные
@ $mol_action
params_get() {
sleep(100)
this.timer_id++
// с этого момента все обращения к this.value будут батчится в другой timer_id
const patch = this.patch
this.patch = {}
return patch
}
// В экшене, что б при перезапусках не менялся timer_id относительно value
// и не мутировался постоянно this.patch
// Т.к. он может быть использован уже в другом таймере
@ $mol_action
params_set(id: string, next?: Partial<Data> | null) {
this.patch[id] = next
return this.timer_id
}
// value(id) - получить данные по id
// value(id, null) - удалить, возвращает null
// value(id, { title: 'some' }) - патчить часть объекта на сервере, возвращает полную сущность
@ $mol_mem_key
value(id: string, next?: Partial<Data> | null) {
const timer_id = this.params_set(id, next)
return this.values(timer_id)[id]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment