@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.
Files changed (2) hide show
  1. package/index.js +159 -0
  2. 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
+ }