UNPKG

14 kBJavaScriptView Raw
1import { useLayoutEffect, useState, useRef, useEffect, useMemo, createElement, useCallback, useDebugValue } from 'react';
2import { createContext, useContext, useContextSelector } from 'use-context-selector';
3
4function _extends() {
5 _extends = Object.assign || function (target) {
6 for (var i = 1; i < arguments.length; i++) {
7 var source = arguments[i];
8
9 for (var key in source) {
10 if (Object.prototype.hasOwnProperty.call(source, key)) {
11 target[key] = source[key];
12 }
13 }
14 }
15
16 return target;
17 };
18
19 return _extends.apply(this, arguments);
20}
21
22const isSSR = typeof window === 'undefined' || /ServerSideRendering/.test(window.navigator && window.navigator.userAgent);
23const useIsoLayoutEffect = isSSR ? fn => fn() : useLayoutEffect;
24
25const appendMap = (dst, src) => {
26 src.forEach((v, k) => {
27 dst.set(k, v);
28 });
29 return dst;
30}; // create new map from two maps
31
32
33const concatMap = (src1, src2) => {
34 const dst = new Map();
35 src1.forEach((v, k) => {
36 dst.set(k, v);
37 });
38 src2.forEach((v, k) => {
39 dst.set(k, v);
40 });
41 return dst;
42};
43
44const warningObject = new Proxy({}, {
45 get() {
46 throw new Error('Please use <Provider>');
47 },
48
49 apply() {
50 throw new Error('Please use <Provider>');
51 }
52
53});
54
55const addDependent = (dependentsMap, atom, dependent) => {
56 let dependents = dependentsMap.get(atom);
57
58 if (!dependents) {
59 dependents = new Set();
60 dependentsMap.set(atom, dependents);
61 }
62
63 dependents.add(dependent);
64};
65
66const deleteDependent = (dependentsMap, atom, dependent) => {
67 const dependents = dependentsMap.get(atom);
68
69 if (dependents && dependents.has(dependent)) {
70 dependents.delete(dependent);
71 return dependents.size === 0; // empty
72 }
73
74 return false; // not found
75};
76
77const listDependents = (dependentsMap, atom) => {
78 const dependents = dependentsMap.get(atom);
79 return dependents || new Set();
80};
81
82const initialState = new Map();
83
84const getAtomStateValue = (state, atom) => {
85 const atomState = state.get(atom);
86 return atomState ? atomState.value : atom.init;
87};
88
89const readAtom = (state, readingAtom, setState, dependentsMap) => {
90 const readAtomValue = (prevState, atom, dependent) => {
91 if (dependent) {
92 addDependent(dependentsMap, atom, dependent);
93 }
94
95 const partialState = new Map();
96 const atomState = prevState.get(atom);
97
98 if (atomState) {
99 return [atomState, partialState];
100 }
101
102 let error = undefined;
103 let promise = undefined;
104 let value = null;
105 let isSync = true;
106
107 try {
108 const promiseOrValue = atom.read(a => {
109 if (a !== atom) {
110 const [nextAtomState, nextPartialState] = readAtomValue(prevState, a, atom);
111
112 if (isSync) {
113 appendMap(partialState, nextPartialState);
114 } else {
115 setState(prev => appendMap(new Map(prev), nextPartialState));
116 }
117
118 if (nextAtomState.error) {
119 throw nextAtomState.error;
120 }
121
122 if (nextAtomState.promise) {
123 throw nextAtomState.promise;
124 }
125
126 return nextAtomState.value;
127 } // primitive atom
128
129
130 const aState = prevState.get(a);
131
132 if (aState) {
133 if (aState.promise) {
134 throw aState.promise;
135 }
136
137 return aState.value;
138 }
139
140 return a.init; // this should not be undefined
141 });
142
143 if (promiseOrValue instanceof Promise) {
144 promise = promiseOrValue.then(value => {
145 setState(prev => new Map(prev).set(atom, {
146 value
147 }));
148 }).catch(e => {
149 setState(prev => new Map(prev).set(atom, {
150 value: getAtomStateValue(prev, atom),
151 error: e instanceof Error ? e : new Error(e)
152 }));
153 });
154 } else {
155 value = promiseOrValue;
156 }
157 } catch (errorOrPromise) {
158 if (errorOrPromise instanceof Promise) {
159 promise = errorOrPromise;
160 } else if (errorOrPromise instanceof Error) {
161 error = errorOrPromise;
162 } else {
163 error = new Error(errorOrPromise);
164 }
165 }
166
167 const nextAtomState = {
168 error,
169 promise,
170 value: promise ? atom.init : value
171 };
172 partialState.set(atom, nextAtomState);
173 isSync = false;
174 return [nextAtomState, partialState];
175 };
176
177 return readAtomValue(state, readingAtom, null);
178};
179
180const addAtom = (id, atom, partialState, setState, dependentsMap) => {
181 addDependent(dependentsMap, atom, id);
182
183 if (partialState) {
184 setState(prev => appendMap(new Map(prev), partialState));
185 }
186};
187
188const delAtom = (id, setState, dependentsMap, gcRequiredRef) => {
189 const deleteAtomState = (prevState, dependent) => {
190 prevState.forEach((_atomState, atom) => {
191 deleteDependent(dependentsMap, atom, dependent);
192 });
193 return new Map(prevState); // to re-render
194 };
195
196 gcRequiredRef.current = true;
197 setState(prev => deleteAtomState(prev, id));
198};
199
200const gcAtom = (state, setState, dependentsMap) => {
201 const gcAtomState = prevState => {
202 const nextState = new Map(prevState);
203
204 while (true) {
205 let deleted = false;
206 nextState.forEach((_atomState, atom) => {
207 var _dependentsMap$get;
208
209 const isEmpty = ((_dependentsMap$get = dependentsMap.get(atom)) == null ? void 0 : _dependentsMap$get.size) === 0;
210
211 if (isEmpty) {
212 nextState.delete(atom);
213 dependentsMap.delete(atom);
214 deleted = true;
215 }
216 });
217
218 if (!deleted) {
219 break;
220 }
221 }
222
223 return nextState;
224 };
225
226 const nextState = gcAtomState(state);
227
228 if (state.size !== nextState.size) {
229 setState(nextState);
230 }
231};
232
233const writeAtom = (updatingAtom, update, dependentsMap, addWriteThunk) => {
234 const updateDependentsState = (prevState, atom) => {
235 const partialState = new Map();
236 listDependents(dependentsMap, atom).forEach(dependent => {
237 if (typeof dependent === 'symbol') return;
238 const v = dependent.read(a => {
239 if (a !== dependent) {
240 addDependent(dependentsMap, a, dependent);
241 }
242
243 return getAtomStateValue(prevState, a);
244 });
245
246 if (v instanceof Promise) {
247 const promise = v.then(vv => {
248 const nextAtomState = {
249 value: vv
250 };
251 addWriteThunk(prev => {
252 const nextState = new Map(prev).set(dependent, nextAtomState);
253 const nextPartialState = updateDependentsState(nextState, dependent);
254 return appendMap(nextState, nextPartialState);
255 });
256 }).catch(e => {
257 addWriteThunk(prev => new Map(prev).set(dependent, {
258 value: getAtomStateValue(prev, dependent),
259 error: e instanceof Error ? e : new Error(e)
260 }));
261 });
262 partialState.set(dependent, {
263 value: getAtomStateValue(prevState, dependent),
264 promise
265 });
266 } else {
267 partialState.set(dependent, {
268 value: v
269 });
270 appendMap(partialState, updateDependentsState(concatMap(prevState, partialState), dependent));
271 }
272 });
273 return partialState;
274 };
275
276 const updateAtomState = (prevState, atom, update) => {
277 const partialState = new Map();
278 let isSync = true;
279
280 try {
281 const promise = atom.write(a => getAtomStateValue(concatMap(prevState, partialState), a), (a, v) => {
282 if (a === atom) {
283 const nextAtomState = {
284 value: v
285 };
286
287 if (isSync) {
288 partialState.set(a, nextAtomState);
289 appendMap(partialState, updateDependentsState(concatMap(prevState, partialState), a));
290 } else {
291 addWriteThunk(prev => {
292 const nextState = new Map(prev).set(a, nextAtomState);
293 const nextPartialState = updateDependentsState(nextState, a);
294 return appendMap(nextState, nextPartialState);
295 });
296 }
297 } else {
298 if (isSync) {
299 const nextPartialState = updateAtomState(prevState, a, v);
300 appendMap(partialState, nextPartialState);
301 } else {
302 addWriteThunk(prev => {
303 const nextPartialState = updateAtomState(prev, a, v);
304 return appendMap(new Map(prev), nextPartialState);
305 });
306 }
307 }
308 }, update);
309
310 if (promise instanceof Promise) {
311 const nextAtomState = {
312 value: getAtomStateValue(concatMap(prevState, partialState), atom),
313 promise: promise.then(() => {
314 addWriteThunk(prev => new Map(prev).set(atom, {
315 value: getAtomStateValue(prev, atom),
316 promise: undefined
317 }));
318 }).catch(e => {
319 addWriteThunk(prev => new Map(prev).set(atom, {
320 value: getAtomStateValue(prev, atom),
321 error: e instanceof Error ? e : new Error(e)
322 }));
323 })
324 };
325 partialState.set(atom, nextAtomState);
326 }
327 } catch (e) {
328 const nextAtomState = {
329 value: getAtomStateValue(concatMap(prevState, partialState), atom),
330 error: e instanceof Error ? e : new Error(e)
331 };
332 partialState.set(atom, nextAtomState);
333 }
334
335 isSync = false;
336 return partialState;
337 };
338
339 addWriteThunk(prevState => {
340 const updatingAtomState = prevState.get(updatingAtom);
341
342 if (updatingAtomState && updatingAtomState.promise) {
343 // schedule update after promise is resolved
344 const promise = updatingAtomState.promise.then(() => {
345 const updateState = updateAtomState(prevState, updatingAtom, update);
346 addWriteThunk(prev => appendMap(new Map(prev), updateState));
347 });
348 return new Map(prevState).set(updatingAtom, _extends({}, updatingAtomState, {
349 promise
350 }));
351 } else {
352 const updateState = updateAtomState(prevState, updatingAtom, update);
353 return appendMap(new Map(prevState), updateState);
354 }
355 });
356};
357
358const runWriteThunk = (lastStateRef, setState, writeThunkQueue) => {
359 while (true) {
360 if (lastStateRef.current === null) {
361 return;
362 }
363
364 if (writeThunkQueue.length === 0) {
365 return;
366 }
367
368 const thunk = writeThunkQueue.shift();
369 const lastState = lastStateRef.current;
370 const nextState = thunk(lastState);
371
372 if (nextState !== lastState) {
373 setState(nextState);
374 return;
375 }
376 }
377};
378
379const ActionsContext = createContext(warningObject);
380const StateContext = createContext(warningObject);
381const Provider = ({
382 children
383}) => {
384 const [state, setStateOrig] = useState(initialState);
385
386 const setState = setStateAction => {
387 lastStateRef.current = null;
388 setStateOrig(setStateAction);
389 };
390
391 const dependentsMapRef = useRef();
392
393 if (!dependentsMapRef.current) {
394 dependentsMapRef.current = new WeakMap();
395 }
396
397 const gcRequiredRef = useRef(false);
398 useEffect(() => {
399 if (!gcRequiredRef.current) {
400 return;
401 }
402
403 gcAtom(state, setState, dependentsMapRef.current);
404 gcRequiredRef.current = false;
405 }, [state]);
406 const lastStateRef = useRef(null);
407 useIsoLayoutEffect(() => {
408 lastStateRef.current = state;
409 });
410 const writeThunkQueueRef = useRef([]);
411 useEffect(() => {
412 runWriteThunk(lastStateRef, setState, writeThunkQueueRef.current);
413 }, [state]);
414 const actions = useMemo(() => ({
415 add: (id, atom, partialState) => addAtom(id, atom, partialState, setState, dependentsMapRef.current),
416 del: id => delAtom(id, setState, dependentsMapRef.current, gcRequiredRef),
417 read: (state, atom) => readAtom(state, atom, setState, dependentsMapRef.current),
418 write: (atom, update) => {
419 writeAtom(atom, update, dependentsMapRef.current, thunk => {
420 writeThunkQueueRef.current.push(thunk);
421 runWriteThunk(lastStateRef, setState, writeThunkQueueRef.current);
422 });
423 }
424 }), []);
425 return createElement(ActionsContext.Provider, {
426 value: actions
427 }, createElement(StateContext.Provider, {
428 value: state
429 }, children));
430};
431
432let keyCount = 0; // global key count for all atoms
433
434function atom(read, write) {
435 const config = {
436 key: ++keyCount
437 };
438
439 if (typeof read === 'function') {
440 config.read = read;
441 } else {
442 config.init = read;
443
444 config.read = get => get(config);
445
446 config.write = (get, set, update) => {
447 set(config, typeof update === 'function' ? update(get(config)) : update);
448 };
449 }
450
451 if (write) {
452 config.write = write;
453 }
454
455 return config;
456}
457
458const isWritable = atom => !!atom.write;
459
460function useAtom(atom) {
461 const actions = useContext(ActionsContext);
462 const pendingListRef = useRef([]);
463 const value = useContextSelector(StateContext, useCallback(state => {
464 let atomState = state.get(atom);
465
466 if (!atomState) {
467 const [initialAtomState, pendingPartialState] = actions.read(state, atom);
468 atomState = initialAtomState;
469
470 if (!atomState.error && !atomState.promise && pendingPartialState.size) {
471 pendingListRef.current.unshift({
472 v: initialAtomState.value,
473 p: pendingPartialState
474 });
475 }
476 }
477
478 if (atomState.error) {
479 throw atomState.error;
480 }
481
482 if (atomState.promise) {
483 throw atomState.promise;
484 }
485
486 return atomState.value;
487 }, [atom, actions]));
488 const pendingPartialStateRef = useRef();
489 useIsoLayoutEffect(() => {
490 const pendingList = pendingListRef.current;
491 const found = pendingList.find(({
492 v
493 }) => v === value);
494
495 if (found) {
496 pendingPartialStateRef.current = found.p;
497 }
498
499 pendingList.splice(0, pendingList.length);
500 });
501 useIsoLayoutEffect(() => {
502 const id = Symbol();
503 actions.add(id, atom, pendingPartialStateRef.current);
504 return () => {
505 actions.del(id);
506 };
507 }, [actions, atom]);
508 const setAtom = useCallback(update => {
509 if (isWritable(atom)) {
510 actions.write(atom, update);
511 } else {
512 throw new Error('not writable atom');
513 }
514 }, [atom, actions]);
515 useDebugValue(value);
516 return [value, setAtom];
517}
518
519export { Provider, atom, useAtom };