bansa 0.0.1

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/src/index.ts ADDED
@@ -0,0 +1,604 @@
1
+ export type Atom<Value> = PrimitiveAtom<Value> | DerivedAtom<Value>;
2
+ export type CommonAtom<Value> = {
3
+ readonly get: () => Value;
4
+ readonly watch: (watcher: AtomWatcher) => () => void;
5
+ readonly subscribe: (subscriber: AtomSubscribe<Value>) => () => void;
6
+ readonly state: AtomState<Value>;
7
+ };
8
+ export type PrimitiveAtom<Value> = CommonAtom<Value> & {
9
+ readonly set: (value: AtomUpdater<Value>) => void;
10
+ readonly state: AtomSuccessState<Value>;
11
+ };
12
+ export type DerivedAtom<Value> = CommonAtom<Value>;
13
+
14
+ export type AtomWatcher = () => void;
15
+ export type AtomSubscribe<Value> = (
16
+ value: Value,
17
+ options: AtomSubscriberOptions,
18
+ ) => void;
19
+ export type AtomInit<Value> = Value | AtomGetter<Value>;
20
+ export type AtomUpdater<Value> = Value | AtomReducer<Value>;
21
+ // TODO: readonly
22
+ export type AtomInactiveState<Value> = {
23
+ promise: typeof inactive;
24
+ error: any;
25
+ value?: Value;
26
+ };
27
+ export type AtomPromiseState<Value> = {
28
+ promise: PromiseLike<Value>;
29
+ error: any;
30
+ value?: Value;
31
+ };
32
+ export type AtomSuccessState<Value> = {
33
+ promise: undefined;
34
+ error: undefined;
35
+ value: Value;
36
+ };
37
+ export type AtomErrorState<Value> = {
38
+ promise: undefined;
39
+ error: any;
40
+ value?: Value;
41
+ };
42
+ export type AtomState<Value> =
43
+ | AtomInactiveState<Value>
44
+ | AtomPromiseState<Value>
45
+ | AtomSuccessState<Value>
46
+ | AtomErrorState<Value>;
47
+
48
+ export type AtomSubscriberOptions = { readonly signal: ThenableSignal };
49
+ export type AtomGetter<Value> = (
50
+ get: GetAtom,
51
+ options: AtomGetOptions,
52
+ ) => Value | PromiseLike<Value>;
53
+ export type AtomReducer<Value> = (value: Value) => Value;
54
+
55
+ export type AtomGetOptions = { readonly signal: ThenableSignal };
56
+ export type ThenableSignal = AbortSignal & { then: (f: () => void) => void };
57
+ type ThenableSignalController = {
58
+ abort: () => void;
59
+ signal: ThenableSignal;
60
+ };
61
+
62
+ export type GetAtom = {
63
+ <Value>(anotherAtom: Atom<Value>, unwrap?: true): Value;
64
+ <Value>(anotherAtom: Atom<Value>, unwrap: false): AtomState<Value>;
65
+ };
66
+
67
+ type CreateAtom = {
68
+ <Value>(
69
+ init: AtomGetter<Value>,
70
+ options?: AtomOptions<Value>,
71
+ ): DerivedAtom<Value>;
72
+ <Value>(init: Value, options?: AtomOptions<Value>): PrimitiveAtom<Value>;
73
+ <Value>(
74
+ init: Value | AtomGetter<Value>,
75
+ options?: AtomOptions<Value>,
76
+ ): Atom<Value>;
77
+ };
78
+ export type AtomOptions<Value> = {
79
+ equals?: AtomEquals<Value>;
80
+ persist?: boolean;
81
+ eager?: boolean;
82
+ };
83
+
84
+ export type AtomEquals<Value> = (value: Value, prevValue: Value) => boolean;
85
+ export type AtomScope = {
86
+ <Value>(baseAtom: PrimitiveAtom<Value>): PrimitiveAtom<Value>;
87
+ <Value>(baseAtom: DerivedAtom<Value>): DerivedAtom<Value>;
88
+ <Value>(baseAtom: Atom<Value>): Atom<Value>;
89
+ } & {
90
+ get: {
91
+ <Value>(baseAtom: PrimitiveAtom<Value>): PrimitiveAtom<Value>;
92
+ <Value>(baseAtom: DerivedAtom<Value>): DerivedAtom<Value>;
93
+ <Value>(baseAtom: Atom<Value>): Atom<Value>;
94
+ };
95
+ };
96
+
97
+ export type SetLike<Key> =
98
+ | Key[]
99
+ | Set<Key>
100
+ | (Key extends object ? WeakSet<Key> : never);
101
+ export type MapLike<Key, Value> =
102
+ | Map<Key, Value>
103
+ | (Key extends object ? WeakMap<Key, Value> : never)
104
+ | (Key extends string | number | symbol ? Record<Key, Value> : never);
105
+
106
+ type AtomInternal<Value> =
107
+ | PrimitiveAtomInternal<Value>
108
+ | DerivedAtomInternal<Value>;
109
+ type CommonAtomInternal<Value> = {
110
+ _equals?: AtomEquals<Value>;
111
+
112
+ _marked: boolean;
113
+ _nextError?: any;
114
+ _nextValue?: Value;
115
+ _children?: Set<DerivedAtomInternal<any>>;
116
+ _watchers?: Set<AtomWatcher>;
117
+ _subscribers?: Set<AtomSubscribeInternal<Value>>;
118
+ };
119
+ type PrimitiveAtomInternal<Value> = CommonAtomInternal<Value> &
120
+ PrimitiveAtom<Value> & {
121
+ readonly _source: true;
122
+ readonly _active: true;
123
+ _needPropagate: boolean;
124
+
125
+ // _init: Value;
126
+ };
127
+ type DerivedAtomInternal<Value> = CommonAtomInternal<Value> &
128
+ DerivedAtom<Value> & {
129
+ readonly _source: false;
130
+ readonly _persist: boolean;
131
+ _active: boolean;
132
+ _needExecute: boolean;
133
+ _needPropagate: boolean;
134
+
135
+ _init: AtomGetterInternal<Value>;
136
+ _options: AtomGetOptions;
137
+ _counter: number;
138
+ _ctrl?: ThenableSignalController;
139
+ _dependencies?: Set<AtomInternal<any>>;
140
+ };
141
+
142
+ type GetAtomInternal = {
143
+ <Value>(anotherAtom: AtomInternal<Value>, unwrap?: true): Value;
144
+ <Value>(anotherAtom: AtomInternal<Value>, unwrap: false): AtomState<Value>;
145
+ };
146
+ type AtomGetterInternal<Value> = (
147
+ get: GetAtomInternal,
148
+ options: AtomGetOptions,
149
+ ) => Value | PromiseLike<Value>;
150
+ type AtomSubscribeInternal<Value> = {
151
+ _subscriber: AtomSubscribe<Value>;
152
+ _options: AtomSubscriberOptions;
153
+ _ctrl?: ThenableSignalController;
154
+ };
155
+
156
+ // JS에서 자료형을 만드는 방법은 여러 가지가 있다:
157
+ // 클로저를 활용해서 익명 함수로 메서드가 구현된 객체 반환하기
158
+ // -> 제일 쉽고 직관적이지만 공통 메서드/멤버 변수가 매번 새로 선언되므로 시간/메모리 측면에서 비효율적이다.
159
+ // 공통 메서드/멤버 변수만 별도의 객체로 추출한 뒤 Object.create로 프로토타입 설정하기
160
+ // -> Object.create가 new보다 2~3배 느린 걸로 보인다. 프로토타입 직접 건드리는 거라 그런가...
161
+ // class 쓰기
162
+ // -> 공통 멤버 변수를 프로토타입에 박을 방법이 없다.
163
+ // function 쓰기
164
+ // -> 그나마 최선의 해결책.
165
+
166
+ const AtomPrototype = function <Value>(
167
+ this: AtomInternal<Value>,
168
+ ) {} as unknown as { new <_Value>(): AtomInternal<_Value> };
169
+ AtomPrototype.prototype.get = function <Value>(this: AtomInternal<Value>) {
170
+ if (!this._active) {
171
+ execute(this);
172
+ disableAtom(this);
173
+ }
174
+ if (this.state.promise) throw this.state.promise;
175
+ if (this.state.error) throw this.state.error;
176
+ return this.state.value!;
177
+ };
178
+ AtomPrototype.prototype.watch = function <Value>(
179
+ this: AtomInternal<Value>,
180
+ watcher: AtomWatcher,
181
+ ) {
182
+ if (!this._active) {
183
+ requestActivate(this);
184
+ }
185
+ (this._watchers ??= new Set()).add(watcher);
186
+ return () => {
187
+ this._watchers!.delete(watcher);
188
+ if (!this._watchers!.size) {
189
+ disableAtom(this);
190
+ }
191
+ };
192
+ };
193
+ AtomPrototype.prototype.subscribe = function <Value>(
194
+ this: AtomInternal<Value>,
195
+ subscriber: AtomSubscribe<unknown>,
196
+ ) {
197
+ const atomSubscriber: AtomSubscribeInternal<unknown> = {
198
+ _subscriber: subscriber,
199
+ _options: {
200
+ get signal() {
201
+ return (atomSubscriber._ctrl ??= createThenableSignal()).signal;
202
+ },
203
+ },
204
+ };
205
+ if (!this._active) {
206
+ requestActivate(this);
207
+ } else if (!this.state.promise && !this.state.error) {
208
+ try {
209
+ subscriber(this.state.value!, atomSubscriber._options);
210
+ } catch (e) {
211
+ console.error(e);
212
+ }
213
+ }
214
+ (this._subscribers ??= new Set()).add(atomSubscriber);
215
+ return () => {
216
+ this._subscribers!.delete(atomSubscriber);
217
+ if (atomSubscriber._ctrl) {
218
+ atomSubscriber._ctrl.abort();
219
+ atomSubscriber._ctrl = undefined;
220
+ }
221
+ if (!this._subscribers!.size) {
222
+ disableAtom(this);
223
+ }
224
+ };
225
+ };
226
+ AtomPrototype.prototype[Symbol.toPrimitive] = function <Value>(
227
+ this: AtomInternal<Value>,
228
+ ) {
229
+ return this.state.value;
230
+ };
231
+
232
+ const PrimitiveAtomPrototype = function <Value>(
233
+ this: PrimitiveAtomInternal<Value>,
234
+ init: Value,
235
+ options?: AtomOptions<Value>,
236
+ ) {
237
+ // this._init = init;
238
+ this._equals = options?.equals;
239
+
240
+ (this as any).state = {
241
+ promise: undefined,
242
+ error: undefined,
243
+ value: init,
244
+ };
245
+ this.state.value = this._nextValue = init;
246
+ } as unknown as {
247
+ new <_Value>(
248
+ init: _Value,
249
+ options?: AtomOptions<_Value>,
250
+ ): PrimitiveAtomInternal<_Value>;
251
+ };
252
+
253
+ PrimitiveAtomPrototype.prototype.set = function <Value>(
254
+ this: PrimitiveAtomInternal<Value>,
255
+ value: AtomUpdater<unknown>,
256
+ ) {
257
+ const nextValue =
258
+ value instanceof Function ? value(this._nextValue!) : value;
259
+ if (!equals(nextValue, this.state.value, this._equals)) {
260
+ this._nextValue = nextValue;
261
+ requestPropagate(this);
262
+ }
263
+ };
264
+ PrimitiveAtomPrototype.prototype._source = true;
265
+ PrimitiveAtomPrototype.prototype._active = true;
266
+ PrimitiveAtomPrototype.prototype._needPropagate = false;
267
+ Object.setPrototypeOf(
268
+ PrimitiveAtomPrototype.prototype,
269
+ AtomPrototype.prototype,
270
+ );
271
+
272
+ const DerivedAtomPrototype = function <Value>(
273
+ this: DerivedAtomInternal<Value>,
274
+ init: AtomGetter<Value>,
275
+ options?: AtomOptions<Value>,
276
+ ) {
277
+ this._init = init as AtomGetterInternal<Value>;
278
+ this._equals = options?.equals;
279
+ (this as any)._persist = options?.persist;
280
+ (this as any).state = {
281
+ promise: inactive,
282
+ error: undefined,
283
+ value: undefined,
284
+ };
285
+
286
+ const self = this;
287
+ this._options = {
288
+ get signal() {
289
+ return (self._ctrl ??= createThenableSignal()).signal;
290
+ },
291
+ };
292
+ } as unknown as {
293
+ new <Value>(
294
+ init: AtomGetter<Value>,
295
+ options?: AtomOptions<Value>,
296
+ ): DerivedAtomInternal<Value>;
297
+ };
298
+
299
+ DerivedAtomPrototype.prototype._source = false;
300
+ DerivedAtomPrototype.prototype._active = false;
301
+ DerivedAtomPrototype.prototype._needPropagate = false;
302
+ DerivedAtomPrototype.prototype._counter = 0;
303
+ Object.setPrototypeOf(DerivedAtomPrototype.prototype, AtomPrototype.prototype);
304
+
305
+ const ouroboros: any = () => ouroboros;
306
+ const toUndefined = () => undefined;
307
+ Object.setPrototypeOf(
308
+ ouroboros,
309
+ new Proxy(ouroboros, {
310
+ get: (_, k) => (k === Symbol.toPrimitive ? toUndefined : ouroboros),
311
+ }),
312
+ );
313
+
314
+ export const inactive = Promise.resolve();
315
+ export const $: CreateAtom = <Value>(
316
+ init: Value | AtomGetter<Value>,
317
+ options?: AtomOptions<Value>,
318
+ ) => {
319
+ if (init instanceof Function)
320
+ return new DerivedAtomPrototype(init, options);
321
+ return new PrimitiveAtomPrototype(init, options) as any;
322
+ };
323
+ export const $$ = <Value>(init: AtomGetter<Value>) =>
324
+ $((get, options) => {
325
+ let promises: PromiseLike<Value>[] | undefined;
326
+ let error: unknown;
327
+ const result = init((atom) => {
328
+ try {
329
+ return get(atom);
330
+ } catch (e) {
331
+ if (!e) {
332
+ throw e;
333
+ }
334
+ if (isPromiseLike(e)) {
335
+ (promises ??= []).push(e as PromiseLike<Value>);
336
+ } else {
337
+ error = e;
338
+ }
339
+ }
340
+ return ouroboros;
341
+ }, options);
342
+ if (error) throw error;
343
+ if (promises) throw Promise.all(promises);
344
+ return result;
345
+ });
346
+
347
+ let pendingUpdateAtoms = false;
348
+ let stack: AtomInternal<any>[] = [];
349
+ const requestActivate = <Value>(atom: DerivedAtomInternal<Value>) => {
350
+ if (!atom._needExecute) {
351
+ atom._needExecute = true;
352
+ requestPropagate(atom);
353
+ }
354
+ };
355
+ const requestPropagate = <Value>(atom: AtomInternal<Value>) => {
356
+ if (!atom._needPropagate) {
357
+ atom._needPropagate = true;
358
+ stack.push(atom);
359
+ if (!pendingUpdateAtoms) {
360
+ pendingUpdateAtoms = true;
361
+ queueMicrotask(updateAtoms);
362
+ }
363
+ }
364
+ };
365
+ const updateAtoms = () => {
366
+ pendingUpdateAtoms = false;
367
+ {
368
+ const updatedAtoms = stack;
369
+ stack = [];
370
+ for (const atom of updatedAtoms) {
371
+ atom.state.promise = undefined;
372
+ atom.state.error = atom._nextError;
373
+ atom.state.value = atom._nextValue;
374
+ mark(atom);
375
+ }
376
+ }
377
+ const markedAtoms = stack as DerivedAtomInternal<any>[];
378
+ stack = [];
379
+ for (let i = markedAtoms.length; i--; ) {
380
+ const atom = markedAtoms[i];
381
+ atom._marked = false;
382
+ if (atom._needExecute) {
383
+ atom._needPropagate = true;
384
+ execute(atom);
385
+ }
386
+ if (atom._needPropagate) {
387
+ propagate(atom);
388
+ }
389
+ }
390
+ };
391
+ const propagate = <Value>(atom: AtomInternal<Value>) => {
392
+ atom._needPropagate = false;
393
+ if (atom._children) {
394
+ for (const child of atom._children) {
395
+ child._needExecute = true;
396
+ }
397
+ }
398
+ if (atom._watchers) {
399
+ for (const watcher of atom._watchers) {
400
+ watcher();
401
+ }
402
+ }
403
+ if (atom._subscribers && !atom.state.promise && !atom.state.error) {
404
+ for (const subscriber of atom._subscribers) {
405
+ if (subscriber._ctrl) {
406
+ subscriber._ctrl.abort();
407
+ subscriber._ctrl = undefined;
408
+ }
409
+ try {
410
+ subscriber._subscriber(atom.state.value!, subscriber._options);
411
+ } catch (e) {
412
+ console.error(e);
413
+ }
414
+ }
415
+ }
416
+ };
417
+ const mark = (atom: AtomInternal<any>) => {
418
+ if (!atom._marked) {
419
+ atom._marked = true;
420
+ if (atom._children) {
421
+ for (const child of atom._children) {
422
+ mark(child);
423
+ }
424
+ }
425
+ stack.push(atom);
426
+ }
427
+ };
428
+
429
+ class Wrapped {
430
+ e: unknown;
431
+ constructor(e: unknown) {
432
+ this.e = e;
433
+ }
434
+ }
435
+ const execute = <Value>(atom: DerivedAtomInternal<Value>) => {
436
+ const counter = ++atom._counter;
437
+ atom._active = true;
438
+ atom._needExecute = false;
439
+ atom.state.promise = undefined;
440
+
441
+ if (atom._ctrl) {
442
+ atom._ctrl.abort();
443
+ atom._ctrl = undefined;
444
+ }
445
+
446
+ // TODO: nextDependencies
447
+ const oldDependencies = atom._dependencies;
448
+ if (oldDependencies) {
449
+ atom._dependencies = new Set();
450
+ }
451
+ try {
452
+ const value = atom._init(
453
+ <V>(anotherAtom: AtomInternal<V>, unwrap = true) => {
454
+ if (counter !== atom._counter) throw undefined;
455
+ if ((atom as unknown) !== anotherAtom) {
456
+ if (!anotherAtom._active) {
457
+ execute(anotherAtom);
458
+ if (anotherAtom._needPropagate) {
459
+ anotherAtom._needPropagate = false;
460
+ propagate(anotherAtom);
461
+ }
462
+ }
463
+ oldDependencies?.delete(anotherAtom);
464
+ (atom._dependencies ??= new Set()).add(anotherAtom);
465
+ (anotherAtom._children ??= new Set()).add(atom);
466
+ }
467
+ if (!unwrap) return anotherAtom.state;
468
+ if (anotherAtom.state.promise)
469
+ throw new Wrapped(anotherAtom.state.promise);
470
+ if (anotherAtom.state.error)
471
+ throw new Wrapped(anotherAtom.state.error);
472
+ return anotherAtom.state.value as V;
473
+ },
474
+ atom._options,
475
+ );
476
+
477
+ if (isPromiseLike(value)) {
478
+ atom.state.promise = value;
479
+ value.then(
480
+ (value) => {
481
+ if (counter === atom._counter) {
482
+ if (equals(value, atom.state.value, atom._equals)) {
483
+ atom.state.promise = undefined;
484
+ // watchers 재실행 해야 할까?
485
+ } else {
486
+ atom._nextValue = value;
487
+ atom._nextError = undefined;
488
+ requestPropagate(atom);
489
+ }
490
+ }
491
+ },
492
+ (error) => {
493
+ if (counter === atom._counter) {
494
+ if (error instanceof Promise) {
495
+ atom.state.promise = undefined;
496
+ } else {
497
+ atom._nextError = error;
498
+ requestPropagate(atom);
499
+ }
500
+ }
501
+ },
502
+ );
503
+ } else {
504
+ ++atom._counter;
505
+ atom.state.error = undefined;
506
+ if (equals(value, atom.state.value, atom._equals)) {
507
+ atom._needPropagate = false;
508
+ } else {
509
+ atom.state.value = atom._nextValue = value;
510
+ }
511
+ }
512
+ } catch (e) {
513
+ ++atom._counter;
514
+ if (!e) {
515
+ atom._needPropagate = false;
516
+ } else {
517
+ if (e instanceof Wrapped) {
518
+ e = e.e;
519
+ } else {
520
+ console.error(e);
521
+ }
522
+ if (isPromiseLike(e)) {
523
+ atom.state.promise = e as PromiseLike<Value>;
524
+ } else {
525
+ atom.state.error = e;
526
+ }
527
+ }
528
+ }
529
+
530
+ if (oldDependencies) {
531
+ for (const dep of oldDependencies) {
532
+ dep._children!.delete(atom);
533
+ disableAtom(dep);
534
+ }
535
+ }
536
+ };
537
+
538
+ // TODO: 좀 대충 짜놨는데 개선할 수 있을지 고민해봐야
539
+ let disabling = false;
540
+ const disableAtom = <Value>(atom: AtomInternal<Value>) => {
541
+ if (
542
+ !atom._source &&
543
+ !atom._persist &&
544
+ !atom._children?.size &&
545
+ !atom._watchers?.size &&
546
+ !atom._subscribers?.size
547
+ ) {
548
+ if (!disabling) {
549
+ setTimeout(() => {
550
+ disabling = true;
551
+ disableAtom(atom);
552
+ disabling = false;
553
+ }, 0);
554
+ return;
555
+ }
556
+ atom.state.promise = inactive;
557
+ atom._nextValue =
558
+ atom._nextError =
559
+ atom.state.error =
560
+ atom.state.value =
561
+ undefined;
562
+ atom._needPropagate = atom._needExecute = atom._active = false;
563
+ if (atom._ctrl) {
564
+ atom._ctrl.abort();
565
+ atom._ctrl = undefined;
566
+ }
567
+ if (atom._dependencies) {
568
+ for (const dep of atom._dependencies) {
569
+ dep._children!.delete(atom);
570
+ disableAtom(dep);
571
+ }
572
+ atom._dependencies.clear();
573
+ }
574
+ }
575
+ };
576
+
577
+ const equals = <Value>(
578
+ value: Value,
579
+ prevValue?: Value,
580
+ equalsFn?: (value: Value, prevValue: Value) => boolean,
581
+ ) =>
582
+ Object.is(value, prevValue) ||
583
+ (equalsFn !== undefined &&
584
+ prevValue !== undefined &&
585
+ equalsFn(value, prevValue));
586
+
587
+ const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
588
+ typeof (x as PromiseLike<unknown>)?.then === 'function';
589
+
590
+ const createThenableSignal = () => {
591
+ const ctrl = new AbortController();
592
+ const signal = ctrl.signal as ThenableSignal;
593
+ const promise = new Promise((resolve) => {
594
+ signal.then = (f: () => void) => promise.then(f);
595
+ signal.addEventListener('abort', resolve, {
596
+ once: true,
597
+ passive: true,
598
+ });
599
+ });
600
+ return {
601
+ abort: () => ctrl.abort(),
602
+ signal,
603
+ };
604
+ };
package/src/react.tsx ADDED
@@ -0,0 +1,26 @@
1
+ import { useState, useSyncExternalStore } from 'react';
2
+ import type { Atom, DerivedAtom, PrimitiveAtom } from '.';
3
+
4
+ export const useAtom = <Value,>(atom: PrimitiveAtom<Value>) =>
5
+ [useSyncExternalStore(atom.watch, atom.get), atom.set] as const;
6
+
7
+ export const useAtomValue = <Value,>(atom: Atom<Value>) =>
8
+ useSyncExternalStore(atom.watch, atom.get);
9
+
10
+ export const useAtomState = <Value,>(atom: DerivedAtom<Value>) =>
11
+ useSyncExternalStore(atom.watch, () => {
12
+ // avoid https://github.com/facebook/react/issues/31730
13
+ try {
14
+ atom.get();
15
+ } catch (_) {}
16
+ return atom.state;
17
+ });
18
+
19
+ export const useStateAtom = <Value,>(atom: PrimitiveAtom<Value>) => {
20
+ const [state, setState] = useState(() => atom.get());
21
+ const setStateWithAtom = (newState: Value) => {
22
+ setState(newState);
23
+ atom.set(newState);
24
+ };
25
+ return [state, setStateWithAtom] as const;
26
+ };