Skip to content

Commit 51ccf16

Browse files
authored
feat: subscribe ops (#177)
* feat: subscribe ops * add value in ops * op includes prev value * re-implement subscribeKey with new subscribe * update size snapshot
1 parent 377826c commit 51ccf16

File tree

4 files changed

+124
-28
lines changed

4 files changed

+124
-28
lines changed

.size-snapshot.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
}
1515
},
1616
"vanilla.js": {
17-
"bundled": 5875,
18-
"minified": 2857,
19-
"gzipped": 1104,
17+
"bundled": 6624,
18+
"minified": 3127,
19+
"gzipped": 1218,
2020
"treeshaked": {
2121
"rollup": {
2222
"code": 22,
@@ -28,9 +28,9 @@
2828
}
2929
},
3030
"utils.js": {
31-
"bundled": 5867,
32-
"minified": 2678,
33-
"gzipped": 1197,
31+
"bundled": 5769,
32+
"minified": 2653,
33+
"gzipped": 1202,
3434
"treeshaked": {
3535
"rollup": {
3636
"code": 45,

src/utils.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,16 @@ export const subscribeKey = <T extends object>(
1818
key: keyof T,
1919
callback: (value: T[typeof key]) => void,
2020
notifyInSync?: boolean
21-
) => {
22-
let prevValue = proxyObject[key]
23-
return subscribe(
21+
) =>
22+
subscribe(
2423
proxyObject,
25-
() => {
26-
const nextValue = proxyObject[key]
27-
if (!Object.is(prevValue, nextValue)) {
28-
callback((prevValue = nextValue))
24+
(ops) => {
25+
if (ops.some((op) => op[1][0] === key)) {
26+
callback(proxyObject[key])
2927
}
3028
},
3129
notifyInSync
3230
)
33-
}
3431

3532
/**
3633
* devtools

src/vanilla.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ const isSupportedObject = (x: unknown): x is object =>
2929
type ProxyObject = object
3030
const proxyCache = new WeakMap<object, ProxyObject>()
3131

32+
type Path = (string | symbol)[]
33+
type Op =
34+
| [op: 'set', path: Path, value: unknown, prevValue: unknown]
35+
| [op: 'delete', path: Path, prevValue: unknown]
36+
| [op: 'resolve', path: Path, value: unknown]
37+
| [op: 'reject', path: Path, error: unknown]
38+
type Listener = (op: Op, nextVersion: number) => void
39+
3240
let globalVersion = 1
3341
const snapshotCache = new WeakMap<
3442
object,
@@ -44,16 +52,34 @@ export const proxy = <T extends object>(initialObject: T = {} as T): T => {
4452
return found
4553
}
4654
let version = globalVersion
47-
const listeners = new Set<(nextVersion: number) => void>()
48-
const notifyUpdate = (nextVersion?: number) => {
55+
const listeners = new Set<Listener>()
56+
const notifyUpdate = (op: Op, nextVersion?: number) => {
4957
if (!nextVersion) {
5058
nextVersion = ++globalVersion
5159
}
5260
if (version !== nextVersion) {
5361
version = nextVersion
54-
listeners.forEach((listener) => listener(nextVersion as number))
62+
listeners.forEach((listener) => listener(op, nextVersion as number))
5563
}
5664
}
65+
const propListeners = new Map<string | symbol, Listener>()
66+
const getPropListener = (prop: string | symbol) => {
67+
let propListener = propListeners.get(prop)
68+
if (!propListener) {
69+
propListener = (op, nextVersion) => {
70+
const newOp: Op = [...op]
71+
newOp[1] = [prop, ...(newOp[1] as Path)]
72+
notifyUpdate(newOp, nextVersion)
73+
}
74+
propListeners.set(prop, propListener)
75+
}
76+
return propListener
77+
}
78+
const popPropListener = (prop: string | symbol) => {
79+
const propListener = propListeners.get(prop)
80+
propListeners.delete(prop)
81+
return propListener
82+
}
5783
const createSnapshot = (target: any, receiver: any) => {
5884
const cache = snapshotCache.get(receiver)
5985
if (cache?.[0] === version) {
@@ -111,11 +137,11 @@ export const proxy = <T extends object>(initialObject: T = {} as T): T => {
111137
const prevValue = target[prop]
112138
const childListeners = (prevValue as any)?.[LISTENERS]
113139
if (childListeners) {
114-
childListeners.delete(notifyUpdate)
140+
childListeners.delete(popPropListener(prop))
115141
}
116142
const deleted = Reflect.deleteProperty(target, prop)
117143
if (deleted) {
118-
notifyUpdate()
144+
notifyUpdate(['delete', [prop], prevValue])
119145
}
120146
return deleted
121147
},
@@ -126,7 +152,7 @@ export const proxy = <T extends object>(initialObject: T = {} as T): T => {
126152
}
127153
const childListeners = (prevValue as any)?.[LISTENERS]
128154
if (childListeners) {
129-
childListeners.delete(notifyUpdate)
155+
childListeners.delete(popPropListener(prop))
130156
}
131157
if (
132158
refSet.has(value) ||
@@ -138,12 +164,12 @@ export const proxy = <T extends object>(initialObject: T = {} as T): T => {
138164
target[prop] = value
139165
.then((v) => {
140166
target[prop][PROMISE_RESULT] = v
141-
notifyUpdate()
167+
notifyUpdate(['resolve', [prop], v])
142168
return v
143169
})
144170
.catch((e) => {
145171
target[prop][PROMISE_ERROR] = e
146-
notifyUpdate()
172+
notifyUpdate(['reject', [prop], e])
147173
})
148174
} else {
149175
value = getUntracked(value) || value
@@ -152,9 +178,9 @@ export const proxy = <T extends object>(initialObject: T = {} as T): T => {
152178
} else {
153179
target[prop] = proxy(value)
154180
}
155-
target[prop][LISTENERS].add(notifyUpdate)
181+
target[prop][LISTENERS].add(getPropListener(prop))
156182
}
157-
notifyUpdate()
183+
notifyUpdate(['set', [prop], value, prevValue])
158184
return true
159185
},
160186
})
@@ -186,7 +212,7 @@ export const getVersion = (proxyObject: any): number => {
186212

187213
export const subscribe = (
188214
proxyObject: any,
189-
callback: () => void,
215+
callback: (ops: Op[]) => void,
190216
notifyInSync?: boolean
191217
) => {
192218
if (
@@ -197,15 +223,17 @@ export const subscribe = (
197223
throw new Error('Please use proxy object')
198224
}
199225
let pendingVersion = 0
200-
const listener = (nextVersion: number) => {
226+
const ops: Op[] = []
227+
const listener: Listener = (op, nextVersion) => {
228+
ops.push(op)
201229
if (notifyInSync) {
202-
callback()
230+
callback(ops.splice(0))
203231
return
204232
}
205233
pendingVersion = nextVersion
206234
Promise.resolve().then(() => {
207235
if (nextVersion === pendingVersion) {
208-
callback()
236+
callback(ops.splice(0))
209237
}
210238
})
211239
}

tests/subscribe.test.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { proxy, ref, subscribe } from '../src/index'
2+
import { subscribeKey } from '../src/utils'
23

34
describe('subscribe', () => {
45
it('should call subscription', async () => {
@@ -95,4 +96,74 @@ describe('subscribe', () => {
9596
await Promise.resolve()
9697
expect(handler).toBeCalledTimes(0)
9798
})
99+
100+
it('should notify ops', async () => {
101+
const obj = proxy<{ count1: number; count2?: number }>({
102+
count1: 0,
103+
count2: 0,
104+
})
105+
const handler = jest.fn()
106+
107+
subscribe(obj, handler)
108+
109+
obj.count1 += 1
110+
obj.count2 = 2
111+
112+
await Promise.resolve()
113+
expect(handler).toBeCalledTimes(1)
114+
expect(handler).lastCalledWith([
115+
['set', ['count1'], 1, 0],
116+
['set', ['count2'], 2, 0],
117+
])
118+
119+
delete obj.count2
120+
121+
await Promise.resolve()
122+
expect(handler).toBeCalledTimes(2)
123+
expect(handler).lastCalledWith([['delete', ['count2'], 2]])
124+
})
125+
126+
it('should notify nested ops', async () => {
127+
const obj = proxy<{ nested: { count?: number } }>({ nested: { count: 0 } })
128+
const handler = jest.fn()
129+
130+
subscribe(obj, handler)
131+
132+
obj.nested.count = 1
133+
134+
await Promise.resolve()
135+
expect(handler).toBeCalledTimes(1)
136+
expect(handler).lastCalledWith([['set', ['nested', 'count'], 1, 0]])
137+
138+
delete obj.nested.count
139+
140+
await Promise.resolve()
141+
expect(handler).toBeCalledTimes(2)
142+
expect(handler).lastCalledWith([['delete', ['nested', 'count'], 1]])
143+
})
144+
})
145+
146+
describe('subscribeKey', () => {
147+
it('should call subscription', async () => {
148+
const obj = proxy({ count1: 0, count2: 0 })
149+
const handler1 = jest.fn()
150+
const handler2 = jest.fn()
151+
152+
subscribeKey(obj, 'count1', handler1)
153+
subscribeKey(obj, 'count2', handler2)
154+
155+
obj.count1 += 10
156+
157+
await Promise.resolve()
158+
expect(handler1).toBeCalledTimes(1)
159+
expect(handler1).lastCalledWith(10)
160+
expect(handler2).toBeCalledTimes(0)
161+
162+
obj.count2 += 20
163+
164+
await Promise.resolve()
165+
expect(handler1).toBeCalledTimes(1)
166+
expect(handler2).toBeCalledTimes(1)
167+
expect(handler2).lastCalledWith(20)
168+
})
98169
})

0 commit comments

Comments
 (0)