memory-orm
Version:
client side ORM + map reduce
355 lines (320 loc) • 8.51 kB
text/typescript
import _cloneDeep from 'lodash/cloneDeep'
import _merge from 'lodash/merge'
import _union from 'lodash/union'
import _uniq from 'lodash/uniq'
import _get from 'lodash/get'
import _set from 'lodash/set'
import { State, PureObject, step, Metadata } from './mem'
import { Datum } from './datum'
import {
DATA,
Memory,
Reduce,
Filter,
Cache,
OrderCmd,
ReduceLeaf,
SetContext,
Emitter,
LeafCmd,
NameBase,
PATH,
CLASS,
DEFAULT_RULE_TYPE,
} from './type'
import { Model } from './model'
import { Map } from './map'
import { Struct } from './struct'
import { List } from './list'
import { Query } from './query'
type IdProcess = (item: string) => void
type PlainProcess<O> = (item: Partial<O>) => void
type Emitters = {
order: Emitter<OrderCmd>
reduce: Emitter<LeafCmd> & { default: Emitter<LeafCmd>; default_origin: Emitter<LeafCmd> }
}
type ReduceContext<A extends DEFAULT_RULE_TYPE> = {
map: typeof Map
query: Query<A>
memory: Memory
cache: Cache['$format']
paths: {
_reduce: Reduce
}
}
function each_by_id<A extends DEFAULT_RULE_TYPE>({ from }: SetContext<A>, process: IdProcess) {
if (from instanceof Array) {
for (const item of from) {
process((item as any).id || item)
}
}
}
function each<A extends DEFAULT_RULE_TYPE>({ from }: SetContext<A>, process: PlainProcess<A[0]>) {
if (from instanceof Array) {
for (const item of from) {
process(item)
}
} else if (from instanceof Object) {
for (const id in from) {
const item = from[id]
item._id = id
process(item)
}
}
}
function validate(item: any, meta: Metadata, chklist: Filter[]): boolean {
if (!item || !chklist) {
return false
}
for (let chk of chklist) {
if (!chk(item, meta)) {
return false
}
}
return true
}
export class Finder<A extends DEFAULT_RULE_TYPE> {
$name!: NameBase
all!: Query<A>
model!: typeof Model | typeof Struct
list!: CLASS<List<A>>
map!: typeof Map
constructor() {}
join({
$name,
model,
all,
map,
list,
}: {
$name: NameBase
all: Query<A>
map: typeof Map
list: CLASS<List<A>>
model: typeof Model | typeof Struct
}) {
this.$name = $name
this.all = all
this.map = map
this.list = list
this.model = model
State.notify(this.$name.list)
}
calculate(query: Query<A>, memory: Memory) {
if (query._step >= State.step[this.$name.list]) {
return
}
const base = State.base(this.$name.list)
delete query._reduce
query._step = step()
const ctx: ReduceContext<A> = {
map: this.map,
query,
memory,
cache: _cloneDeep(base.$format),
paths: {
_reduce: {
list: [],
hash: {},
},
},
}
if (query._all_ids) {
let ids = query._all_ids
if (query._is_uniq) {
ids = _uniq(ids)
}
this.reduce(ctx, ids)
} else if (query === query.all) {
this.reduce(ctx, Object.keys(memory))
} else if (query._is_uniq) {
let ids = []
for (const partition of query.$partition) {
const tgt = _get(query.all, `reduce.${partition}`)
ids = _union(ids, tgt)
}
this.reduce(ctx, ids)
} else {
for (const partition of query.$partition) {
const tgt = _get(query.all, `reduce.${partition}`)
this.reduce(ctx, tgt)
}
}
this.finish(ctx)
}
reduce({ map, cache, paths, query, memory }: ReduceContext<A>, ids: string[]) {
if (!ids) {
return
}
for (let id of ids) {
const o = memory[id]
if (o) {
const { meta, item, $group } = o
if (!validate(item, meta, query._filters)) {
continue
}
for (let [path, a] of $group) {
const o = (paths[path] = cache[path])
map.reduce(query, path, item, o, a)
}
}
}
}
finish({ map, paths, query }: ReduceContext<A>) {
for (const path in paths) {
const o = paths[path]
map.finish(query, path, o, this.list)
_set(query, path, o)
}
for (const path in query.$sort) {
const cmd: OrderCmd = query.$sort[path]
const from: ReduceLeaf = _get(query, path)
if (from) {
const sorted = map.order(query, path, from, from, cmd, this.list)
const dashed = map.dash(query, path, sorted, from, cmd, this.list)
const result = map.post_proc(query, path, dashed, from, cmd, this.list)
this.list.bless(result as any, query)
result.from = from
_set(query, path, result)
}
}
}
data_set(type: string, from: DATA<A[0]>, parent: Object | undefined) {
const meta = State.meta()
const base = State.base(this.$name.list)
const journal = State.journal(this.$name.list)
const { deploys } = this.$name
return this[type]({
base,
journal,
meta,
model: this.model,
all: this.all,
deploys,
from,
parent,
})
}
data_emitter({ base, journal }: SetContext<A>, { item, $group }): Emitters {
if (!base.$format) {
throw new Error('bad context.')
}
const order = (keys: PATH, cmd: OrderCmd) => {
if ('string' === typeof keys) {
keys = [keys]
}
const path = [`_reduce`, ...keys].join('.')
base.$sort[path] = cmd
journal.$sort[path] = cmd
}
const reduce = (keys: PATH, cmd: LeafCmd) => {
if ('string' === typeof keys) {
keys = [keys]
}
const path = [`_reduce`, ...keys].join('.')
cmd = reduce.default(keys, cmd)
$group.push([path, cmd])
const map = base.$format[path] || (base.$format[path] = {})
const map_j = journal.$format[path] || (journal.$format[path] = {})
this.map.init(map, cmd)
this.map.init(map_j, cmd)
}
reduce.default = reduce.default_origin = function (keys: PATH, cmd: LeafCmd) {
if (keys.length) {
return cmd
}
reduce.default = (_keys, cmd) => cmd
const bare = {
set: item.id,
list: true,
}
return Object.assign(bare, cmd)
}
return ({ reduce, order } as any) as Emitters
}
data_init(
{ model, parent, deploys }: SetContext<A>,
{ item }: Datum,
{ reduce, order }: Emitters
) {
model.bless(item as any)
parent && _merge(item, parent)
model.deploy.call(item, model)
for (const deploy of deploys) {
deploy.call(item, { o: item, model, reduce, order })
}
}
data_entry({ model }: SetContext<A>, { item }: Datum, { reduce, order }: Emitters) {
model.map_partition(item, reduce)
model.map_reduce(item, reduce)
if (reduce.default === reduce.default_origin) {
reduce([], {})
}
model.order(item, order)
}
reset(ctx: SetContext<A>) {
ctx.journal.$memory = PureObject()
const news = (ctx.base.$memory = ctx.all.$memory = PureObject())
this.merge(ctx)
for (let key in ctx.base.$memory) {
const old = ctx.base.$memory[key]
const item = news[key]
if (item == null) {
ctx.model.delete(old)
}
}
return true
}
merge(ctx: SetContext<A>) {
let is_hit = false
each(ctx, (item) => {
const o = new Datum(ctx.meta, item as any)
const emit = this.data_emitter(ctx, o)
this.data_init(ctx, o, emit)
this.data_entry(ctx, o, emit)
const id = item.id
if (!id) {
throw new Error(`detect bad data: ${JSON.stringify(item)}`)
}
ctx.journal.$memory[id] = o
ctx.base.$memory[id] = o
const old = ctx.base.$memory[item.id!]
if (old != null) {
ctx.model.update(item, old.item)
} else {
ctx.model.create(item)
}
return (is_hit = true)
})
return is_hit
}
remove(ctx: SetContext<A>) {
let is_hit = false
each_by_id(ctx, (id) => {
const old = ctx.base.$memory[id]
if (old != null) {
ctx.model.delete(old.item)
delete ctx.journal.$memory[id]
delete ctx.base.$memory[id]
is_hit = true
}
})
return is_hit
}
update(ctx: SetContext<A>, parent: Object) {
let is_hit = false
each_by_id(ctx, (id) => {
const old = ctx.base.$memory[id]
if (!old) {
return
}
_merge(old.item, parent)
old.$group = []
const emit = this.data_emitter(ctx, old)
this.data_entry(ctx, old, emit)
ctx.model.update(old.item, old.item)
is_hit = true
})
return is_hit
}
}