Skip to content

Commit 9ad5228

Browse files
authored
feat(vanilla): make ops callback opt-in with unstable_enableOp (#1189)
* feat: new subscribe option * wip: needsOp * wip * wip 3 * wip 4 * wip 5 * restore everything * global enableOp flag * does this help? * Revert "does this help?" This reverts commit 63054a5. * minor change
1 parent 2fc6d3b commit 9ad5228

3 files changed

Lines changed: 109 additions & 60 deletions

File tree

src/vanilla.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Op =
2222
| [op: 'delete', path: Path, prevValue: unknown]
2323

2424
/** Function called when a proxy object changes */
25-
type Listener = (op: Op, nextVersion: number) => void
25+
type Listener = (op: Op | undefined, nextVersion: number) => void
2626

2727
export type INTERNAL_Op = Op
2828

@@ -122,14 +122,14 @@ const createHandlerDefault = <T extends object>(
122122
isInitializing: () => boolean,
123123
addPropListener: (prop: string | symbol, propValue: unknown) => void,
124124
removePropListener: (prop: string | symbol) => void,
125-
notifyUpdate: (op: Op) => void,
125+
notifyUpdate: (op: Op | undefined) => void,
126126
): ProxyHandler<T> => ({
127127
deleteProperty(target: T, prop: string | symbol) {
128128
const prevValue = Reflect.get(target, prop)
129129
removePropListener(prop)
130130
const deleted = Reflect.deleteProperty(target, prop)
131131
if (deleted) {
132-
notifyUpdate(['delete', [prop], prevValue])
132+
notifyUpdate(createOp?.('delete', prop, prevValue))
133133
}
134134
return deleted
135135
},
@@ -151,11 +151,17 @@ const createHandlerDefault = <T extends object>(
151151
!proxyStateMap.has(value) && canProxy(value) ? proxy(value) : value
152152
addPropListener(prop, nextValue)
153153
Reflect.set(target, prop, nextValue, receiver)
154-
notifyUpdate(['set', [prop], value, prevValue])
154+
notifyUpdate(createOp?.('set', prop, value, prevValue))
155155
return true
156156
},
157157
})
158158

159+
const createOpDefault = (
160+
type: 'set' | 'delete',
161+
prop: symbol | string,
162+
...args: unknown[]
163+
) => [type, [prop], ...args] as Op
164+
159165
// internal states
160166
const proxyStateMap: WeakMap<ProxyObject, ProxyState> = new WeakMap()
161167
const refSet: WeakSet<object> = new WeakSet()
@@ -171,6 +177,7 @@ let newProxy = <T extends object>(target: T, handler: ProxyHandler<T>): T =>
171177
let canProxy: typeof canProxyDefault = canProxyDefault
172178
let createSnapshot: typeof createSnapshotDefault = createSnapshotDefault
173179
let createHandler: typeof createHandlerDefault = createHandlerDefault
180+
let createOp: typeof createOpDefault | undefined
174181

175182
/**
176183
* Creates a reactive proxy object that can be tracked for changes
@@ -185,7 +192,10 @@ export function proxy<T extends object>(baseObject: T = {} as T): T {
185192
}
186193
let version = versionHolder[0]
187194
const listeners = new Set<Listener>()
188-
const notifyUpdate = (op: Op, nextVersion = ++versionHolder[0]) => {
195+
const notifyUpdate = (
196+
op: Op | undefined,
197+
nextVersion = ++versionHolder[0],
198+
) => {
189199
if (version !== nextVersion) {
190200
checkVersion = version = nextVersion
191201
listeners.forEach((listener) => listener(op, nextVersion))
@@ -207,8 +217,11 @@ export function proxy<T extends object>(baseObject: T = {} as T): T {
207217
const createPropListener =
208218
(prop: string | symbol): Listener =>
209219
(op, nextVersion) => {
210-
const newOp: Op = [...op]
211-
newOp[1] = [prop, ...(newOp[1] as Path)]
220+
let newOp: Op | undefined
221+
if (op) {
222+
newOp = [...op]
223+
newOp[1] = [prop, ...(newOp[1] as Path)]
224+
}
212225
notifyUpdate(newOp, nextVersion)
213226
}
214227
const propProxyStates = new Map<
@@ -310,7 +323,9 @@ export function subscribe<T extends object>(
310323
const addListener = (proxyState as ProxyState)[2]
311324
let isListenerActive = false
312325
const listener: Listener = (op) => {
313-
ops.push(op)
326+
if (op) {
327+
ops.push(op)
328+
}
314329
if (notifyInSync) {
315330
callback(ops.splice(0))
316331
return
@@ -429,3 +444,15 @@ export function unstable_replaceInternalFunction(
429444
throw new Error('unknown function')
430445
}
431446
}
447+
448+
export function unstable_enableOp(
449+
enabled: boolean | typeof createOpDefault = true,
450+
): void {
451+
if (enabled === true) {
452+
createOp = createOpDefault
453+
} else if (enabled === false) {
454+
createOp = undefined
455+
} else {
456+
createOp = enabled
457+
}
458+
}

src/vanilla/utils/devtools.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { snapshot, subscribe } from '../../vanilla.ts'
1+
import { snapshot, subscribe, unstable_enableOp } from '../../vanilla.ts'
22
import type {} from '@redux-devtools/extension'
33

44
// FIXME https://github.com/reduxjs/redux-devtools/issues/1097
@@ -55,10 +55,11 @@ export function devtools<T extends object>(
5555
return
5656
}
5757

58+
unstable_enableOp()
5859
let isTimeTraveling = false
5960
const devtools = extension.connect({ name, ...rest })
60-
const unsub1 = subscribe(proxyObject, (ops) => {
61-
const action = ops
61+
const unsub1 = subscribe(proxyObject, (unstable_ops) => {
62+
const action = unstable_ops
6263
.filter(([_, path]) => path[0] !== DEVTOOLS)
6364
.map(([op, path]) => `${op}:${path.map(String).join('.')}`)
6465
.join(', ')

tests/subscribe.test.tsx

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { proxy, ref, snapshot, subscribe } from 'valtio'
2+
import { proxy, ref, snapshot, subscribe, unstable_enableOp } from 'valtio'
33
import { subscribeKey } from 'valtio/utils'
44

55
describe('subscribe', () => {
@@ -49,7 +49,7 @@ describe('subscribe', () => {
4949
const obj = proxy({ count: 0 })
5050
const handler = vi.fn()
5151

52-
const _unsubscribeA = subscribe(obj, () => {
52+
subscribe(obj, () => {
5353
unsubscribeB()
5454
})
5555

@@ -135,53 +135,6 @@ describe('subscribe', () => {
135135
expect(handler).toBeCalledTimes(0)
136136
})
137137

138-
it('should notify ops', async () => {
139-
const obj = proxy<{ count1: number; count2?: number }>({
140-
count1: 0,
141-
count2: 0,
142-
})
143-
const handler = vi.fn()
144-
145-
subscribe(obj, handler)
146-
147-
obj.count1 += 1
148-
obj.count2 = 2
149-
150-
await vi.advanceTimersByTimeAsync(0)
151-
expect(handler).toBeCalledTimes(1)
152-
expect(handler).lastCalledWith([
153-
['set', ['count1'], 1, 0],
154-
['set', ['count2'], 2, 0],
155-
])
156-
157-
delete obj.count2
158-
159-
await vi.advanceTimersByTimeAsync(0)
160-
expect(handler).toBeCalledTimes(2)
161-
expect(handler).lastCalledWith([['delete', ['count2'], 2]])
162-
})
163-
164-
it('should notify nested ops', async () => {
165-
const obj = proxy<{ nested: { count?: number } }>({
166-
nested: { count: 0 },
167-
})
168-
const handler = vi.fn()
169-
170-
subscribe(obj, handler)
171-
172-
obj.nested.count = 1
173-
174-
await vi.advanceTimersByTimeAsync(0)
175-
expect(handler).toBeCalledTimes(1)
176-
expect(handler).lastCalledWith([['set', ['nested', 'count'], 1, 0]])
177-
178-
delete obj.nested.count
179-
180-
await vi.advanceTimersByTimeAsync(0)
181-
expect(handler).toBeCalledTimes(2)
182-
expect(handler).lastCalledWith([['delete', ['nested', 'count'], 1]])
183-
})
184-
185138
it('should not notify with assigning same object', async () => {
186139
const obj = {}
187140
const state = proxy({ obj })
@@ -245,3 +198,71 @@ describe('subscribeKey', () => {
245198
expect(snapshot1).not.toEqual(snapshot2)
246199
})
247200
})
201+
202+
describe('subscribe with op', () => {
203+
const consoleWarn = console.warn
204+
205+
beforeEach(() => {
206+
unstable_enableOp(true)
207+
console.warn = vi.fn((message: string) => {
208+
if (message === 'Please use proxy object') {
209+
return
210+
}
211+
consoleWarn(message)
212+
})
213+
vi.useFakeTimers()
214+
})
215+
216+
afterEach(() => {
217+
console.warn = consoleWarn
218+
vi.useRealTimers()
219+
unstable_enableOp(false)
220+
})
221+
222+
it('should notify ops', async () => {
223+
const obj = proxy<{ count1: number; count2?: number }>({
224+
count1: 0,
225+
count2: 0,
226+
})
227+
const handler = vi.fn()
228+
229+
subscribe(obj, handler)
230+
231+
obj.count1 += 1
232+
obj.count2 = 2
233+
234+
await vi.advanceTimersByTimeAsync(0)
235+
expect(handler).toBeCalledTimes(1)
236+
expect(handler).lastCalledWith([
237+
['set', ['count1'], 1, 0],
238+
['set', ['count2'], 2, 0],
239+
])
240+
241+
delete obj.count2
242+
243+
await vi.advanceTimersByTimeAsync(0)
244+
expect(handler).toBeCalledTimes(2)
245+
expect(handler).lastCalledWith([['delete', ['count2'], 2]])
246+
})
247+
248+
it('should notify nested ops', async () => {
249+
const obj = proxy<{ nested: { count?: number } }>({
250+
nested: { count: 0 },
251+
})
252+
const handler = vi.fn()
253+
254+
subscribe(obj, handler)
255+
256+
obj.nested.count = 1
257+
258+
await vi.advanceTimersByTimeAsync(0)
259+
expect(handler).toBeCalledTimes(1)
260+
expect(handler).lastCalledWith([['set', ['nested', 'count'], 1, 0]])
261+
262+
delete obj.nested.count
263+
264+
await vi.advanceTimersByTimeAsync(0)
265+
expect(handler).toBeCalledTimes(2)
266+
expect(handler).lastCalledWith([['delete', ['nested', 'count'], 1]])
267+
})
268+
})

0 commit comments

Comments
 (0)