@logux/client
Version:
Logux base components to build web client
363 lines (335 loc) • 10.8 kB
JavaScript
import { isFirstOlder } from '@logux/core'
import { map, onMount, startTask } from 'nanostores'
import { track } from '../track/index.js'
export function createFilter(client, Template, filter = {}, opts = {}) {
let id = Template.plural + JSON.stringify(filter) + JSON.stringify(opts)
if (!Template.filters) Template.filters = {}
if (!Template.filters[id]) {
let filterStore = map()
onMount(filterStore, () => {
let listener
if (opts.listChangesOnly) {
listener = () => {}
} else {
listener = () => {
filterStore.setKey(
'list',
Array.from(stores.values()).map(i => i.value)
)
}
}
let stores = new Map()
let isLoading = true
let list = []
filterStore.setKey('list', list)
filterStore.set({
isEmpty: true,
isLoading: true,
list,
stores
})
let channelPrefix = Template.plural + '/'
let createdType = `${Template.plural}/created`
let createType = `${Template.plural}/create`
let changedType = `${Template.plural}/changed`
let changeType = `${Template.plural}/change`
let deletedType = `${Template.plural}/deleted`
let deleteType = `${Template.plural}/delete`
let subscribe = {
channel: Template.plural,
filter,
type: 'logux/subscribe'
}
let unbinds = []
let unbindIds = new Map()
let subscribed = new Set()
async function add(child) {
let unbindChild = child.listen(listener)
if (stores.has(child.value.id)) {
unbindChild()
return
}
unbindIds.set(child.value.id, unbindChild)
stores.set(child.value.id, child)
filterStore.setKey(
'list',
Array.from(stores.values()).map(i => i.value)
)
filterStore.setKey('isEmpty', stores.size === 0)
}
function remove(childId) {
subscribed.delete(channelPrefix + childId)
if (stores.has(childId)) {
unbindIds.get(childId)()
unbindIds.delete(childId)
stores.delete(childId)
filterStore.setKey(
'list',
Array.from(stores.values()).map(i => i.value)
)
filterStore.setKey('isEmpty', stores.size === 0)
}
}
function checkSomeFields(fields) {
let some = Object.keys(filter).length === 0
for (let key in filter) {
if (key in fields) {
if (fields[key] === filter[key]) {
some = true
} else {
return false
}
}
}
return some
}
function checkAllFields(fields) {
for (let key in filter) {
if (fields[key] !== filter[key]) {
return false
}
}
return true
}
let subscriptionError
let endTask = startTask()
filterStore.loading = new Promise((resolve, reject) => {
async function processSubscribe(subscription) {
await subscription
.then(() => {
if (isLoading) {
isLoading = false
if (filterStore.value) {
filterStore.setKey('isLoading', false)
}
endTask()
resolve()
}
})
.catch(e => {
subscriptionError = true
reject(e)
endTask()
})
}
async function loadAndCheck(child) {
let clear = child.listen(() => {})
try {
if (child.value.isLoading) await child.loading
if (checkAllFields(child.value)) {
await add(child)
}
} finally {
clear()
}
}
for (let i in Template.cache) {
loadAndCheck(Template.cache[i])
}
let load = true
if (process.env.NODE_ENV !== 'production') {
if (Template.mocked) {
load = false
filterStore.setKey('isLoading', false)
endTask()
resolve()
}
}
if (load) {
let ignore = new Set()
let checking = []
if (Template.offline) {
let latestMeta
client.log
.each({ index: Template.plural }, async (action, meta) => {
if (latestMeta === undefined) {
latestMeta = meta
} else if (isFirstOlder(meta, latestMeta)) {
latestMeta = meta
}
if (action.id && !ignore.has(action.id)) {
let type = action.type
if (
type === createdType ||
type === createType ||
type === changedType ||
type === changeType
) {
if (checkSomeFields(action.fields)) {
checking.push(loadAndCheck(Template(action.id, client)))
ignore.add(action.id)
}
} else if (type === deletedType || type === deleteType) {
ignore.add(action.id)
}
}
})
.then(async () => {
await Promise.all(checking)
if (!Template.remote && isLoading) {
isLoading = false
filterStore.setKey('isLoading', false)
endTask()
resolve()
} else if (Template.remote) {
let subscribeSinceLatest =
latestMeta !== undefined
? {
...subscribe,
since: { id: latestMeta.id, time: latestMeta.time }
}
: subscribe
await processSubscribe(client.sync(subscribeSinceLatest))
}
})
}
if (Template.remote && !Template.offline) {
processSubscribe(client.sync(subscribe))
}
}
function setReason(action, meta) {
if (checkAllFields(action.fields)) {
meta.reasons.push(id)
}
}
function createAt(childId) {
return Template.cache[childId].createdAt
}
let removeAndListen = (childId, actionId) => {
remove(childId)
if (Template.remote) {
let child = Template(childId, client)
let clear = child.listen(() => {})
track(client, actionId)
.catch(() => {
add(child)
})
.finally(() => {
clear()
})
}
}
if (Template.remote) {
unbinds.push(
client.type(createdType, setReason, { event: 'preadd' }),
client.type(createType, setReason, { event: 'preadd' })
)
}
unbinds.push(
client.type('logux/subscribed', action => {
if (action.channel.startsWith(channelPrefix)) {
subscribed.add(action.channel)
}
}),
client.type(createdType, async (action, meta) => {
if (checkAllFields(action.fields)) {
add(
Template(
action.id,
client,
action,
meta,
subscribed.has(channelPrefix + action.id)
)
)
}
}),
client.type(createType, async (action, meta) => {
if (checkAllFields(action.fields)) {
let child = Template(action.id, client, action, meta)
try {
add(child)
track(client, meta.id).catch(() => {
remove(action.id)
})
} catch {}
}
}),
client.type(changedType, async (action, meta) => {
await Promise.resolve()
if (stores.has(action.id)) {
if (!checkAllFields(stores.get(action.id).value)) {
remove(action.id)
}
} else if (checkSomeFields(action.fields)) {
loadAndCheck(
Template(
action.id,
client,
action,
meta,
subscribed.has(channelPrefix + action.id)
)
)
}
}),
client.type(changeType, async (action, meta) => {
await Promise.resolve()
if (stores.has(action.id)) {
if (!checkAllFields(stores.get(action.id).value)) {
removeAndListen(action.id, meta.id)
}
} else if (checkSomeFields(action.fields)) {
let child = Template(action.id, client)
let clear = child.listen(() => {})
try {
if (child.value.isLoading) await child.loading
/* c8 ignore next 3 */
} catch {
return
}
if (checkAllFields(child.value)) {
clear()
add(child)
track(client, meta.id).catch(async () => {
let unbind = child.listen(() => {
if (!checkAllFields(child.value)) {
remove(action.id)
}
unbind()
})
})
}
}
}),
client.type(deletedType, (action, meta) => {
if (
stores.has(action.id) &&
isFirstOlder(createAt(action.id), meta)
) {
remove(action.id)
}
}),
client.type(deleteType, (action, meta) => {
if (
stores.has(action.id) &&
isFirstOlder(createAt(action.id), meta)
) {
removeAndListen(action.id, meta.id)
}
})
)
})
return () => {
for (let unbind of unbinds) unbind()
for (let unbindChild of unbindIds.values()) unbindChild()
if (Template.remote) {
if (!subscriptionError) {
client.log.add(
{
channel: Template.plural,
filter,
type: 'logux/unsubscribe'
},
{ sync: true }
)
}
}
client.log.removeReason(id)
delete Template.filters[id]
}
})
Template.filters[id] = filterStore
}
return Template.filters[id]
}