@exodus/atoms 2.1.0 → 2.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/atoms",
3
- "version": "2.1.0",
4
- "main": "index.js",
3
+ "version": "2.3.0",
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
- "index.js"
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": "231a041f31a91990024a1282067b737a3c9dfe41"
32
+ "gitHead": "0aa83431337d91d8a519aaa77fb3512c4e5ed3cc"
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,17 @@
1
+ import { compute } from '../index'
2
+
3
+ const createEnabledAssetsAtom = ({ enabledAndDisabledAssetsAtom }) => {
4
+ const selector = (values) =>
5
+ Object.fromEntries(
6
+ Object.keys(values.disabled)
7
+ .filter((assetName) => {
8
+ const disabled = values.disabled[assetName]
9
+ return disabled === false
10
+ })
11
+ .map((assetName) => [assetName, true])
12
+ )
13
+
14
+ return compute({ atom: enabledAndDisabledAssetsAtom, selector })
15
+ }
16
+
17
+ export default createEnabledAssetsAtom
@@ -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,9 @@
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 createEnabledAssetsAtom } from './factories/enabled-assets'
7
+ export { default as fromEventEmitter } from './event-emitter'
8
+ export { default as compute } from './enhancers/compute'
9
+ 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
- }