Skip to content

Commit 3bc7232

Browse files
authored
Merge pull request #114 from ReactiveDB/feature/query-token-traces
Feat: traces in QueryToken
2 parents d94eae9 + 2988285 commit 3bc7232

File tree

9 files changed

+760
-10
lines changed

9 files changed

+760
-10
lines changed

src/proxy/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { QueryToken, SelectorMeta } from '../storage/modules/QueryToken'
1+
export { QueryToken, SelectorMeta, TraceResult } from '../storage/modules/QueryToken'
22
export { ProxySelector } from '../storage/modules/ProxySelector'
33
export * from '../interface'

src/storage/modules/QueryToken.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import { Observable, OperatorFunction, from } from 'rxjs'
2-
import { combineAll, map, publishReplay, refCount, skipWhile, switchMap, take, tap } from 'rxjs/operators'
2+
import {
3+
combineAll,
4+
filter,
5+
map,
6+
pairwise,
7+
publishReplay,
8+
refCount,
9+
skipWhile,
10+
switchMap,
11+
startWith,
12+
take,
13+
tap,
14+
} from 'rxjs/operators'
315
import { Selector } from './Selector'
416
import { ProxySelector } from './ProxySelector'
517
import { assert } from '../../utils/assert'
618
import { TokenConsumed } from '../../exception/token'
19+
import { diff, Ops, OpsType, OpType } from '../../utils/diff'
20+
21+
export type TraceResult<T> = Ops & {
22+
result: ReadonlyArray<T>
23+
}
24+
25+
function initialTraceResult<T>(list: ReadonlyArray<T>): TraceResult<T> {
26+
return {
27+
type: OpsType.Success,
28+
ops: list.map((_value, index) => ({ type: OpType.New, index })),
29+
result: list,
30+
}
31+
}
732

833
export type SelectorMeta<T> = Selector<T> | ProxySelector<T>
934

@@ -15,9 +40,19 @@ export class QueryToken<T> {
1540
selector$: Observable<SelectorMeta<T>>
1641

1742
private consumed = false
43+
private lastEmit: ReadonlyArray<T> | undefined
44+
private trace: ReadonlyArray<T> | undefined
45+
46+
constructor(selector$: Observable<SelectorMeta<T>>, trace?: ReadonlyArray<T>) {
47+
this.selector$ = selector$.pipe(
48+
publishReplay(1),
49+
refCount(),
50+
)
51+
this.trace = trace
52+
}
1853

19-
constructor(selector$: Observable<SelectorMeta<T>>) {
20-
this.selector$ = selector$.pipe(publishReplay(1), refCount())
54+
setTrace(data: T[]) {
55+
this.trace = data
2156
}
2257

2358
map<K>(fn: OperatorFunction<T[], K[]>) {
@@ -42,6 +77,23 @@ export class QueryToken<T> {
4277
return this.selector$.pipe(switchMap((s) => s.changes()))
4378
}
4479

80+
traces(pk?: string): Observable<TraceResult<T>> {
81+
return this.changes().pipe(
82+
startWith<undefined | ReadonlyArray<T>>(this.trace),
83+
pairwise(),
84+
map(([prev, curr]) => {
85+
const result = curr!
86+
if (!prev) {
87+
return initialTraceResult(result)
88+
}
89+
const ops = diff(prev, result, pk)
90+
return { result, ...ops }
91+
}),
92+
filter(({ type }) => type !== OpsType.ShouldSkip),
93+
tap(({ result }) => (this.lastEmit = result)),
94+
)
95+
}
96+
4597
concat(...tokens: QueryToken<T>[]) {
4698
tokens.unshift(this)
4799
const newSelector$ = from(tokens).pipe(
@@ -52,7 +104,7 @@ export class QueryToken<T> {
52104
return first!.concat(...r)
53105
}),
54106
)
55-
return new QueryToken<T>(newSelector$)
107+
return new QueryToken<T>(newSelector$, this.lastEmit)
56108
}
57109

58110
combine(...tokens: QueryToken<any>[]) {
@@ -65,7 +117,7 @@ export class QueryToken<T> {
65117
return first!.combine(...r)
66118
}),
67119
)
68-
return new QueryToken<T>(newSelector$)
120+
return new QueryToken<T>(newSelector$, this.lastEmit)
69121
}
70122

71123
toString() {

src/utils/diff.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
export enum OpType {
2+
// 0 = reuse
3+
// 1 = use new item
4+
Reuse,
5+
New,
6+
}
7+
8+
export type Op = {
9+
type: OpType
10+
index: number
11+
}
12+
13+
export enum OpsType {
14+
// 0 = error
15+
// 1 = success
16+
// 2 = success but should skip
17+
Error,
18+
Success,
19+
ShouldSkip,
20+
}
21+
22+
export type Ops = {
23+
type: OpsType
24+
ops: Op[]
25+
message?: string
26+
}
27+
28+
// as an example, use diff to patch data
29+
export const patch = <T>(ops: ReadonlyArray<Op>, oldList: ReadonlyArray<T>, newList: ReadonlyArray<T>) => {
30+
if (!oldList.length) {
31+
return newList
32+
}
33+
34+
return newList.map((data, i) => {
35+
const op = ops[i]
36+
37+
if (op.type === OpType.Reuse) {
38+
return oldList[op.index]
39+
}
40+
41+
return data
42+
})
43+
}
44+
45+
export const getPatchResult = <T>(oldList: ReadonlyArray<T>, newList: ReadonlyArray<T>, ops: Ops): ReadonlyArray<T> => {
46+
switch (ops.type) {
47+
case OpsType.Error:
48+
return newList
49+
case OpsType.ShouldSkip:
50+
return oldList
51+
case OpsType.Success:
52+
default:
53+
return patch(ops.ops, oldList, newList)
54+
}
55+
}
56+
57+
function fastEqual(left: object, right: object) {
58+
if (left === right) {
59+
return true
60+
}
61+
62+
if (left && right && typeof left == 'object' && typeof right == 'object') {
63+
const isLeftArray = Array.isArray(left)
64+
const isRightArray = Array.isArray(right)
65+
66+
if (isLeftArray && isRightArray) {
67+
const length = (left as any[]).length
68+
69+
if (length != (right as any[]).length) {
70+
return false
71+
}
72+
73+
for (let i = length; i-- !== 0; ) {
74+
if (!fastEqual(left[i], right[i])) {
75+
return false
76+
}
77+
}
78+
79+
return true
80+
}
81+
82+
if (isLeftArray !== isRightArray) {
83+
return false
84+
}
85+
86+
const isLeftDate = left instanceof Date
87+
const isRightDate = right instanceof Date
88+
89+
if (isLeftDate != isRightDate) {
90+
return false
91+
}
92+
93+
if (isLeftDate && isRightDate) {
94+
return (left as Date).getTime() == (right as Date).getTime()
95+
}
96+
97+
const keys = Object.keys(left)
98+
const LeftLen = keys.length
99+
100+
if (LeftLen !== Object.keys(right).length) {
101+
return false
102+
}
103+
104+
for (let k = LeftLen; k-- !== 0; ) {
105+
if (!right.hasOwnProperty(keys[k])) {
106+
return false
107+
}
108+
}
109+
110+
for (let j = LeftLen; j-- !== 0; ) {
111+
const key = keys[j]
112+
if (!fastEqual(left[key], right[key])) {
113+
return false
114+
}
115+
}
116+
117+
return true
118+
}
119+
120+
return left !== left && right !== right
121+
}
122+
123+
export function diff<T>(oldList: ReadonlyArray<T>, newList: ReadonlyArray<T>, pk = '_id'): Ops {
124+
const prev = oldList
125+
const curr = newList
126+
127+
if (!Array.isArray(prev) || !Array.isArray(curr)) {
128+
return {
129+
type: OpsType.Error,
130+
ops: [],
131+
message: `cannot compare non-list object`,
132+
}
133+
}
134+
135+
const index = {}
136+
for (let i = 0; i < prev.length; i++) {
137+
const value = prev[i][pk]
138+
if (value === undefined) {
139+
return {
140+
type: OpsType.Error,
141+
ops: [],
142+
message: `cannot find pk: ${pk} at prev.${i}`,
143+
}
144+
}
145+
index[value] = i
146+
}
147+
148+
const ret: Op[] = []
149+
let reused = 0
150+
151+
for (let k = 0; k < curr.length; k++) {
152+
const key = curr[k][pk]
153+
if (key === undefined) {
154+
return {
155+
type: OpsType.Error,
156+
ops: [],
157+
message: `cannot find pk: ${pk} at curr.${k}`,
158+
}
159+
}
160+
161+
const prevIndex = index[key]
162+
163+
if (prevIndex !== undefined) {
164+
const isEqual = fastEqual((curr as any)[k], (prev as any)[prevIndex])
165+
// if equal then reuse the previous data otherwise use the new data
166+
const op: Op = isEqual ? { type: OpType.Reuse, index: prevIndex } : { type: OpType.New, index: k }
167+
168+
if (prevIndex === k && isEqual) {
169+
reused++
170+
}
171+
ret.push(op)
172+
} else {
173+
ret.push({ type: OpType.New, index: k })
174+
}
175+
}
176+
177+
const arrayIsSame = reused === curr.length && prev.length === curr.length
178+
return {
179+
type: arrayIsSame ? OpsType.ShouldSkip : OpsType.Success,
180+
ops: ret,
181+
}
182+
}

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export * from './identity'
1010
export * from './get-type'
1111
export * from './truthy'
1212
export * from './try-catch'
13+
export * from './diff'
1314
export { warn } from './warn'

test/specs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './storage/modules/PredicateProvider.spec'
77
export * from './storage/modules/Mutation.spec'
88
export * from './shared/Traversable.spec'
99
export * from './utils/utils.spec'
10+
export * from './utils/diff.spec'
1011
export * from './storage/helper/definition.spec'
1112
export * from './storage/helper/graph.spec'
1213

0 commit comments

Comments
 (0)