Skip to content

Commit

Permalink
Partial progress for array implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vladnicula committed Nov 29, 2023
1 parent f63b2c4 commit 748ee25
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 54 deletions.
282 changes: 240 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,38 @@ export class MutationsManager {
): T => {
const mutationProxies = this.mutationMaps.get(target)
let proxy = mutationProxies?.get(subTarget) as T | undefined
if ( !proxy ) {
proxy = new Proxy(subTarget, new ProxyMutationObjectHandler({
target: subTarget,
selectorPointerArray: selectorTreePointer,
mutationNode: relevantMutationPointer,
dirtyPaths: this.mutationDirtyPaths.get(target) as Set<ProxyMutationObjectHandler<object>>,
// pathArray: currentPathArray,
proxyfyAccess: <T extends ObjectTree>(
subentityFromTarget: T,
relevantMutationPointer: MutationTreeNode,
relevantSelectionPointers: SelectorTreeBranch[],
// someOtherPathArray: string[]
) => {
return this.getSubProxy(
target,
relevantMutationPointer,
relevantSelectionPointers,
subentityFromTarget,
// someOtherPathArray
)
}
}) as ProxyHandler<T>) as T
mutationProxies?.set(subTarget, proxy)
if ( proxy ) {
return proxy
}
const subProxyParams = {
target: subTarget,
selectorPointerArray: selectorTreePointer,
mutationNode: relevantMutationPointer,
dirtyPaths: this.mutationDirtyPaths.get(target) as Set<ProxyMutationObjectHandler<object>>,
// pathArray: currentPathArray,
proxyfyAccess: <T extends ObjectTree>(
subentityFromTarget: T,
relevantMutationPointer: MutationTreeNode,
relevantSelectionPointers: SelectorTreeBranch[],
// someOtherPathArray: string[]
) => {
return this.getSubProxy(
target,
relevantMutationPointer,
relevantSelectionPointers,
subentityFromTarget,
// someOtherPathArray
)
}
}

const proxyHandler = Array.isArray(subTarget)
? new ProxyMutationArrayHandler(subProxyParams as any) as ProxyHandler<T>
: new ProxyMutationObjectHandler(subProxyParams) as ProxyHandler<T>

proxy = new Proxy(subTarget, proxyHandler) as T

mutationProxies?.set(subTarget, proxy)

return proxy
}
Expand Down Expand Up @@ -452,17 +460,10 @@ export class ProxyMutationObjectHandler<T extends object> {
}

const subEntity = target[prop]
if (ProxyCache.exists(subEntity as unknown as object)) {
return subEntity
}
if (typeof subEntity === 'object' && subEntity !== null) {
// There is no way of knowing if an object is a Proxy or not.
// In order for us to avoid creating Proxies in Proxies, we are
// caching the already created ones and if in the future we
// need them, we are just getting them from the cache
// Other framerworks/libs use a symbol to mark their proxies
// with it and check for that symbol.
if (ProxyCache.exists(subEntity as unknown as object)) {
return subEntity
}

const { selectorPointerArray } = this
const subPropSelectionPointers = selectorPointerArray.reduce((acc: SelectorTreeBranch[], item) => {
const descendentPointers = getRefDescedents(
Expand Down Expand Up @@ -499,7 +500,7 @@ export class ProxyMutationObjectHandler<T extends object> {
// console.log('set handler called', [prop, value], this.path)
// TODO consider moving this from a global into a normal var
if ( prop === 'length' && Array.isArray(target) ) {
return true
return Reflect.set(target, prop, value)
}
// console.log(`set`, target, prop, value)

Expand Down Expand Up @@ -607,7 +608,6 @@ export class ProxyMutationObjectHandler<T extends object> {
deleteProperty <K extends keyof T>(target: T, prop: K) {
if (prop in target) {
if ( typeof prop === 'string' ) {

this.writeSelectorPointerArray.push(
...this.selectorPointerArray.reduce((acc: SelectorTreeBranch[], item) => {
const descendentPointers = getRefDescedents(
Expand Down Expand Up @@ -643,13 +643,6 @@ export class ProxyMutationObjectHandler<T extends object> {
if ( typeof opOriginal === 'object' && opOriginal !== null ) {
opOriginal = {...opOriginal} as Partial<T>[K & string]
}

// this.ops.push({
// op: 'remove',
// path: `${prop}`,
// old: opOriginal,
// value: undefined
// })
}
}

Expand Down Expand Up @@ -687,6 +680,211 @@ export class ProxyMutationObjectHandler<T extends object> {
}
}


export class ProxyMutationArrayHandler<T extends Array<any>> {
// readonly pathArray: string[]
readonly deleted: Record<string, boolean> = {}

readonly original: T = [] as unknown as T

// parent set this (origin mutation manager)
// targetRef is the current object that is proxied over?
// or is it the ROOT of the mutation? It is the target
// of the proxy, so the current object that is being
// used by the proxy.
// COULD BE CLOUSER BASED
readonly targetRef: T

/**
* ops are the individual operations happening on this
* object. All the intermediary entities that would
* most probably dissapear with the new change.
*/
// readonly ops: JSONPatch[] = []

// parent set this (origin mutation manager)
// COULD BE CLOUSER BASED
readonly dirtyPaths: Set<ProxyMutationArrayHandler<T>>

// parent set this (origin mutation manager)
// call that can create new proxies, managed by mutation manager
// COULD BE CLOUSER BASED
readonly proxyfyAccess: ProxyAccessFN

// parent set this (origin mutation manager)
// contains the starting selection pointers for this root, then
// for each sublevel
// COULD BE CLOUSER BASED
readonly selectorPointerArray: Array<SelectorTreeBranch>

// locally created, then sent over array for writes. Probably can
// be imrpvoed
readonly writeSelectorPointerArray: Array<SelectorTreeBranch> = []


mutationNode: MutationTreeNode

constructor (params: {
mutationNode: MutationTreeNode,
target: T,
selectorPointerArray: Array<SelectorTreeBranch>,
dirtyPaths: Set<ProxyMutationArrayHandler<T>>,
proxyfyAccess: ProxyAccessFN
}) {
const { target, proxyfyAccess, dirtyPaths} = params
this.targetRef = target
this.proxyfyAccess = proxyfyAccess
this.dirtyPaths = dirtyPaths
this.selectorPointerArray = params.selectorPointerArray
this.mutationNode = params.mutationNode
}

get <K extends keyof T>(target: T, prop: K) {
switch (prop) {
case 'splice':
return (...args: [number, ...number[]]) => {
// splice should be transactional, meaning a
// mutate that does not success should not
// end up writing in the array.

// slice should simply say "hey I am deleting
// the indexes provided between start and end"

// for now we won't deal with adding items that
// get inserted in the array or the -1 count for
// the end index.
const [start, end] = args

// we should announce that we are removing the
// indexes between start and end in the mutations
// and dirty paths and what not.

for ( let i = start; i < end; i += 1 ) {

this.writeSelectorPointerArray.push(
...this.selectorPointerArray.reduce((acc: SelectorTreeBranch[], item) => {
const descendentPointers = getRefDescedents(
item,
i
)
if ( descendentPointers ) {
acc.push(...descendentPointers)
}
return acc
}, [])
)
const childMutationPointer = makeAndGetChildPointer(
this.mutationNode,
i
)

createMutaitonInMutationTree(
childMutationPointer,
target[i],
NO_VALUE
)

this.deleted[i] = true
}

this.dirtyPaths.add(this)


console.log('splice happening', target, args)
return target.splice(...args)
}
case 'push':
return (...args: any[]) => {
const pushSymbol = '-'
this.writeSelectorPointerArray.push(
...this.selectorPointerArray.reduce((acc: SelectorTreeBranch[], item) => {
const descendentPointers = getRefDescedents(
item,
pushSymbol
)
if ( descendentPointers ) {
acc.push(...descendentPointers)
}
return acc
}, [])
)

const childMutationPointer = makeAndGetChildPointer(
this.mutationNode,
pushSymbol
)

createMutaitonInMutationTree(
childMutationPointer,
undefined,
args
)

this.dirtyPaths.add(this)

return target.push(...args)
}
}
return Reflect.get(target, prop);
}

//get set deleteProperty getOwnPropertyDescriptor ownKeys has
set <K extends keyof T>(target: T, prop: K, value: T[K]) {
console.log('set happening', target, prop, value)
this.writeSelectorPointerArray.push(
...this.selectorPointerArray.reduce((acc: SelectorTreeBranch[], item) => {
const descendentPointers = getRefDescedents(
item,
prop as number
)
if ( descendentPointers ) {
acc.push(...descendentPointers)
}
return acc
}, [])
)

const childMutationPointer = makeAndGetChildPointer(
this.mutationNode,
prop as number
)

createMutaitonInMutationTree(
childMutationPointer,
target[prop as number],
value
)

this.dirtyPaths.add(this)

return Reflect.set(target, prop, value)
}

/**
* Proxy trap for delete keyword
*/
deleteProperty <K extends keyof T>(target: T, prop: K) {
console.log('delete happening', target, prop)
return Reflect.deleteProperty(target, prop)
}

/**
* Proxy trap for Object.getOwnPropertyDescriptor()
*/
getOwnPropertyDescriptor <K extends keyof T>(target: T, prop: K) {
console.log('getOwnPropertyDescriptor happening', target, prop)
return Reflect.getOwnPropertyDescriptor(target, prop)
}

/**
* Proxy trap for when looking at what keys we have
*/
ownKeys (target: T) {
console.log('ownKeys happening', target)
return Reflect.ownKeys(target)
}
}

export const pathMatchesSource = (source: string[], target: string[] ) => {
if ( source.indexOf('**') === -1 && source.length !== target.length ) {
return false
Expand Down
17 changes: 8 additions & 9 deletions tests/array/baisc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ describe('basic select over array', () => {

// console.log('patches', patches)
expect(patches).toHaveLength(1)
expect(patches![0].pathArray).toStrictEqual(['words', '2'])
expect(patches![0].value).toBe('!')
expect(patches![0].pathArray).toStrictEqual(['words', '-'])
expect(patches![0].value).toStrictEqual(['!'])

selector.dispose()
})
Expand Down Expand Up @@ -103,7 +103,7 @@ describe('basic select over array', () => {
selector.dispose()
})

it('removeing via delete', () => {
it.only('removeing via delete', () => {
const state = {
words: ['hello', 'world', '!']
}
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('basic select over array', () => {
// should be a replace with undefined not a remove, but it would require
// implementing a different mutation logic for arrays.
expect(patches![0].op).toBe('remove')
expect(patches![0].pathArray).toStrictEqual(['words', '1'])
expect(patches![0].pathArray).toEqual(['words', 0])
expect(patches![0].value).toBe(undefined)

selector.dispose()
Expand All @@ -162,11 +162,10 @@ describe('basic select over array', () => {
modifiable.words.splice(0, 1)
})

// console.log('patches', patches)

// not ideal, it will "replace" all items in the array that are not deleted,
// but it is a true operation actually.
expect(patches).toHaveLength(3)
expect(state.words).toHaveLength(2)
expect(patches).toHaveLength(1)
expect(patches![0].op).toBe('remove')
expect(patches![0].pathArray).toEqual(['words', 0])

selector.dispose()
})
Expand Down
6 changes: 3 additions & 3 deletions tests/selector-computations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ describe('selector reaction to ancestors', () => {
const callbackSpy = vi.fn()
select(myState, ["nodesById/2/styles"], () => {
callbackSpy("nodesById/2/styles selector running");
console.log("nodesById/2/styles selector running");
// console.log("nodesById/2/styles selector running");
})

select(myState, ["nodesById/2/*"], () => {
callbackSpy("nodesById/2/* selector running")
console.log("nodesById/2/* selector running")
// console.log("nodesById/2/* selector running")
})

mutate(myState, (doc) => {
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('selector reaction to ancestors', () => {

select(myState, ["nodesById/*"], () => {
callbackSpy("nodesById/* selector running")
console.log("nodesById/* selector running")
// console.log("nodesById/* selector running")
})

mutate(myState, (doc) => {
Expand Down

0 comments on commit 748ee25

Please sign in to comment.