UNPKG

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