@exodus/atoms 1.0.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/index.js +159 -0
- package/package.json +32 -0
package/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
export const createStorageAtomFactory =
|
|
77
|
+
({ storage }) =>
|
|
78
|
+
({ key, defaultValue }) => {
|
|
79
|
+
const set = async (value) => storage.set(key, value)
|
|
80
|
+
|
|
81
|
+
const get = () => storage.get(key)
|
|
82
|
+
|
|
83
|
+
return enforceObservableRules({
|
|
84
|
+
get,
|
|
85
|
+
set,
|
|
86
|
+
observe: async () => {
|
|
87
|
+
throw new Error('storage atom does not support observe')
|
|
88
|
+
},
|
|
89
|
+
defaultValue,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const createRemoteConfigAtomFactory =
|
|
94
|
+
({ remoteConfig }) =>
|
|
95
|
+
({ path, defaultValue }) => {
|
|
96
|
+
const getValue = (remoteConfigJSON) => getValueAtPath(remoteConfigJSON, path)
|
|
97
|
+
|
|
98
|
+
let listeners = []
|
|
99
|
+
|
|
100
|
+
const notify = async (remoteConfigJSON) => {
|
|
101
|
+
const value = getValue(remoteConfigJSON)
|
|
102
|
+
return Promise.all(listeners.map((listener) => listener(value)))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const get = async () => {
|
|
106
|
+
const remoteConfigJSON = await remoteConfig.get()
|
|
107
|
+
return getValue(remoteConfigJSON)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const set = async () => {
|
|
111
|
+
throw new Error('remoteConfig is read-only')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const observe = (listener) => {
|
|
115
|
+
listeners.push(listener)
|
|
116
|
+
return () => {
|
|
117
|
+
listeners = listeners.filter((fn) => fn !== listener)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
remoteConfig.on('remote-config', notify)
|
|
122
|
+
|
|
123
|
+
return enforceObservableRules({
|
|
124
|
+
get,
|
|
125
|
+
set,
|
|
126
|
+
observe,
|
|
127
|
+
defaultValue,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const fromEventEmitter = ({ emitter, event, get, defaultValue }) => {
|
|
132
|
+
const observe = (listener) => {
|
|
133
|
+
emitter.on(event, listener)
|
|
134
|
+
return () => emitter.removeListener(event, listener)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return enforceObservableRules({
|
|
138
|
+
get,
|
|
139
|
+
observe,
|
|
140
|
+
defaultValue,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const createAtomMock = ({ defaultValue }) => {
|
|
145
|
+
let latestValue = defaultValue
|
|
146
|
+
|
|
147
|
+
const emitter = new EventEmitter()
|
|
148
|
+
const get = async () => latestValue
|
|
149
|
+
|
|
150
|
+
const set = (data) => {
|
|
151
|
+
latestValue = data
|
|
152
|
+
emitter.emit('data', data)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
...fromEventEmitter({ emitter, event: 'data', get, defaultValue }),
|
|
157
|
+
set,
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exodus/atoms",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"author": "Exodus Movement Inc.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"lint:fix": "yarn lint --fix"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/ExodusMovement/exodus-hydra.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/ExodusMovement/exodus-hydra/tree/master/modules/atom",
|
|
19
|
+
"license": "UNLICENSED",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/ExodusMovement/exodus-hydra/issues?q=is%3Aissue+is%3Aopen+label%3Aatom"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"lodash": "^4.17.21",
|
|
25
|
+
"make-concurrent": ">=4 <6",
|
|
26
|
+
"proxy-freeze": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@exodus/storage-memory": "^1.0.0",
|
|
30
|
+
"delay": "^5.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|