UNPKG

17 kBJavaScriptView Raw
1import { useLayoutEffect, useRef, useState, useEffect, useMemo, createElement, useCallback, useDebugValue } from 'react';
2import { createContext, useContext, useContextSelector, BridgeProvider } 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) => appendMap(new Map(src1), src2); // create new map and delete item
34
35
36const deleteMapItem = (src, key) => {
37 const dst = new Map(src);
38 dst.delete(key);
39 return dst;
40};
41
42const warningObject = new Proxy({}, {
43 get() {
44 throw new Error('Please use <Provider>');
45 },
46
47 apply() {
48 throw new Error('Please use <Provider>');
49 }
50
51});
52
53const addDependent = (dependentsMap, atom, dependent) => {
54 let dependents = dependentsMap.get(atom);
55
56 if (!dependents) {
57 dependents = new Set();
58 dependentsMap.set(atom, dependents);
59 }
60
61 dependents.add(dependent);
62};
63
64const deleteDependent = (dependentsMap, dependent) => {
65 dependentsMap.forEach(dependents => {
66 dependents.delete(dependent);
67 });
68};
69
70const setDependencies = (dependentsMap, atom, dependencies) => {
71 deleteDependent(dependentsMap, atom);
72 dependencies.forEach(dependency => {
73 addDependent(dependentsMap, dependency, atom);
74 });
75};
76
77const listDependents = (dependentsMap, atom, excludeSelf) => {
78 const dependents = new Set(dependentsMap.get(atom));
79
80 if (excludeSelf) {
81 dependents.delete(atom);
82 }
83
84 return dependents;
85};
86
87const initialState = new Map();
88
89const getAtomState = (atom, state, tmpState) => {
90 const atomState = tmpState && tmpState.get(atom) || state.get(atom);
91
92 if (!atomState) {
93 throw new Error('atom state not found. possibly a bug.');
94 }
95
96 return atomState;
97};
98
99const getAtomStateValue = (atom, state, tmpState) => {
100 const atomState = tmpState && tmpState.get(atom) || state.get(atom);
101 return atomState ? atomState.value : atom.init;
102};
103
104const readAtom = (state, readingAtom, setState, dependentsMap, readPendingMap) => {
105 const readAtomValue = (prevState, atom) => {
106 const partialState = new Map();
107 const atomState = prevState.get(atom);
108
109 if (atomState) {
110 return [atomState, partialState];
111 }
112
113 let error = undefined;
114 let promise = undefined;
115 let value = null;
116 let dependencies = new Set();
117 let isSync = true;
118
119 try {
120 const promiseOrValue = atom.read(a => {
121 if (dependencies) {
122 dependencies.add(a);
123 } else {
124 addDependent(dependentsMap, a, atom);
125 }
126
127 if (a !== atom) {
128 const [nextAtomState, nextPartialState] = readAtomValue(concatMap(prevState, partialState), a);
129
130 if (isSync) {
131 appendMap(partialState, nextPartialState);
132 } else {
133 setState(prev => concatMap(prev, nextPartialState));
134 }
135
136 if (nextAtomState.readE) {
137 throw nextAtomState.readE;
138 }
139
140 if (nextAtomState.readP) {
141 throw nextAtomState.readP;
142 }
143
144 return nextAtomState.value;
145 } // a === atom
146
147
148 const aState = partialState.get(a) || prevState.get(a);
149
150 if (aState) {
151 if (aState.readP) {
152 throw aState.readP;
153 }
154
155 return aState.value;
156 }
157
158 return a.init; // this should not be undefined
159 });
160
161 if (promiseOrValue instanceof Promise) {
162 promise = promiseOrValue.then(value => {
163 var _prev$get;
164
165 setDependencies(dependentsMap, atom, dependencies);
166 dependencies = null;
167 const prev = readPendingMap.get(atom);
168
169 if (prev && ((_prev$get = prev.get(atom)) == null ? void 0 : _prev$get.readP) === promise) {
170 readPendingMap.set(atom, deleteMapItem(prev, atom));
171 }
172
173 setState(prev => new Map(prev).set(atom, {
174 value
175 }));
176 }).catch(e => {
177 var _prev$get2;
178
179 const prev = readPendingMap.get(atom);
180
181 if (prev && ((_prev$get2 = prev.get(atom)) == null ? void 0 : _prev$get2.readP) === promise) {
182 readPendingMap.set(atom, deleteMapItem(prev, atom));
183 }
184
185 setState(prev => new Map(prev).set(atom, {
186 value: getAtomStateValue(atom, prev),
187 readE: e instanceof Error ? e : new Error(e)
188 }));
189 });
190 } else {
191 setDependencies(dependentsMap, atom, dependencies);
192 dependencies = null;
193 value = promiseOrValue;
194 }
195 } catch (errorOrPromise) {
196 if (errorOrPromise instanceof Promise) {
197 promise = errorOrPromise;
198 } else if (errorOrPromise instanceof Error) {
199 error = errorOrPromise;
200 } else {
201 error = new Error(errorOrPromise);
202 }
203 }
204
205 const nextAtomState = {
206 readE: error,
207 readP: promise,
208 value: promise ? atom.init : value
209 };
210 partialState.set(atom, nextAtomState);
211 isSync = false;
212 return [nextAtomState, partialState];
213 };
214
215 const prevPartialState = readPendingMap.get(readingAtom);
216 const [atomState, partialState] = readAtomValue(prevPartialState ? concatMap(state, prevPartialState) : state, readingAtom);
217 readPendingMap.set(readingAtom, prevPartialState ? concatMap(prevPartialState, partialState) : partialState);
218 return atomState;
219};
220
221const addAtom = (id, atom, setState, dependentsMap, readPendingMap) => {
222 addDependent(dependentsMap, atom, id);
223 const partialState = readPendingMap.get(atom);
224
225 if (partialState) {
226 readPendingMap.delete(atom);
227 setState(prev => concatMap(prev, partialState));
228 }
229};
230
231const delAtom = (id, setGcCount, dependentsMap) => {
232 deleteDependent(dependentsMap, id);
233 setGcCount(c => c + 1); // trigger re-render for gc
234};
235
236const gcAtom = (state, setState, dependentsMap) => {
237 const nextState = new Map(state);
238 let deleted;
239
240 do {
241 deleted = false;
242 nextState.forEach((_atomState, atom) => {
243 var _dependentsMap$get;
244
245 const isEmpty = ((_dependentsMap$get = dependentsMap.get(atom)) == null ? void 0 : _dependentsMap$get.size) === 0;
246
247 if (isEmpty) {
248 nextState.delete(atom);
249 dependentsMap.delete(atom);
250 deleted = true;
251 }
252 });
253 } while (deleted);
254
255 if (nextState.size !== state.size) {
256 setState(nextState);
257 }
258};
259
260const writeAtom = (updatingAtom, update, setState, dependentsMap, addWriteThunk) => {
261 const pendingPromises = [];
262
263 const updateDependentsState = (prevState, atom) => {
264 const partialState = new Map();
265 listDependents(dependentsMap, atom, true).forEach(dependent => {
266 if (typeof dependent === 'symbol') return;
267 let dependencies = new Set();
268
269 try {
270 const promiseOrValue = dependent.read(a => {
271 if (dependencies) {
272 dependencies.add(a);
273 } else {
274 addDependent(dependentsMap, a, dependent);
275 }
276
277 const s = getAtomState(a, prevState);
278
279 if (s.readE) {
280 throw s.readE;
281 }
282
283 return s.value;
284 });
285
286 if (promiseOrValue instanceof Promise) {
287 const promise = promiseOrValue.then(value => {
288 setDependencies(dependentsMap, dependent, dependencies);
289 dependencies = null;
290 const nextAtomState = {
291 value
292 };
293 setState(prev => {
294 var _prev$get3;
295
296 const prevPromise = (_prev$get3 = prev.get(dependent)) == null ? void 0 : _prev$get3.readP;
297
298 if (prevPromise && prevPromise !== promise) {
299 // There is a new promise, so we skip updating this one.
300 return prev;
301 }
302
303 const nextState = new Map(prev).set(dependent, nextAtomState);
304 const nextPartialState = updateDependentsState(nextState, dependent);
305 return appendMap(nextState, nextPartialState);
306 });
307 }).catch(e => {
308 setState(prev => new Map(prev).set(dependent, {
309 value: getAtomStateValue(dependent, prev),
310 readE: e instanceof Error ? e : new Error(e)
311 }));
312 });
313 partialState.set(dependent, {
314 value: getAtomStateValue(dependent, prevState),
315 readP: promise
316 });
317 } else {
318 setDependencies(dependentsMap, dependent, dependencies);
319 dependencies = null;
320 partialState.set(dependent, {
321 value: promiseOrValue
322 });
323 appendMap(partialState, updateDependentsState(concatMap(prevState, partialState), dependent));
324 }
325 } catch (e) {
326 partialState.set(dependent, {
327 value: getAtomStateValue(dependent, prevState),
328 readE: e instanceof Error ? e : new Error(e)
329 });
330 appendMap(partialState, updateDependentsState(concatMap(prevState, partialState), dependent));
331 }
332 });
333 return partialState;
334 };
335
336 const updateAtomState = (prevState, atom, update) => {
337 const prevAtomState = prevState.get(atom);
338
339 if (prevAtomState && prevAtomState.writeP) {
340 const promise = prevAtomState.writeP.then(() => {
341 addWriteThunk(prev => {
342 const nextPartialState = updateAtomState(prev, atom, update);
343
344 if (nextPartialState) {
345 return concatMap(prevState, nextPartialState);
346 }
347
348 return prev;
349 });
350 });
351 pendingPromises.push(promise);
352 return null;
353 }
354
355 const partialState = new Map();
356 let isSync = true;
357
358 try {
359 const promiseOrVoid = atom.write(a => {
360 if (process.env.NODE_ENV !== 'production') {
361 const s = prevState.get(a);
362
363 if (s && s.readP) {
364 console.log('Reading pending atom state in write operation. Not sure how to deal with it. Returning stale vaule for', a);
365 }
366 }
367
368 return getAtomStateValue(a, prevState, partialState);
369 }, (a, v) => {
370 if (a === atom) {
371 const nextAtomState = {
372 value: v
373 };
374
375 if (isSync) {
376 partialState.set(a, nextAtomState);
377 appendMap(partialState, updateDependentsState(concatMap(prevState, partialState), a));
378 } else {
379 setState(prev => {
380 const nextState = new Map(prev).set(a, nextAtomState);
381 const nextPartialState = updateDependentsState(nextState, a);
382 return appendMap(nextState, nextPartialState);
383 });
384 }
385 } else {
386 if (isSync) {
387 const nextPartialState = updateAtomState(concatMap(prevState, partialState), a, v);
388
389 if (nextPartialState) {
390 appendMap(partialState, nextPartialState);
391 }
392 } else {
393 addWriteThunk(prev => {
394 const nextPartialState = updateAtomState(prev, a, v);
395
396 if (nextPartialState) {
397 return concatMap(prev, nextPartialState);
398 }
399
400 return prev;
401 });
402 }
403 }
404 }, update);
405
406 if (promiseOrVoid instanceof Promise) {
407 pendingPromises.push(promiseOrVoid);
408
409 const nextAtomState = _extends({}, getAtomState(atom, prevState, partialState), {
410 writeP: promiseOrVoid.then(() => {
411 addWriteThunk(prev => new Map(prev).set(atom, _extends({}, getAtomState(atom, prev), {
412 writeP: undefined
413 })));
414 })
415 });
416
417 partialState.set(atom, nextAtomState);
418 }
419 } catch (e) {
420 if (pendingPromises.length) {
421 pendingPromises.push(new Promise((_resolve, reject) => {
422 reject(e);
423 }));
424 } else {
425 throw e;
426 }
427 }
428
429 isSync = false;
430 return partialState;
431 };
432
433 addWriteThunk(prevState => {
434 const nextPartialState = updateAtomState(prevState, updatingAtom, update);
435
436 if (nextPartialState) {
437 return concatMap(prevState, nextPartialState);
438 }
439
440 return prevState;
441 });
442
443 if (pendingPromises.length) {
444 return new Promise((resolve, reject) => {
445 const loop = () => {
446 const len = pendingPromises.length;
447
448 if (len === 0) {
449 resolve();
450 } else {
451 Promise.all(pendingPromises).then(() => {
452 pendingPromises.splice(0, len);
453 loop();
454 }).catch(reject);
455 }
456 };
457
458 loop();
459 });
460 }
461};
462
463const runWriteThunk = (lastStateRef, setState, writeThunkQueue) => {
464 while (true) {
465 if (!lastStateRef.current) {
466 return;
467 }
468
469 if (writeThunkQueue.length === 0) {
470 return;
471 }
472
473 const thunk = writeThunkQueue.shift();
474 const lastState = lastStateRef.current;
475 const nextState = thunk(lastState);
476
477 if (nextState !== lastState) {
478 setState(nextState);
479 return;
480 }
481 }
482};
483
484const ActionsContext = createContext(warningObject);
485const StateContext = createContext(warningObject);
486const Provider = ({
487 children
488}) => {
489 const dependentsMapRef = useRef();
490
491 if (!dependentsMapRef.current) {
492 dependentsMapRef.current = new Map();
493 }
494
495 const readPendingMapRef = useRef();
496
497 if (!readPendingMapRef.current) {
498 readPendingMapRef.current = new WeakMap();
499 }
500
501 const [state, setStateOrig] = useState(initialState);
502 const lastStateRef = useRef(null);
503
504 const setState = setStateAction => {
505 lastStateRef.current = null;
506 setStateOrig(setStateAction);
507 };
508
509 useIsoLayoutEffect(() => {
510 if (state !== initialState) {
511 lastStateRef.current = state;
512 }
513 });
514 const [gcCount, setGcCount] = useState(0); // to trigger gc
515
516 useEffect(() => {
517 gcAtom(state, setState, dependentsMapRef.current);
518 }, [state, gcCount]);
519 const writeThunkQueueRef = useRef([]);
520 useEffect(() => {
521 runWriteThunk(lastStateRef, setState, writeThunkQueueRef.current);
522 }, [state]);
523 const actions = useMemo(() => ({
524 add: (id, atom) => addAtom(id, atom, setState, dependentsMapRef.current, readPendingMapRef.current),
525 del: id => delAtom(id, setGcCount, dependentsMapRef.current),
526 read: (state, atom) => readAtom(state, atom, setState, dependentsMapRef.current, readPendingMapRef.current),
527 write: (atom, update) => writeAtom(atom, update, setState, dependentsMapRef.current, thunk => {
528 writeThunkQueueRef.current.push(thunk);
529 runWriteThunk(lastStateRef, setState, writeThunkQueueRef.current);
530 })
531 }), []);
532 return createElement(ActionsContext.Provider, {
533 value: actions
534 }, createElement(StateContext.Provider, {
535 value: state
536 }, children));
537};
538
539let keyCount = 0; // global key count for all atoms
540
541function atom(read, write) {
542 const config = {
543 key: ++keyCount
544 };
545
546 if (typeof read === 'function') {
547 config.read = read;
548 } else {
549 config.init = read;
550
551 config.read = get => get(config);
552
553 config.write = (get, set, update) => {
554 set(config, typeof update === 'function' ? update(get(config)) : update);
555 };
556 }
557
558 if (write) {
559 config.write = write;
560 }
561
562 return config;
563}
564
565const isWritable = atom => !!atom.write;
566
567function useAtom(atom) {
568 const actions = useContext(ActionsContext);
569 const value = useContextSelector(StateContext, useCallback(state => {
570 const atomState = actions.read(state, atom);
571
572 if (atomState.readE) {
573 throw atomState.readE; // read error
574 }
575
576 if (atomState.readP) {
577 throw atomState.readP; // read promise
578 }
579
580 if (atomState.writeP) {
581 throw atomState.writeP; // write promise
582 }
583
584 return atomState.value;
585 }, [atom, actions]));
586 useIsoLayoutEffect(() => {
587 const id = Symbol();
588 actions.add(id, atom);
589 return () => {
590 actions.del(id);
591 };
592 }, [actions, atom]);
593 const setAtom = useCallback(update => {
594 if (isWritable(atom)) {
595 return actions.write(atom, update);
596 } else {
597 throw new Error('not writable atom');
598 }
599 }, [atom, actions]);
600 useDebugValue(value);
601 return [value, setAtom];
602}
603
604const useBridge = () => {
605 const actions = useContext(ActionsContext);
606 const state = useContext(StateContext);
607 return useMemo(() => [actions, state], [actions, state]);
608};
609const Bridge = ({
610 value,
611 children
612}) => {
613 const [actions, state] = value;
614 return createElement(BridgeProvider, {
615 context: ActionsContext,
616 value: actions
617 }, createElement(BridgeProvider, {
618 context: StateContext,
619 value: state
620 }, children));
621};
622
623export { Bridge, Provider, atom, useAtom, useBridge };