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/README.ko.md +459 -0
- package/README.md +459 -0
- package/biome.json +39 -0
- package/package.json +31 -0
- package/src/index.ts +604 -0
- package/src/react.tsx +26 -0
- package/tests/bansa.test.ts +1036 -0
- package/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Bansa
|
|
2
|
+
|
|
3
|
+
English | [한국어](https://github.com/cgiosy/bansa/blob/main/README.ko.md)
|
|
4
|
+
|
|
5
|
+
## Introduction
|
|
6
|
+
|
|
7
|
+
Bansa is a library that makes it easy to manage derived state, asynchronous values, dependencies and subscriptions, lifecycles, and side effects. Similar to [Jotai](https://jotai.org/), it follows a bottom-up approach using atoms.
|
|
8
|
+
|
|
9
|
+
It is a framework-independent library that can be used in a pure JavaScript environment without any other libraries or frameworks, as well as with React, Vue, Svelte, and others.
|
|
10
|
+
|
|
11
|
+
## Concepts
|
|
12
|
+
|
|
13
|
+
### State
|
|
14
|
+
|
|
15
|
+
You can create a state with the `$` function. There are two types of states.
|
|
16
|
+
|
|
17
|
+
#### Primitive State
|
|
18
|
+
|
|
19
|
+
The most basic unit of state. Its value is static and can be updated to any value using the `.set` method.
|
|
20
|
+
|
|
21
|
+
It is created by passing a normal value (number/string/object, etc.) to the `$` function.
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
import { $ } from 'bansa';
|
|
25
|
+
|
|
26
|
+
const $count = $(42);
|
|
27
|
+
|
|
28
|
+
const $user = $({ name: 'John Doe', age: 30 });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### Derived State
|
|
32
|
+
|
|
33
|
+
A state whose value is dynamic and has a lifecycle. It cannot be updated directly; it can only be re-executed when the value of a state it depends on changes. If the state is not active (i.e., there are no subscribers), the function will not run, and it is treated as having no dependencies.
|
|
34
|
+
|
|
35
|
+
It is created by passing a function to `$`. The arguments to this function are a `get` function, which can read the values of other states, and `{ signal }`, which represents the state's lifetime. The `signal` will be discussed in more detail in another section.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
const $countDouble = $((get) => get($count) * 2);
|
|
39
|
+
|
|
40
|
+
const $userMessage = $((get) => {
|
|
41
|
+
if (get($count) < 50) return 'no hello.';
|
|
42
|
+
return `Hello, ${get($user).name}!`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const $signalExample = $((_, { signal }) => {
|
|
46
|
+
signal.then(() => console.log("$signalExample died"));
|
|
47
|
+
return fetch(`/users/${get($count)}`, { signal }).then((res) => res.json());
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Here, `$countDouble` depends on `$count`, so if the value of `$count` changes, the value of `$countDouble` can be automatically recalculated.
|
|
52
|
+
|
|
53
|
+
`$userMessage` depends on `$count`, and if the value of `$count` is not less than `50`, it also depends on `$user`. This means if `$count` is less than `50`, `$userMessage` will not be recalculated even if the value of `$user` changes.
|
|
54
|
+
|
|
55
|
+
This explanation describes the behavior when the state is active. It will not execute in either case until it is subscribed to using the `.subscribe()` or `.watch()` methods, which will be discussed later.
|
|
56
|
+
|
|
57
|
+
##### Reading `state` (Preventing unwrap)
|
|
58
|
+
|
|
59
|
+
The second parameter of `get` is an optional `unwrap` option. The default value is `true`, so it always returns the unwrapped value. If set to `false`, it returns the `state` of type `AtomState<Value>`.
|
|
60
|
+
|
|
61
|
+
`state` is a read-only object representing the current status of the state. It is useful for handling situations where the value is not ready, such as with asynchronous states or expected errors (e.g., for showing a placeholder). `value` holds the last successfully resolved value. `promise` and `error` hold their respective values if the state is currently loading or has encountered an error. The exact type is as follows:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
type AtomState<Value> =
|
|
65
|
+
| { promise: undefined; error: undefined; value: Value; } // Success
|
|
66
|
+
| { promise: undefined; error: any; value?: Value; } // Error
|
|
67
|
+
| { promise: PromiseLike<Value>; error: any; value?: Value; } // Loading
|
|
68
|
+
| { promise: typeof inactive; error: undefined; value?: Value; } // Inactive
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
##### Keeping a state active
|
|
72
|
+
|
|
73
|
+
You can pass an options object as the second parameter to `$`. If `persist` is set to `true` in the options object, the state will not be deactivated once it becomes active. This can be used instead of adding a meaningless subscription just to keep the state active.
|
|
74
|
+
|
|
75
|
+
This is useful for values that rarely change but need to be ready for use at any time. A typical example is fetching or importing static assets.
|
|
76
|
+
|
|
77
|
+
### Reading State Directly
|
|
78
|
+
|
|
79
|
+
You can read the current value of a state using the `atom.get()` method or `atom.state` property.
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
console.log($count.get()); // 42
|
|
83
|
+
console.log($countDouble.get()); // 84
|
|
84
|
+
|
|
85
|
+
console.log($countDouble.state); // { promise: undefined, error: Symbol(), value: 84 }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For derived states, `.get()` can throw. It throws the `Promise` during asynchronous loading and throws the error when in an error state. This is useful when you want to primarily handle the success case and push all exception handling into a `catch` block or similar.
|
|
89
|
+
|
|
90
|
+
If a state is inactive, the `.get()` method temporarily transitions it to an active state. Naturally, the state and all its dependencies will be re-executed. "Temporarily" means at least until the end of the current microtask. This means that calling `.get()` synchronously multiple times in a row will not cause everything to re-execute each time.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### Updating State
|
|
94
|
+
|
|
95
|
+
You can update the value of a primitive state using the `.set(updater)` method. If `updater` is a normal value, the state is updated to that value. If it's a function, the state is updated with `updater(nextValue)`, where `nextValue` is the state's 'pending value'.
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
console.log($count.get()); // 42
|
|
99
|
+
|
|
100
|
+
$count.set(100);
|
|
101
|
+
console.log($count.get(), $countDouble.get()); // !!! 42 84 !!!
|
|
102
|
+
queueMicrotask(() => console.log($count.get(), $countDouble.get())); // 100 200
|
|
103
|
+
|
|
104
|
+
const increment = (x) => x + 1;
|
|
105
|
+
$count.set(increment);
|
|
106
|
+
console.log($count.get()); // 101
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
All updates are batched per microtask. This means multiple synchronous updates are processed at once. In particular, if a single state is updated multiple times, it is treated as if it were updated only once with the final value.
|
|
110
|
+
|
|
111
|
+
If the `updater` is a function, it can access the last received 'pending value' `nextValue`. Therefore, when `.set` is called multiple times synchronously as shown below, `$count` will be incremented by `3`, but the update still happens only once.
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
$count.set(increment);
|
|
115
|
+
$count.set(increment);
|
|
116
|
+
$count.set(increment);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If you must update based on the current value, you can use `.get()` or `.state`, like `$count.set($count.state.value + 1)`.
|
|
120
|
+
|
|
121
|
+
### Subscribing to State
|
|
122
|
+
|
|
123
|
+
You can detect updates with the `.subscribe(listener)` or `.watch(listener)` methods. Each method returns an unsubscribe function.
|
|
124
|
+
|
|
125
|
+
Upon subscription, if the state was inactive, an update is scheduled. During the update, the state and all its dependencies are activated. Upon unsubscription, if there are no more subscribers to the state, its deactivation is scheduled, and its dependencies are also checked for deactivation.
|
|
126
|
+
|
|
127
|
+
`.subscribe` calls the given function when the state is successfully updated. If the state has already been successfully updated, the function is called once with the current value upon subscription. The listener is called with the state's value as the first argument and `{ signal }` as the second. The `signal` is linked to the state's lifetime.
|
|
128
|
+
|
|
129
|
+
`.watch` calls the given function whenever the state changes. It can be used when you need to handle error or asynchronous states as well.
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
const $count = $(0);
|
|
133
|
+
const unsubscribe = $count.subscribe((value, { signal }) => {
|
|
134
|
+
console.log('value', value);
|
|
135
|
+
signal.then(() => console.log('value end', value));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// value 0
|
|
139
|
+
|
|
140
|
+
$count.set(1);
|
|
141
|
+
// value 1
|
|
142
|
+
// value end 0
|
|
143
|
+
|
|
144
|
+
unsubscribe();
|
|
145
|
+
// value end 1
|
|
146
|
+
|
|
147
|
+
$count.set(2);
|
|
148
|
+
// (no output)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`.subscribe()` returns a function that can be used to unsubscribe. It is important to call this function when a component unmounts to prevent memory leaks.
|
|
152
|
+
|
|
153
|
+
If you want to subscribe to multiple states simultaneously, you should declare another state.
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
const $merged = $((get) => ({
|
|
157
|
+
count: get($count),
|
|
158
|
+
countDouble: get($countDouble),
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
$merged.subscribe(({ count, countDouble }) => console.log(`${count} * 2 = ${countDouble}`));
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Asynchronous State
|
|
165
|
+
|
|
166
|
+
For a derived state where the function returns a `Promise`, you can use the automatically unwrapped value when you `get` or `subscribe` to it. If you want to handle loading or failure cases, you can use `watch` or `state`.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
const $user = $(async (get) => {
|
|
170
|
+
const response = await fetch(`/users/${get($count)}`);
|
|
171
|
+
if (!response.ok) throw new Error('Failed to fetch user');
|
|
172
|
+
return response.json();
|
|
173
|
+
});
|
|
174
|
+
$user.watch(() => {
|
|
175
|
+
console.log($user.state);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const $userName = $((get) => get($user).name);
|
|
179
|
+
|
|
180
|
+
const faultyAtom = $(() => Promise.reject(new Error('Something went wrong')));
|
|
181
|
+
faultyAtom.watch(() => {
|
|
182
|
+
if (!faultyAtom.state.promise && faultyAtom.state.error) {
|
|
183
|
+
console.error('An error occurred:', faultyAtom.state.error.message);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### State Lifetime (`signal`)
|
|
189
|
+
|
|
190
|
+
The `options.signal` passed as an argument to a derived function can be used like an `AbortSignal` and a `Promise` (strictly speaking, a thenable). It is `abort`ed and `resolve`d when the state's lifetime changes, such as when the state is updated or deactivated.
|
|
191
|
+
|
|
192
|
+
Like an `AbortSignal`, it can be passed to existing web APIs like `fetch` or `addEventListener` for cancellation or unsubscription. Like a `Promise`, you can use `signal.then` to write your own cleanup functions.
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
const $user = $(async (get, { signal }) => {
|
|
196
|
+
const count = get($count);
|
|
197
|
+
const json = await fetch(`/users/${count}`, { signal }).then((res) => res.json());
|
|
198
|
+
signal.then(() => {
|
|
199
|
+
console.log(count, json, "not used");
|
|
200
|
+
});
|
|
201
|
+
return json;
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Custom Update Condition (Equality Check)
|
|
206
|
+
|
|
207
|
+
By default, equality is checked with `Object.is`, so for objects or arrays, an update can occur if the reference is different even if the content is the same. To perform additional equality checks, you can provide an `equals` option when declaring the state. In this case, it first checks with `Object.is`, and if they are different, it checks again with the `equals` function. If either returns true, the value change is ignored.
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
const $user = $(
|
|
211
|
+
{ id: 1, name: 'Alice' },
|
|
212
|
+
{ equals: (next, prev) => next.id === prev.id },
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const $user2 = $(
|
|
216
|
+
(get) => get($user).name,
|
|
217
|
+
{ equals: (next, prev) => next.name === prev.name },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
userAtom.set({ id: 1, name: 'Bob' });
|
|
221
|
+
|
|
222
|
+
userAtom.set({ id: 2, name: 'Alice' });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
In the example above, the first update is ignored because the `id` is the same. The second update has a different `id`, so `$user` is updated, but since the `name` is the same, `$user2` is not updated.
|
|
226
|
+
|
|
227
|
+
### Merging Multiple States
|
|
228
|
+
|
|
229
|
+
You can create a new state by merging multiple states with `$$`. It's actually the same as `$`, but while `$`'s `get` function throws immediately when it encounters a `Promise` or an error, `$$`'s `get` function returns a special object to track maximum dependencies with minimum re-executions.
|
|
230
|
+
|
|
231
|
+
The following code takes 5 seconds to merge states with `$`, whereas it takes only 1 second with `$$`.
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
const timer = (time) => new Promise((resolve) => setTimeout(() => resolve(1), time));
|
|
235
|
+
const a = [1, 2, 3, 4, 5].map(() => $(() => timer(1000)));
|
|
236
|
+
const merged = $$((get) => a.map(get));
|
|
237
|
+
console.time();
|
|
238
|
+
merged.subscribe(() => console.timeEnd());
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
For reference, the value that `$$`'s `get` function returns instead of throwing when it encounters a `Promise` or an error is created through the following process:
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const o = () => o;
|
|
245
|
+
const toUndefined = () => undefined;
|
|
246
|
+
Object.setPrototypeOf(o, new Proxy(o, { get: (_, k) => k === Symbol.toPrimitive ? toUndefined : o }));
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The `o` in this code returns the same value no matter how many properties are accessed or functions are called. `o.a.b.c().d()().asdf()()()() === o` is `true`. Therefore, it allows most state-merging functions composed of selectors and simple methods like filter/map/reduce to execute without issues. However, it's not a silver bullet, so some caution is needed, and it should preferably be used only for state merging.
|
|
250
|
+
|
|
251
|
+
## In-Depth
|
|
252
|
+
|
|
253
|
+
### How much should I split the state?
|
|
254
|
+
|
|
255
|
+
Split your state as much as possible, as long as it doesn't significantly harm code readability. Also, wrap as much logic as you can in as many layers of state as possible.
|
|
256
|
+
|
|
257
|
+
In fact, the reason `subscribe` wasn't designed to take a `get` function like `$` is to encourage splitting states as much as possible, so that `subscribe` only deals with the 'final state'.
|
|
258
|
+
|
|
259
|
+
Implicit 'intermediate states' remain 'hidden,' causing you to lose many of the library's benefits and potentially face issues like unnecessary recalculations, inability to reuse intermediate values, complicated dependency tracking, code repetition, broken side-effect idempotency, and the inability to manage fine-grained lifecycles and subscriptions.
|
|
260
|
+
|
|
261
|
+
For example, the following shows a situation where not splitting the state enough leads to 'unnecessary recalculations and inability to reuse intermediate values'.
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
const $userId = $(123);
|
|
265
|
+
const $postId = $(456);
|
|
266
|
+
const $pageData = $(async (get, { signal }) => {
|
|
267
|
+
const user = await fetch(`/users/${get($userId)}`, { signal }).then((res) => res.json());
|
|
268
|
+
const post = await fetch(`/posts/${get($postId)}`, { signal }).then((res) => res.json());
|
|
269
|
+
userElm.innerHTML = user.name;
|
|
270
|
+
postElm.innerHTML = post.html;
|
|
271
|
+
commentElm.innerHTML = `Hello ${user.name}! Comment to ${post.author}.`;
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
This code looks simple and clean, but if only one of `userId` or `postId` is updated, both requests are sent again. The latencies of `user` and `post` are summed up (which can be solved with `Promise.all`, but this increases code complexity). Unrelated side effects coexist, mixing contexts. And even if other values in `user` or `post` don't change, the `innerHTML` is updated, causing the DOM to be completely replaced. There are several problems. It should be split as follows:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
const $userId = $(123);
|
|
279
|
+
const $user = $((get) => fetch(`/users/${get($userId)}`, { signal }).then((res) => res.json()));
|
|
280
|
+
$user.subscribe((user) => { userElm.innerHTML = user.name; });
|
|
281
|
+
|
|
282
|
+
const $postId = $(456);
|
|
283
|
+
const $post = $((get) => fetch(`/posts/${get($postId)}`, { signal }).then((res) => res.json()));
|
|
284
|
+
$post.subscribe((post) => { postElm.innerHTML = post.html; });
|
|
285
|
+
|
|
286
|
+
const $pageData = $$((get) => ({
|
|
287
|
+
userName: get($user).name,
|
|
288
|
+
postAuthor: get($post).author,
|
|
289
|
+
}));
|
|
290
|
+
$pageData.subscribe(({ userName, postAuthor }) => { commentElm.innerHTML = `Hello ${userName}! Comment to ${postAuthor}.`; });
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The number of lines of code has increased slightly, but the previously mentioned problems have been resolved.
|
|
294
|
+
|
|
295
|
+
This might be too simple of an example to be fully convincing, but in real-world scenarios, it's easy to accidentally mix states in a moment of carelessness. Also, the desire to handle everything in one place can often be hard to resist.
|
|
296
|
+
|
|
297
|
+
It's important to always be mindful of this and to split, wrap, and layer your states.
|
|
298
|
+
|
|
299
|
+
### How to implement `onMount`/`onCleanup`?
|
|
300
|
+
|
|
301
|
+
Sometimes you need to call a function not every time a state's value changes, but when the state becomes active and inactive (i.e., when subscribers start appearing and when there are no longer any subscribers). In other words, you need functionality like `onMount`/`onCleanup` (or `onDestroy`, etc.).
|
|
302
|
+
|
|
303
|
+
This can be solved in two ways. One is to create and return a state from within another state:
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const $shared = $((_, { signal }) => {
|
|
307
|
+
const $state = $(0);
|
|
308
|
+
/* onMount */
|
|
309
|
+
signal.then(() => /* onCleanup */);
|
|
310
|
+
return $state;
|
|
311
|
+
});
|
|
312
|
+
const $a = $((get) => {
|
|
313
|
+
const $state = get($shared);
|
|
314
|
+
const value = get($state);
|
|
315
|
+
return value;
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
If modifications to `$state` only occur within `onMount` and `onCleanup` (for example, when subscribing to external events), this is the cleanest pattern. The following is an example that applies this to manage a connection shared by multiple places:
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
const $wsConnection = $(() => {
|
|
323
|
+
const conn = new WebSocket("...");
|
|
324
|
+
signal.then(() => conn.close());
|
|
325
|
+
|
|
326
|
+
const listeners = new Set();
|
|
327
|
+
conn.onmessage = (e) => {
|
|
328
|
+
const data = JSON.parse(e.data);
|
|
329
|
+
for (const listener of listeners) subscriber(data);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
send: (message) => conn.send(JSON.stringify(message));
|
|
334
|
+
addEventListener: (listener, signal) => {
|
|
335
|
+
listeners.add(listener);
|
|
336
|
+
signal.then(() => listener.delete(listener));
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const lastMessage = (name) =>
|
|
342
|
+
$((get, { signal }) => {
|
|
343
|
+
const { send, addEventListener } = get($wsConnection);
|
|
344
|
+
const $lastMessage = $(null);
|
|
345
|
+
addEventListener(({ type, value }) => {
|
|
346
|
+
if (type === name) $lastMessage.set(value);
|
|
347
|
+
}, signal);
|
|
348
|
+
|
|
349
|
+
send(`+${name}`);
|
|
350
|
+
signal.then(() => send(`-${name}`));
|
|
351
|
+
return $lastMessage;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const $alice = lastMessage("alice");
|
|
355
|
+
const $bob = lastMessage("bob");
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
If the state needs to be modifiable from the outside, you can create two states like this:
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
const $writer = $(0);
|
|
362
|
+
const $shared = $((_, { signal }) => {
|
|
363
|
+
// onMount
|
|
364
|
+
signal.then(() => /* onCleanup */);
|
|
365
|
+
});
|
|
366
|
+
const $reader = $((get) => {
|
|
367
|
+
get($shared);
|
|
368
|
+
return get($writer);
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Now, you can read from `$reader` and write to `$writer`. Since `$shared` does not depend on `$writer`, `$shared` will not be updated even if `$writer` is modified.
|
|
373
|
+
|
|
374
|
+
## Examples
|
|
375
|
+
|
|
376
|
+
#### Debounce-Throttling
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
const delayedState = (initial, minDelay, maxDelay) => {
|
|
380
|
+
const $value = $(initial);
|
|
381
|
+
const $delayedValue = $(initial);
|
|
382
|
+
|
|
383
|
+
const $eventStartTime = $(0);
|
|
384
|
+
const $eventLastTime = $(0);
|
|
385
|
+
const $delayedTime = $((get) => Math.min(get($eventStartTime) + maxDelay, get($eventLastTime) + minDelay));
|
|
386
|
+
const $delayedInfo = $((get) => ({
|
|
387
|
+
value: get($value),
|
|
388
|
+
time: get($delayedTime),
|
|
389
|
+
}));
|
|
390
|
+
$delayedInfo.subscribe(({ value, time }, { signal }) => {
|
|
391
|
+
const timeout = Math.max(0, time - Date.now());
|
|
392
|
+
const timer = setTimeout(() => $delayedValue.set(value), timeout);
|
|
393
|
+
signal.then(() => clearTimeout(timer));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const update = (value, eager = false) => {
|
|
397
|
+
const now = eager ? -Infinity : Date.now();
|
|
398
|
+
if ($value.get() === $delayedValue.get()) $eventStartTime.set(now);
|
|
399
|
+
$eventLastTime.set(now);
|
|
400
|
+
$value.set(value);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return [$delayedValue, update];
|
|
404
|
+
};
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
const [$inputValue, updateInput] = delayedState("", 200, 1000);
|
|
409
|
+
inputElm.addEventListener("input", (e) => updateInput(e.currentTarget.value));
|
|
410
|
+
$inputValue.subscribe(console.log);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Scroll Direction Detection
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const $windowScroll = $((_, { signal }) => {
|
|
417
|
+
let lastTime = Date.now();
|
|
418
|
+
const $scrollY = $(window.scrollY);
|
|
419
|
+
const $scrollOnTop = $((get) => get($scrollY) === 0);
|
|
420
|
+
|
|
421
|
+
const $scrollMovingAvgY = $(0);
|
|
422
|
+
const $scrollDirectionY = $((get) => Math.sign(get($scrollMovingAvgY)));
|
|
423
|
+
|
|
424
|
+
const onScrollChange = () => {
|
|
425
|
+
const now = Date.now();
|
|
426
|
+
lastTime = now;
|
|
427
|
+
|
|
428
|
+
const alpha = 0.995 ** (now - lastTime);
|
|
429
|
+
const scrollY = window.scrollY;
|
|
430
|
+
const deltaY = scrollY - $scrollY.get();
|
|
431
|
+
const movingAvgY = alpha * $scrollMovingAvgY.get() + (1 - alpha) * deltaY;
|
|
432
|
+
|
|
433
|
+
$scrollY.set(scrollY);
|
|
434
|
+
$scrollMovingAvgY.set(movingAvgY);
|
|
435
|
+
};
|
|
436
|
+
window.addEventListener('scroll', onScrollChange, {
|
|
437
|
+
passive: true,
|
|
438
|
+
signal,
|
|
439
|
+
});
|
|
440
|
+
window.addEventListener('resize', onScrollChange, {
|
|
441
|
+
passive: true,
|
|
442
|
+
signal,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
$scrollY,
|
|
447
|
+
$scrollOnTop,
|
|
448
|
+
$scrollMovingAvgY,
|
|
449
|
+
$scrollDirectionY,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const $navHidden = $((get) => {
|
|
454
|
+
const { $scrollOnTop, $scrollDirectionY } = get($windowScroll);
|
|
455
|
+
const scrollOnTop = get($scrollOnTop);
|
|
456
|
+
const directionY = get($scrollDirectionY);
|
|
457
|
+
return !scrollOnTop && directionY > 0;
|
|
458
|
+
});
|
|
459
|
+
```
|
package/biome.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"maxSize": 16777216,
|
|
4
|
+
"ignoreUnknown": true
|
|
5
|
+
},
|
|
6
|
+
"formatter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"lineWidth": 80,
|
|
9
|
+
"indentStyle": "tab",
|
|
10
|
+
"indentWidth": 4
|
|
11
|
+
},
|
|
12
|
+
"javascript": {
|
|
13
|
+
"formatter": {
|
|
14
|
+
"quoteStyle": "single"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"linter": {
|
|
18
|
+
"rules": {
|
|
19
|
+
"correctness": {
|
|
20
|
+
"noConstantCondition": "off",
|
|
21
|
+
"noEmptyCharacterClassInRegex": "off"
|
|
22
|
+
},
|
|
23
|
+
"security": {
|
|
24
|
+
"noDangerouslySetInnerHtml": "off"
|
|
25
|
+
},
|
|
26
|
+
"style": {
|
|
27
|
+
"noParameterAssign": "off",
|
|
28
|
+
"noNonNullAssertion": "off"
|
|
29
|
+
},
|
|
30
|
+
"suspicious": {
|
|
31
|
+
"noAssignInExpressions": "off",
|
|
32
|
+
"noCatchAssign": "off",
|
|
33
|
+
"noConfusingLabels": "off",
|
|
34
|
+
"noExplicitAny": "off",
|
|
35
|
+
"noThenProperty": "off"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bansa",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@biomejs/biome": "2.0.0-beta.6",
|
|
8
|
+
"@types/react": "^19.1.8",
|
|
9
|
+
"@types/react-dom": "^19.1.6",
|
|
10
|
+
"vitest": "^3.1.4"
|
|
11
|
+
},
|
|
12
|
+
"author": "cgiosy",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git://github.com/cgiosy/bansa.git"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/cgiosy/bansa",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"bansa",
|
|
24
|
+
"state",
|
|
25
|
+
"react"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest",
|
|
29
|
+
"format": "biome format --write ./src"
|
|
30
|
+
}
|
|
31
|
+
}
|