@exodus/atoms 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/enforce-rules.js +53 -0
- package/src/enhancers/compute.js +20 -0
- package/src/enhancers/with-serialization.js +19 -0
- package/src/event-emitter.js +16 -0
- package/src/factories/fusion.js +29 -0
- package/src/factories/local-config.js +19 -0
- package/src/factories/mock.js +23 -0
- package/src/factories/remote-config.js +27 -0
- package/src/factories/storage.js +30 -0
- package/src/index.js +8 -0
- package/src/simple-observer.js +22 -0
- package/index.js +0 -184
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/atoms",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"main": "index.js",
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"main": "src/index.js",
|
|
5
5
|
"author": "Exodus Movement Inc.",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"lint:fix": "yarn lint --fix"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"src"
|
|
13
13
|
],
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"@exodus/storage-memory": "^1.0.0",
|
|
30
30
|
"delay": "^5.0.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "cc37441cf6d7f4b5bf63857ec87cf7cfb101a807"
|
|
33
33
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import makeConcurrent from 'make-concurrent'
|
|
2
|
+
import proxyFreeze from 'proxy-freeze'
|
|
3
|
+
|
|
4
|
+
const withChangeDetection = (listener) => {
|
|
5
|
+
let currentValue
|
|
6
|
+
let called = false
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
listener: async (value) => {
|
|
10
|
+
if (called && value === currentValue) return
|
|
11
|
+
|
|
12
|
+
called = true
|
|
13
|
+
currentValue = value
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await listener(currentValue)
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.warn('observer failed', err)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
get called() {
|
|
22
|
+
return called
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const enforceObservableRules = ({ defaultValue, ...atom }) => {
|
|
28
|
+
// ensure observers get called in series
|
|
29
|
+
const enqueue = makeConcurrent((fn) => fn(), { concurrency: 1 })
|
|
30
|
+
|
|
31
|
+
const postProcessValue = (value = defaultValue) =>
|
|
32
|
+
value && typeof value === 'object' ? proxyFreeze(value) : value
|
|
33
|
+
|
|
34
|
+
const get = () => atom.get().then(postProcessValue)
|
|
35
|
+
|
|
36
|
+
const observe = (listener) => {
|
|
37
|
+
const deduped = withChangeDetection(listener)
|
|
38
|
+
const publishSerially = (value) => enqueue(() => deduped.listener(postProcessValue(value)))
|
|
39
|
+
// note: call observe() first to give it a chance to throw if it's not supported
|
|
40
|
+
const unsubscribe = atom.observe(publishSerially)
|
|
41
|
+
// if the subscription already fired once, ignore first get
|
|
42
|
+
get().then((value) => !deduped.called && publishSerially(value))
|
|
43
|
+
return unsubscribe
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
get,
|
|
48
|
+
set: atom.set,
|
|
49
|
+
observe,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default enforceObservableRules
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import enforceObservableRules from '../enforce-rules'
|
|
2
|
+
|
|
3
|
+
const compute = ({ atom, selector }) => {
|
|
4
|
+
const get = async () => {
|
|
5
|
+
const values = await atom.get()
|
|
6
|
+
return selector(values)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const set = async () => {
|
|
10
|
+
throw new Error('selected atom does not support set')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const observe = (callback) => {
|
|
14
|
+
return atom.observe((values) => callback(selector(values), values))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return enforceObservableRules({ ...atom, get, set, observe })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default compute
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const withSerialization = ({ atom, serialize, deserialize }) => {
|
|
2
|
+
const get = async () => {
|
|
3
|
+
const serialized = await atom.get()
|
|
4
|
+
return deserialize(serialized)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const set = async (value) => {
|
|
8
|
+
const serialized = serialize(value)
|
|
9
|
+
return atom.set(serialized)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const observe = async (callback) => {
|
|
13
|
+
return atom.observe((value) => callback(deserialize(value), value))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { ...atom, get, set, observe }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default withSerialization
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import enforceObservableRules from './enforce-rules'
|
|
2
|
+
|
|
3
|
+
const fromEventEmitter = ({ emitter, event, get, defaultValue }) => {
|
|
4
|
+
const observe = (listener) => {
|
|
5
|
+
emitter.on(event, listener)
|
|
6
|
+
return () => emitter.removeListener(event, listener)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return enforceObservableRules({
|
|
10
|
+
get,
|
|
11
|
+
observe,
|
|
12
|
+
defaultValue,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default fromEventEmitter
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { get as getValueAtPath, set as setValueAtPath } from 'lodash'
|
|
2
|
+
|
|
3
|
+
import enforceObservableRules from '../enforce-rules'
|
|
4
|
+
|
|
5
|
+
const createFusionAtomFactory =
|
|
6
|
+
({ fusion }) =>
|
|
7
|
+
({ path, defaultValue }) => {
|
|
8
|
+
const set = async (value) => {
|
|
9
|
+
await fusion.mergeProfile(setValueAtPath({}, path, value))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const get = async () => {
|
|
13
|
+
const profile = await fusion.getProfile()
|
|
14
|
+
return getValueFromProfile(profile)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getValueFromProfile = (profile) => getValueAtPath(profile, path)
|
|
18
|
+
|
|
19
|
+
const observe = (listener) => fusion.subscribe(getValueFromProfile, listener)
|
|
20
|
+
|
|
21
|
+
return enforceObservableRules({
|
|
22
|
+
get,
|
|
23
|
+
set,
|
|
24
|
+
observe,
|
|
25
|
+
defaultValue,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default createFusionAtomFactory
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import createSimpleObserver from '../simple-observer'
|
|
2
|
+
import enforceObservableRules from '../enforce-rules'
|
|
3
|
+
|
|
4
|
+
const createLocalConfigAtomFactory =
|
|
5
|
+
({ localConfig }) =>
|
|
6
|
+
({ key, defaultValue }) => {
|
|
7
|
+
const { notify, observe } = createSimpleObserver()
|
|
8
|
+
|
|
9
|
+
localConfig.on(`update:${key}`, ({ current }) => notify(current))
|
|
10
|
+
|
|
11
|
+
return enforceObservableRules({
|
|
12
|
+
get: () => localConfig.get(key),
|
|
13
|
+
set: (value) => localConfig.set(key, value),
|
|
14
|
+
observe,
|
|
15
|
+
defaultValue,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default createLocalConfigAtomFactory
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
|
|
3
|
+
import fromEventEmitter from '../event-emitter'
|
|
4
|
+
|
|
5
|
+
const createAtomMock = ({ defaultValue }) => {
|
|
6
|
+
let latestValue = defaultValue
|
|
7
|
+
|
|
8
|
+
const emitter = new EventEmitter()
|
|
9
|
+
|
|
10
|
+
const get = async () => latestValue
|
|
11
|
+
|
|
12
|
+
const set = (data) => {
|
|
13
|
+
latestValue = data
|
|
14
|
+
emitter.emit('data', data)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
...fromEventEmitter({ emitter, event: 'data', get, defaultValue }),
|
|
19
|
+
set,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default createAtomMock
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { get as getValueAtPath } from 'lodash'
|
|
2
|
+
|
|
3
|
+
import createSimpleObserver from '../simple-observer'
|
|
4
|
+
import enforceObservableRules from '../enforce-rules'
|
|
5
|
+
|
|
6
|
+
const createRemoteConfigAtomFactory =
|
|
7
|
+
({ remoteConfig }) =>
|
|
8
|
+
({ path, defaultValue }) => {
|
|
9
|
+
const { notify, observe } = createSimpleObserver()
|
|
10
|
+
|
|
11
|
+
const get = () => remoteConfig.get(path)
|
|
12
|
+
|
|
13
|
+
const set = async () => {
|
|
14
|
+
throw new Error('remoteConfig is read-only')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
remoteConfig.on('sync', ({ current }) => notify(getValueAtPath(current, path)))
|
|
18
|
+
|
|
19
|
+
return enforceObservableRules({
|
|
20
|
+
get,
|
|
21
|
+
set,
|
|
22
|
+
observe,
|
|
23
|
+
defaultValue,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default createRemoteConfigAtomFactory
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import createSimpleObserver from '../simple-observer'
|
|
2
|
+
import enforceObservableRules from '../enforce-rules'
|
|
3
|
+
|
|
4
|
+
const createStorageAtomFactory =
|
|
5
|
+
({ storage }) =>
|
|
6
|
+
({ key, defaultValue, isSoleWriter }) => {
|
|
7
|
+
const { notify, observe } = createSimpleObserver({ enable: isSoleWriter })
|
|
8
|
+
|
|
9
|
+
const set = async (value) => {
|
|
10
|
+
if (typeof value === 'undefined') {
|
|
11
|
+
await storage.delete(key)
|
|
12
|
+
} else {
|
|
13
|
+
await storage.set(key, value)
|
|
14
|
+
}
|
|
15
|
+
if (isSoleWriter) {
|
|
16
|
+
await notify(value)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const get = () => storage.get(key)
|
|
21
|
+
|
|
22
|
+
return enforceObservableRules({
|
|
23
|
+
get,
|
|
24
|
+
set,
|
|
25
|
+
observe,
|
|
26
|
+
defaultValue,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default createStorageAtomFactory
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as createAtomMock } from './factories/mock'
|
|
2
|
+
export { default as createFusionAtomFactory } from './factories/fusion'
|
|
3
|
+
export { default as createStorageAtomFactory } from './factories/storage'
|
|
4
|
+
export { default as createLocalConfigAtomFactory } from './factories/local-config'
|
|
5
|
+
export { default as createRemoteConfigAtomFactory } from './factories/remote-config'
|
|
6
|
+
export { default as fromEventEmitter } from './event-emitter'
|
|
7
|
+
export { default as compute } from './enhancers/compute'
|
|
8
|
+
export { default as withSerialization } from './enhancers/with-serialization'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const createSimpleObserver = ({ enable = true } = {}) => {
|
|
2
|
+
let listeners = []
|
|
3
|
+
|
|
4
|
+
const notify = async (value) => Promise.all(listeners.map((listener) => listener(value)))
|
|
5
|
+
|
|
6
|
+
const observe = (listener) => {
|
|
7
|
+
if (!enable) {
|
|
8
|
+
throw new Error('observe method is not supported')
|
|
9
|
+
}
|
|
10
|
+
listeners.push(listener)
|
|
11
|
+
return () => {
|
|
12
|
+
listeners = listeners.filter((fn) => fn !== listener)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
observe,
|
|
18
|
+
notify,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default createSimpleObserver
|
package/index.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events'
|
|
2
|
-
import { get as getValueAtPath, set as setValueAtPath } from 'lodash'
|
|
3
|
-
import makeConcurrent from 'make-concurrent'
|
|
4
|
-
import proxyFreeze from 'proxy-freeze'
|
|
5
|
-
|
|
6
|
-
const withChangeDetection = (listener) => {
|
|
7
|
-
let currentValue
|
|
8
|
-
let called = false
|
|
9
|
-
return {
|
|
10
|
-
listener: async (value) => {
|
|
11
|
-
if (called && value === currentValue) return
|
|
12
|
-
|
|
13
|
-
called = true
|
|
14
|
-
currentValue = value
|
|
15
|
-
try {
|
|
16
|
-
await listener(currentValue)
|
|
17
|
-
} catch (err) {
|
|
18
|
-
console.warn('observer failed', err)
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
get called() {
|
|
22
|
-
return called
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const enforceObservableRules = ({ defaultValue, ...atom }) => {
|
|
28
|
-
// ensure observers get called in series
|
|
29
|
-
const enqueue = makeConcurrent((fn) => fn(), { concurrency: 1 })
|
|
30
|
-
|
|
31
|
-
const postProcessValue = (value = defaultValue) =>
|
|
32
|
-
value && typeof value === 'object' ? proxyFreeze(value) : value
|
|
33
|
-
const get = () => atom.get().then(postProcessValue)
|
|
34
|
-
|
|
35
|
-
const observe = (listener) => {
|
|
36
|
-
const deduped = withChangeDetection(listener)
|
|
37
|
-
const publishSerially = (value) => enqueue(() => deduped.listener(postProcessValue(value)))
|
|
38
|
-
// note: call observe() first to give it a chance to throw if it's not supported
|
|
39
|
-
const unsubscribe = atom.observe(publishSerially)
|
|
40
|
-
// if the subscription already fired once, ignore first get
|
|
41
|
-
get().then((value) => !deduped.called && publishSerially(value))
|
|
42
|
-
return unsubscribe
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
get,
|
|
47
|
-
set: atom.set,
|
|
48
|
-
observe,
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const createFusionAtomFactory =
|
|
53
|
-
({ fusion }) =>
|
|
54
|
-
({ path, defaultValue }) => {
|
|
55
|
-
const set = async (value) => {
|
|
56
|
-
await fusion.mergeProfile(setValueAtPath({}, path, value))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const get = async () => {
|
|
60
|
-
const profile = await fusion.getProfile()
|
|
61
|
-
return getValueFromProfile(profile)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const getValueFromProfile = (profile) => getValueAtPath(profile, path)
|
|
65
|
-
|
|
66
|
-
const observe = (listener) => fusion.subscribe(getValueFromProfile, listener)
|
|
67
|
-
|
|
68
|
-
return enforceObservableRules({
|
|
69
|
-
get,
|
|
70
|
-
set,
|
|
71
|
-
observe,
|
|
72
|
-
defaultValue,
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const createSimpleObserver = ({ enable = true } = {}) => {
|
|
77
|
-
let listeners = []
|
|
78
|
-
const notify = async (value) => Promise.all(listeners.map((listener) => listener(value)))
|
|
79
|
-
|
|
80
|
-
const observe = (listener) => {
|
|
81
|
-
if (!enable) {
|
|
82
|
-
throw new Error('observe method is not supported')
|
|
83
|
-
}
|
|
84
|
-
listeners.push(listener)
|
|
85
|
-
return () => {
|
|
86
|
-
listeners = listeners.filter((fn) => fn !== listener)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
observe,
|
|
92
|
-
notify,
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const createStorageAtomFactory =
|
|
97
|
-
({ storage }) =>
|
|
98
|
-
({ key, defaultValue, isSoleWriter }) => {
|
|
99
|
-
const { notify, observe } = createSimpleObserver({ enable: isSoleWriter })
|
|
100
|
-
|
|
101
|
-
const set = async (value) => {
|
|
102
|
-
if (typeof value === 'undefined') {
|
|
103
|
-
await storage.delete(key)
|
|
104
|
-
} else {
|
|
105
|
-
await storage.set(key, value)
|
|
106
|
-
}
|
|
107
|
-
if (isSoleWriter) {
|
|
108
|
-
await notify(value)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const get = () => storage.get(key)
|
|
113
|
-
|
|
114
|
-
return enforceObservableRules({
|
|
115
|
-
get,
|
|
116
|
-
set,
|
|
117
|
-
observe,
|
|
118
|
-
defaultValue,
|
|
119
|
-
})
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const createRemoteConfigAtomFactory =
|
|
123
|
-
({ remoteConfig }) =>
|
|
124
|
-
({ path, defaultValue }) => {
|
|
125
|
-
const { notify, observe } = createSimpleObserver()
|
|
126
|
-
|
|
127
|
-
const get = () => remoteConfig.get(path)
|
|
128
|
-
const set = async () => {
|
|
129
|
-
throw new Error('remoteConfig is read-only')
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
remoteConfig.on('sync', ({ current }) => notify(getValueAtPath(current, path)))
|
|
133
|
-
|
|
134
|
-
return enforceObservableRules({
|
|
135
|
-
get,
|
|
136
|
-
set,
|
|
137
|
-
observe,
|
|
138
|
-
defaultValue,
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export const createLocalConfigAtomFactory =
|
|
143
|
-
({ localConfig }) =>
|
|
144
|
-
({ key, defaultValue }) => {
|
|
145
|
-
const { notify, observe } = createSimpleObserver()
|
|
146
|
-
localConfig.on(`update:${key}`, ({ current }) => notify(current))
|
|
147
|
-
|
|
148
|
-
return enforceObservableRules({
|
|
149
|
-
get: () => localConfig.get(key),
|
|
150
|
-
set: (value) => localConfig.set(key, value),
|
|
151
|
-
observe,
|
|
152
|
-
defaultValue,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export const fromEventEmitter = ({ emitter, event, get, defaultValue }) => {
|
|
157
|
-
const observe = (listener) => {
|
|
158
|
-
emitter.on(event, listener)
|
|
159
|
-
return () => emitter.removeListener(event, listener)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return enforceObservableRules({
|
|
163
|
-
get,
|
|
164
|
-
observe,
|
|
165
|
-
defaultValue,
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export const createAtomMock = ({ defaultValue }) => {
|
|
170
|
-
let latestValue = defaultValue
|
|
171
|
-
|
|
172
|
-
const emitter = new EventEmitter()
|
|
173
|
-
const get = async () => latestValue
|
|
174
|
-
|
|
175
|
-
const set = (data) => {
|
|
176
|
-
latestValue = data
|
|
177
|
-
emitter.emit('data', data)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
...fromEventEmitter({ emitter, event: 'data', get, defaultValue }),
|
|
182
|
-
set,
|
|
183
|
-
}
|
|
184
|
-
}
|