Last active
August 7, 2020 11:11
-
-
Save LucaColonnello/e5a772a089c19725d90626f41628ba4a to your computer and use it in GitHub Desktop.
Redux like store with per slice subscription - using bit masks to propagate changes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<title>JS Bin</title> | |
</head> | |
<body> | |
<script id="jsbin-javascript"> | |
const initialState = { | |
count1: 0, | |
count2: 0, | |
}; | |
class UniqueKeysIdMap { | |
constructor() { | |
this.idCount = 0; | |
this.keysToId = new Map(); | |
} | |
add(key) { | |
if (this.keysToId.has(key)) { | |
return; | |
} | |
this.keysToId.set(key, ++this.idCount); | |
} | |
get(key) { | |
return this.keysToId.get(key); | |
} | |
has(key) { | |
return this.keysToId.has(key); | |
} | |
delete(key) { | |
this.keysToId.delete(key); | |
} | |
} | |
class StateManager { | |
constructor(initialState, reducer) { | |
this.uniqueKeysIdMap = new UniqueKeysIdMap(); | |
this.subscriptions = []; | |
this.state = initialState; | |
this.prevState = initialState; | |
this.reducer = reducer; | |
this.createUniqueKeysIdMap(); | |
} | |
createUniqueKeysIdMap() { | |
Object.keys(this.state).forEach(key => { | |
this.uniqueKeysIdMap.add(key); | |
}); | |
} | |
calculateChanged() { | |
// do it on reverse, save prevState as Set | |
// and remove the ones found in the first loop; | |
// the second loop should loop the set | |
// deleting the remaining ones from prevState, | |
// which will be the ones not found in the current state | |
const stateSet = new Set(Object.keys(this.state)); | |
let changed = 0; | |
Object.keys(this.state).forEach(key => { | |
if (this.state[key] !== this.prevState[key]) { | |
if (!this.uniqueKeysIdMap.has(key)) { | |
this.uniqueKeysIdMap.add(key); | |
} | |
changed |= this.uniqueKeysIdMap.get(key); | |
} | |
}); | |
Object.keys(this.prevState) | |
.filter(key => !stateSet.has(key)) | |
.forEach(key => { | |
this.uniqueKeysIdMap.delete(key); | |
}); | |
return changed; | |
} | |
dispatch(action) { | |
this.prevState = this.state; | |
this.state = this.reducer(this.state, action); | |
const changed = this.calculateChanged(); | |
this.subscriptions.filter( | |
({ handler, slices }) => | |
Boolean(slices.find(slice => ( | |
this.uniqueKeysIdMap.has(slice) && | |
(this.uniqueKeysIdMap.get(slice) & changed) | |
))) | |
) | |
.forEach(({ handler }) => { | |
handler(this.state); | |
}); | |
} | |
subscribe(handler, slices) { | |
const subscription = { | |
slices, | |
handler | |
}; | |
this.subscriptions.push(subscription); | |
return this.unsubscribe.bind(this, subscription); | |
} | |
unsubscribe(subscription) { | |
this.subscriptions.splice(this.subscriptions.findIndex(subscription), 1); | |
} | |
} | |
const reducer = (state, { payload: { whichOnes }, type }) => { | |
switch(type) { | |
case 'INCREMENT': | |
return { | |
...state, | |
count1: whichOnes.indexOf('count1') !== -1 ? state.count1 + 1 : state.count1, | |
count2: whichOnes.indexOf('count2') !== -1 ? state.count2 + 1 : state.count2, | |
}; | |
case 'DECREMENT': | |
return { | |
...state, | |
count1:(whichOnes.indexOf('count1') !== -1 && state.count1 !== 0) ? state.count1 - 1 : state.count1, | |
count2:(whichOnes.indexOf('count2') !== -1 && state.count2 !== 0) ? state.count2 - 1 : state.count2, | |
}; | |
} | |
}; | |
const state = new StateManager(initialState, reducer); | |
state.subscribe((state) => { | |
console.log('called count1', state.count1); | |
}, ['count1']); | |
state.subscribe((state) => { | |
console.log('called count2', state.count2); | |
}, ['count2']); | |
state.subscribe((state) => { | |
console.log('called both', state.count1, state.count2); | |
}, ['count1', 'count2']); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count2'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1', 'count2'] | |
} | |
}); | |
</script> | |
<script id="jsbin-source-javascript" type="text/javascript">const initialState = { | |
count1: 0, | |
count2: 0, | |
}; | |
class UniqueKeysIdMap { | |
constructor() { | |
this.idCount = 0; | |
this.keysToId = new Map(); | |
} | |
add(key) { | |
if (this.keysToId.has(key)) { | |
return; | |
} | |
this.keysToId.set(key, ++this.idCount); | |
} | |
get(key) { | |
return this.keysToId.get(key); | |
} | |
has(key) { | |
return this.keysToId.has(key); | |
} | |
delete(key) { | |
this.keysToId.delete(key); | |
} | |
} | |
class StateManager { | |
constructor(initialState, reducer) { | |
this.uniqueKeysIdMap = new UniqueKeysIdMap(); | |
this.subscriptions = []; | |
this.state = initialState; | |
this.prevState = initialState; | |
this.reducer = reducer; | |
this.createUniqueKeysIdMap(); | |
} | |
createUniqueKeysIdMap() { | |
Object.keys(this.state).forEach(key => { | |
this.uniqueKeysIdMap.add(key); | |
}); | |
} | |
calculateChanged() { | |
// do it on reverse, save prevState as Set | |
// and remove the ones found in the first loop; | |
// the second loop should loop the set | |
// deleting the remaining ones from prevState, | |
// which will be the ones not found in the current state | |
const stateSet = new Set(Object.keys(this.state)); | |
let changed = 0; | |
Object.keys(this.state).forEach(key => { | |
if (this.state[key] !== this.prevState[key]) { | |
if (!this.uniqueKeysIdMap.has(key)) { | |
this.uniqueKeysIdMap.add(key); | |
} | |
changed |= this.uniqueKeysIdMap.get(key); | |
} | |
}); | |
Object.keys(this.prevState) | |
.filter(key => !stateSet.has(key)) | |
.forEach(key => { | |
this.uniqueKeysIdMap.delete(key); | |
}); | |
return changed; | |
} | |
dispatch(action) { | |
this.prevState = this.state; | |
this.state = this.reducer(this.state, action); | |
const changed = this.calculateChanged(); | |
this.subscriptions.filter( | |
({ handler, slices }) => | |
Boolean(slices.find(slice => ( | |
this.uniqueKeysIdMap.has(slice) && | |
(this.uniqueKeysIdMap.get(slice) & changed) | |
))) | |
) | |
.forEach(({ handler }) => { | |
handler(this.state); | |
}); | |
} | |
subscribe(handler, slices) { | |
const subscription = { | |
slices, | |
handler | |
}; | |
this.subscriptions.push(subscription); | |
return this.unsubscribe.bind(this, subscription); | |
} | |
unsubscribe(subscription) { | |
this.subscriptions.splice(this.subscriptions.findIndex(subscription), 1); | |
} | |
} | |
const reducer = (state, { payload: { whichOnes }, type }) => { | |
switch(type) { | |
case 'INCREMENT': | |
return { | |
...state, | |
count1: whichOnes.indexOf('count1') !== -1 ? state.count1 + 1 : state.count1, | |
count2: whichOnes.indexOf('count2') !== -1 ? state.count2 + 1 : state.count2, | |
}; | |
case 'DECREMENT': | |
return { | |
...state, | |
count1:(whichOnes.indexOf('count1') !== -1 && state.count1 !== 0) ? state.count1 - 1 : state.count1, | |
count2:(whichOnes.indexOf('count2') !== -1 && state.count2 !== 0) ? state.count2 - 1 : state.count2, | |
}; | |
} | |
}; | |
const state = new StateManager(initialState, reducer); | |
state.subscribe((state) => { | |
console.log('called count1', state.count1); | |
}, ['count1']); | |
state.subscribe((state) => { | |
console.log('called count2', state.count2); | |
}, ['count2']); | |
state.subscribe((state) => { | |
console.log('called both', state.count1, state.count2); | |
}, ['count1', 'count2']); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count2'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1', 'count2'] | |
} | |
}); | |
</script></body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const initialState = { | |
count1: 0, | |
count2: 0, | |
}; | |
class UniqueKeysIdMap { | |
constructor() { | |
this.idCount = 0; | |
this.keysToId = new Map(); | |
} | |
add(key) { | |
if (this.keysToId.has(key)) { | |
return; | |
} | |
this.keysToId.set(key, ++this.idCount); | |
} | |
get(key) { | |
return this.keysToId.get(key); | |
} | |
has(key) { | |
return this.keysToId.has(key); | |
} | |
delete(key) { | |
this.keysToId.delete(key); | |
} | |
} | |
class StateManager { | |
constructor(initialState, reducer) { | |
this.uniqueKeysIdMap = new UniqueKeysIdMap(); | |
this.subscriptions = []; | |
this.state = initialState; | |
this.prevState = initialState; | |
this.reducer = reducer; | |
this.createUniqueKeysIdMap(); | |
} | |
createUniqueKeysIdMap() { | |
Object.keys(this.state).forEach(key => { | |
this.uniqueKeysIdMap.add(key); | |
}); | |
} | |
calculateChanged() { | |
// do it on reverse, save prevState as Set | |
// and remove the ones found in the first loop; | |
// the second loop should loop the set | |
// deleting the remaining ones from prevState, | |
// which will be the ones not found in the current state | |
const stateSet = new Set(Object.keys(this.state)); | |
let changed = 0; | |
Object.keys(this.state).forEach(key => { | |
if (this.state[key] !== this.prevState[key]) { | |
if (!this.uniqueKeysIdMap.has(key)) { | |
this.uniqueKeysIdMap.add(key); | |
} | |
changed |= this.uniqueKeysIdMap.get(key); | |
} | |
}); | |
Object.keys(this.prevState) | |
.filter(key => !stateSet.has(key)) | |
.forEach(key => { | |
this.uniqueKeysIdMap.delete(key); | |
}); | |
return changed; | |
} | |
dispatch(action) { | |
this.prevState = this.state; | |
this.state = this.reducer(this.state, action); | |
const changed = this.calculateChanged(); | |
this.subscriptions.filter( | |
({ handler, slices }) => | |
Boolean(slices.find(slice => ( | |
this.uniqueKeysIdMap.has(slice) && | |
(this.uniqueKeysIdMap.get(slice) & changed) | |
))) | |
) | |
.forEach(({ handler }) => { | |
handler(this.state); | |
}); | |
} | |
subscribe(handler, slices) { | |
const subscription = { | |
slices, | |
handler | |
}; | |
this.subscriptions.push(subscription); | |
return this.unsubscribe.bind(this, subscription); | |
} | |
unsubscribe(subscription) { | |
this.subscriptions.splice(this.subscriptions.findIndex(subscription), 1); | |
} | |
} | |
const reducer = (state, { payload: { whichOnes }, type }) => { | |
switch(type) { | |
case 'INCREMENT': | |
return { | |
...state, | |
count1: whichOnes.indexOf('count1') !== -1 ? state.count1 + 1 : state.count1, | |
count2: whichOnes.indexOf('count2') !== -1 ? state.count2 + 1 : state.count2, | |
}; | |
case 'DECREMENT': | |
return { | |
...state, | |
count1:(whichOnes.indexOf('count1') !== -1 && state.count1 !== 0) ? state.count1 - 1 : state.count1, | |
count2:(whichOnes.indexOf('count2') !== -1 && state.count2 !== 0) ? state.count2 - 1 : state.count2, | |
}; | |
} | |
}; | |
const state = new StateManager(initialState, reducer); | |
state.subscribe((state) => { | |
console.log('called count1', state.count1); | |
}, ['count1']); | |
state.subscribe((state) => { | |
console.log('called count2', state.count2); | |
}, ['count2']); | |
state.subscribe((state) => { | |
console.log('called both', state.count1, state.count2); | |
}, ['count1', 'count2']); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count2'] | |
} | |
}); | |
state.dispatch({ | |
type: 'INCREMENT', | |
payload: { | |
whichOnes: ['count1', 'count2'] | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment