bunja 0.0.12 → 0.1.0
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/.vscode/settings.json +2 -1
- package/README.md +167 -1
- package/bunja.ts +59 -48
- package/deno.json +9 -0
- package/deno.lock +487 -0
- package/dist/bunja-3B0nQ1vI.js +155 -0
- package/dist/bunja-QjknYXY-.cjs +186 -0
- package/dist/bunja.cjs +9 -0
- package/dist/bunja.d.cts +53 -0
- package/dist/bunja.d.ts +53 -0
- package/dist/bunja.js +3 -0
- package/dist/react.cjs +65 -0
- package/dist/react.d.cts +9 -0
- package/dist/react.d.ts +9 -0
- package/dist/react.js +36 -0
- package/package.json +44 -4
- package/react.ts +16 -11
- package/tsconfig.json +7 -1
- package/tsdown.config.ts +10 -0
package/.vscode/settings.json
CHANGED
package/README.md
CHANGED
|
@@ -57,4 +57,170 @@ function MyComponent() {
|
|
|
57
57
|
}
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
### Defining a Bunja that relies on other Bunja
|
|
61
|
+
|
|
62
|
+
If you want to manage a state with a broad lifetime and another state with a narrower lifetime, you can create a (narrower) bunja that depends on a (broader) bunja.
|
|
63
|
+
For example, you can think of a bunja that manages the WebSocket connection and disconnection, and another bunja that subscribes to a specific resource over the connected WebSocket.
|
|
64
|
+
|
|
65
|
+
In an application composed of multiple pages, you might want to subscribe to the Foo resource on page A and the Bar resource on page B, while using the same WebSocket connection regardless of which page you're on.
|
|
66
|
+
In such a case, you can write the following code.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// To simplify the example, code for buffering and reconnection has been omitted.
|
|
70
|
+
const websocketBunja = bunja([], () => {
|
|
71
|
+
let socket;
|
|
72
|
+
const send = (message) => socket.send(JSON.stringify(message));
|
|
73
|
+
|
|
74
|
+
const emitter = new EventEmitter();
|
|
75
|
+
const on = (handler) => {
|
|
76
|
+
emitter.on("message", handler);
|
|
77
|
+
return () => emitter.off("message", handler);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
send,
|
|
82
|
+
on,
|
|
83
|
+
[bunja.effect]() {
|
|
84
|
+
socket = new WebSocket("...");
|
|
85
|
+
socket.onmessage = (e) => emitter.emit("message", JSON.parse(e.data));
|
|
86
|
+
return () => socket.close();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const resourceFooBunja = bunja([websocketBunja], ({ send, on }) => {
|
|
92
|
+
const resourceFooAtom = atom();
|
|
93
|
+
return {
|
|
94
|
+
resourceFooAtom,
|
|
95
|
+
[bunja.effect]() {
|
|
96
|
+
const off = on((message) => {
|
|
97
|
+
if (message.type === "foo") store.set(resourceAtom, message.value);
|
|
98
|
+
});
|
|
99
|
+
send("subscribe-foo");
|
|
100
|
+
return () => {
|
|
101
|
+
send("unsubscribe-foo");
|
|
102
|
+
off();
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const resourceBarBunja = bunja([websocketBunja], ({ send, on }) => {
|
|
109
|
+
const resourceBarAtom = atom();
|
|
110
|
+
// ...
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function PageA() {
|
|
114
|
+
const { resourceFooAtom } = useBunja(resourceFooBunja);
|
|
115
|
+
const resourceFoo = useAtomValue(resourceFooAtom);
|
|
116
|
+
// ...
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function PageB() {
|
|
120
|
+
const { resourceBarAtom } = useBunja(resourceBarBunja);
|
|
121
|
+
const resourceBar = useAtomValue(resourceBarAtom);
|
|
122
|
+
// ...
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Notice that `websocketBunja` is not directly `useBunja`-ed.
|
|
127
|
+
When you `useBunja` either `resourceFooBunja` or `resourceBarBunja`, since they depend on `websocketBunja`,
|
|
128
|
+
it has the same effect as if `websocketBunja` were also `useBunja`-ed.
|
|
129
|
+
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> When a bunja starts, the initialization effect of the bunja with a broader lifetime is called first.\
|
|
132
|
+
> Similarly, when a bunja ends, the cleanup effect of the bunja with the broader lifetime is called first.\
|
|
133
|
+
> This behavior is aligned with how React's `useEffect` cleanup function is invoked, where the parent’s cleanup is executed before the child’s in the render tree.
|
|
134
|
+
>
|
|
135
|
+
> See: <https://github.com/facebook/react/issues/16728>
|
|
136
|
+
|
|
137
|
+
### Dependency injection using Scope
|
|
138
|
+
|
|
139
|
+
You can use a bunja for local state management.\
|
|
140
|
+
When you specify a scope as a dependency of the bunja, separate bunja instances are created based on the values injected into the scope.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { bunja, createScope } from "bunja";
|
|
144
|
+
|
|
145
|
+
const UrlScope = createScope();
|
|
146
|
+
|
|
147
|
+
const fetchBunja = bunja([UrlScope], (url) => {
|
|
148
|
+
const queryAtom = atomWithQuery((get) => ({
|
|
149
|
+
queryKey: [url],
|
|
150
|
+
queryFn: async () => (await fetch(url)).json(),
|
|
151
|
+
}));
|
|
152
|
+
return { queryAtom };
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Injecting dependencies via React context
|
|
157
|
+
|
|
158
|
+
If you bind a scope to a React context, bunjas that depend on the scope can retrieve values from the corresponding React context.
|
|
159
|
+
|
|
160
|
+
In the example below, there are two React instances (`<ChildComponent />`) that reference the same `fetchBunja`, but since each looks at a different context value, two separate bunja instances are also created.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { createContext } from "react";
|
|
164
|
+
import { bunja, createScope } from "bunja";
|
|
165
|
+
import { bindScope } from "bunja/react";
|
|
166
|
+
|
|
167
|
+
const UrlContext = createContext("https://example.com/");
|
|
168
|
+
const UrlScope = createScope();
|
|
169
|
+
bindScope(UrlScope, UrlContext);
|
|
170
|
+
|
|
171
|
+
const fetchBunja = bunja([UrlScope], (url) => {
|
|
172
|
+
const queryAtom = atomWithQuery((get) => ({
|
|
173
|
+
queryKey: [url],
|
|
174
|
+
queryFn: async () => (await fetch(url)).json(),
|
|
175
|
+
}));
|
|
176
|
+
return { queryAtom };
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function ParentComponent() {
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<UrlContext.Provider value="https://example.com/foo">
|
|
183
|
+
<ChildComponent />
|
|
184
|
+
</UrlContext.Provider>
|
|
185
|
+
<UrlContext.Provider value="https://example.com/bar">
|
|
186
|
+
<ChildComponent />
|
|
187
|
+
</UrlContext.Provider>
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ChildComponent() {
|
|
193
|
+
const { queryAtom } = useBunja(fetchBunja);
|
|
194
|
+
const { data, isPending, isError } = useAtomValue(queryAtom);
|
|
195
|
+
// Your component logic here
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
You can use the `createScopeFromContext` function to handle both the creation of the scope and the binding to the context in one step.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { createContext } from "react";
|
|
203
|
+
import { createScopeFromContext } from "bunja/react";
|
|
204
|
+
|
|
205
|
+
const UrlContext = createContext("https://example.com/");
|
|
206
|
+
const UrlScope = createScopeFromContext(UrlContext);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### Injecting dependencies directly into the scope
|
|
210
|
+
|
|
211
|
+
You might want to use a bunja directly within a React component where the values to be injected into the scope are created.
|
|
212
|
+
|
|
213
|
+
In such cases, you can use the inject function to inject values into the scope without wrapping the context separately.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { inject } from "bunja/react";
|
|
217
|
+
|
|
218
|
+
function MyComponent() {
|
|
219
|
+
const { queryAtom } = useBunja(
|
|
220
|
+
fetchBunja,
|
|
221
|
+
inject([[UrlScope, "https://example.com/"]])
|
|
222
|
+
);
|
|
223
|
+
const { data, isPending, isError } = useAtomValue(queryAtom);
|
|
224
|
+
// Your component logic here
|
|
225
|
+
}
|
|
226
|
+
```
|
package/bunja.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type Dep<T> = Bunja<T> | Scope<T>;
|
|
2
2
|
|
|
3
|
+
const bunjaEffectSymbol: unique symbol = Symbol("Bunja.effect");
|
|
4
|
+
type BunjaEffectSymbol = typeof bunjaEffectSymbol;
|
|
5
|
+
|
|
3
6
|
export class Bunja<T> {
|
|
4
7
|
public static readonly bunjas: Bunja<any>[] = [];
|
|
5
8
|
public readonly id: number;
|
|
@@ -9,13 +12,13 @@ export class Bunja<T> {
|
|
|
9
12
|
public parents: Bunja<any>[], // one depth parents
|
|
10
13
|
public relatedBunjas: Bunja<any>[], // toposorted parents without self
|
|
11
14
|
public relatedScopes: Scope<any>[], // deduped
|
|
12
|
-
public init: (...args: any[]) => T & BunjaValue
|
|
15
|
+
public init: (...args: any[]) => T & BunjaValue,
|
|
13
16
|
) {
|
|
14
17
|
this.id = Bunja.bunjas.length;
|
|
15
18
|
Bunja.bunjas.push(this);
|
|
16
19
|
}
|
|
17
|
-
static readonly effect =
|
|
18
|
-
toString() {
|
|
20
|
+
static readonly effect: BunjaEffectSymbol = bunjaEffectSymbol;
|
|
21
|
+
toString(): string {
|
|
19
22
|
const { id, debugLabel } = this;
|
|
20
23
|
return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
21
24
|
}
|
|
@@ -29,7 +32,7 @@ export class Scope<T> {
|
|
|
29
32
|
this.id = Scope.scopes.length;
|
|
30
33
|
Scope.scopes.push(this);
|
|
31
34
|
}
|
|
32
|
-
toString() {
|
|
35
|
+
toString(): string {
|
|
33
36
|
const { id, debugLabel } = this;
|
|
34
37
|
return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
35
38
|
}
|
|
@@ -40,12 +43,19 @@ export type ReadScope = <T>(scope: Scope<T>) => T;
|
|
|
40
43
|
export class BunjaStore {
|
|
41
44
|
#bunjas: Record<string, BunjaInstance> = {};
|
|
42
45
|
#scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
|
|
43
|
-
get<T>(
|
|
46
|
+
get<T>(
|
|
47
|
+
bunja: Bunja<T>,
|
|
48
|
+
readScope: ReadScope,
|
|
49
|
+
): {
|
|
50
|
+
value: T;
|
|
51
|
+
mount: () => void;
|
|
52
|
+
deps: any[];
|
|
53
|
+
} {
|
|
44
54
|
const scopeInstanceMap = new Map(
|
|
45
55
|
bunja.relatedScopes.map((scope) => [
|
|
46
56
|
scope,
|
|
47
57
|
this.#getScopeInstance(scope, readScope(scope)),
|
|
48
|
-
])
|
|
58
|
+
]),
|
|
49
59
|
);
|
|
50
60
|
const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
|
|
51
61
|
const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
|
|
@@ -55,7 +65,7 @@ export class BunjaStore {
|
|
|
55
65
|
relatedBunjaInstanceMap.forEach((related) => related.add());
|
|
56
66
|
bunjaInstance.add();
|
|
57
67
|
scopeInstanceMap.forEach((scope) => scope.add());
|
|
58
|
-
return function unmount() {
|
|
68
|
+
return function unmount(): void {
|
|
59
69
|
// concern: reverse order?
|
|
60
70
|
relatedBunjaInstanceMap.forEach((related) => related.sub());
|
|
61
71
|
bunjaInstance.sub();
|
|
@@ -67,10 +77,10 @@ export class BunjaStore {
|
|
|
67
77
|
}
|
|
68
78
|
#getBunjaInstance(
|
|
69
79
|
bunja: Bunja<any>,
|
|
70
|
-
scopeInstanceMap: Map<Scope<any>, ScopeInstance
|
|
80
|
+
scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
|
|
71
81
|
): BunjaInstance {
|
|
72
82
|
const localScopeInstanceMap = new Map(
|
|
73
|
-
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!])
|
|
83
|
+
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
|
|
74
84
|
);
|
|
75
85
|
const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
|
|
76
86
|
.map(({ instanceId }) => instanceId)
|
|
@@ -81,7 +91,7 @@ export class BunjaStore {
|
|
|
81
91
|
bunja.relatedBunjas.map((relatedBunja) => [
|
|
82
92
|
relatedBunja,
|
|
83
93
|
this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
|
|
84
|
-
])
|
|
94
|
+
]),
|
|
85
95
|
);
|
|
86
96
|
const args = bunja.deps.map((dep) => {
|
|
87
97
|
if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
|
|
@@ -92,73 +102,74 @@ export class BunjaStore {
|
|
|
92
102
|
() => delete this.#bunjas[bunjaInstanceId],
|
|
93
103
|
bunjaInstanceId,
|
|
94
104
|
relatedBunjaInstanceMap,
|
|
95
|
-
bunja.init.apply(bunja, args)
|
|
105
|
+
bunja.init.apply(bunja, args),
|
|
96
106
|
);
|
|
97
107
|
this.#bunjas[bunjaInstanceId] = bunjaInstance;
|
|
98
108
|
return bunjaInstance;
|
|
99
109
|
}
|
|
100
110
|
#getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
|
|
101
|
-
const scopeInstanceMap =
|
|
102
|
-
this.#scopes.
|
|
111
|
+
const scopeInstanceMap = this.#scopes.get(scope) ??
|
|
112
|
+
this.#scopes.set(scope, new Map()).get(scope)!;
|
|
103
113
|
const init = () =>
|
|
104
114
|
new ScopeInstance(
|
|
105
115
|
() => scopeInstanceMap.delete(value),
|
|
106
116
|
ScopeInstance.counter++,
|
|
107
117
|
scope,
|
|
108
|
-
value
|
|
118
|
+
value,
|
|
109
119
|
);
|
|
110
120
|
return (
|
|
111
121
|
scopeInstanceMap.get(value) ??
|
|
112
|
-
|
|
122
|
+
scopeInstanceMap.set(value, init()).get(value)!
|
|
113
123
|
);
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
export const createBunjaStore = () => new BunjaStore();
|
|
127
|
+
export const createBunjaStore = (): BunjaStore => new BunjaStore();
|
|
118
128
|
|
|
119
129
|
export type BunjaEffectFn = () => () => void;
|
|
120
130
|
export interface BunjaValue {
|
|
121
131
|
[Bunja.effect]?: BunjaEffectFn;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
|
-
|
|
125
|
-
export function bunja<T, U>(
|
|
126
|
-
deps: [Dep<U>],
|
|
127
|
-
init: (u: U) => T & BunjaValue
|
|
128
|
-
): Bunja<T>;
|
|
129
|
-
export function bunja<T, U, V>(
|
|
130
|
-
deps: [Dep<U>, Dep<V>],
|
|
131
|
-
init: (u: U, v: V) => T & BunjaValue
|
|
132
|
-
): Bunja<T>;
|
|
133
|
-
export function bunja<T, U, V, W>(
|
|
134
|
-
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
135
|
-
init: (u: U, v: V, w: W) => T & BunjaValue
|
|
136
|
-
): Bunja<T>;
|
|
137
|
-
export function bunja<T, U, V, W, X>(
|
|
138
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
|
|
139
|
-
init: (u: U, v: V, w: W, x: X) => T & BunjaValue
|
|
140
|
-
): Bunja<T>;
|
|
141
|
-
export function bunja<T, U, V, W, X, Y>(
|
|
142
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
|
|
143
|
-
init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue
|
|
144
|
-
): Bunja<T>;
|
|
145
|
-
export function bunja<T, U, V, W, X, Y, Z>(
|
|
146
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
|
|
147
|
-
init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue
|
|
148
|
-
): Bunja<T>;
|
|
149
|
-
export function bunja<T, const U extends any[]>(
|
|
134
|
+
function bunjaImpl<T, const U extends any[]>(
|
|
150
135
|
deps: { [K in keyof U]: Dep<U[K]> },
|
|
151
|
-
init: (...args: U) => T & BunjaValue
|
|
136
|
+
init: (...args: U) => T & BunjaValue,
|
|
152
137
|
): Bunja<T> {
|
|
153
138
|
const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
|
|
154
139
|
const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
|
|
155
140
|
const relatedBunjas = toposort(parents);
|
|
156
141
|
const relatedScopes = Array.from(
|
|
157
|
-
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)])
|
|
142
|
+
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
|
|
158
143
|
);
|
|
159
144
|
return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
|
|
160
145
|
}
|
|
161
|
-
|
|
146
|
+
bunjaImpl.effect = Bunja.effect;
|
|
147
|
+
|
|
148
|
+
export const bunja: {
|
|
149
|
+
<T>(deps: [], init: () => T & BunjaValue): Bunja<T>;
|
|
150
|
+
<T, U>(deps: [Dep<U>], init: (u: U) => T & BunjaValue): Bunja<T>;
|
|
151
|
+
<T, U, V>(
|
|
152
|
+
deps: [Dep<U>, Dep<V>],
|
|
153
|
+
init: (u: U, v: V) => T & BunjaValue,
|
|
154
|
+
): Bunja<T>;
|
|
155
|
+
<T, U, V, W>(
|
|
156
|
+
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
157
|
+
init: (u: U, v: V, w: W) => T & BunjaValue,
|
|
158
|
+
): Bunja<T>;
|
|
159
|
+
<T, U, V, W, X>(
|
|
160
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
|
|
161
|
+
init: (u: U, v: V, w: W, x: X) => T & BunjaValue,
|
|
162
|
+
): Bunja<T>;
|
|
163
|
+
<T, U, V, W, X, Y>(
|
|
164
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
|
|
165
|
+
init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue,
|
|
166
|
+
): Bunja<T>;
|
|
167
|
+
<T, U, V, W, X, Y, Z>(
|
|
168
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
|
|
169
|
+
init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue,
|
|
170
|
+
): Bunja<T>;
|
|
171
|
+
readonly effect: BunjaEffectSymbol;
|
|
172
|
+
} = bunjaImpl;
|
|
162
173
|
|
|
163
174
|
export function createScope<T>(): Scope<T> {
|
|
164
175
|
return new Scope();
|
|
@@ -191,7 +202,7 @@ class BunjaInstance extends RefCounter {
|
|
|
191
202
|
dispose: () => void,
|
|
192
203
|
public instanceId: string,
|
|
193
204
|
public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
|
|
194
|
-
public value: BunjaValue
|
|
205
|
+
public value: BunjaValue,
|
|
195
206
|
) {
|
|
196
207
|
super();
|
|
197
208
|
this.#dispose = () => {
|
|
@@ -199,7 +210,7 @@ class BunjaInstance extends RefCounter {
|
|
|
199
210
|
dispose();
|
|
200
211
|
};
|
|
201
212
|
}
|
|
202
|
-
add() {
|
|
213
|
+
override add() {
|
|
203
214
|
this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
|
|
204
215
|
super.add();
|
|
205
216
|
}
|
|
@@ -214,7 +225,7 @@ class ScopeInstance extends RefCounter {
|
|
|
214
225
|
public dispose: () => void,
|
|
215
226
|
public instanceId: number,
|
|
216
227
|
public scope: Scope<any>,
|
|
217
|
-
public value: any
|
|
228
|
+
public value: any,
|
|
218
229
|
) {
|
|
219
230
|
super();
|
|
220
231
|
}
|