bunja 0.0.11 → 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 +60 -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 +18 -13
- 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,21 +65,22 @@ 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();
|
|
62
72
|
scopeInstanceMap.forEach((scope) => scope.sub());
|
|
63
73
|
};
|
|
64
74
|
},
|
|
75
|
+
deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
|
|
65
76
|
};
|
|
66
77
|
}
|
|
67
78
|
#getBunjaInstance(
|
|
68
79
|
bunja: Bunja<any>,
|
|
69
|
-
scopeInstanceMap: Map<Scope<any>, ScopeInstance
|
|
80
|
+
scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
|
|
70
81
|
): BunjaInstance {
|
|
71
82
|
const localScopeInstanceMap = new Map(
|
|
72
|
-
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!])
|
|
83
|
+
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
|
|
73
84
|
);
|
|
74
85
|
const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
|
|
75
86
|
.map(({ instanceId }) => instanceId)
|
|
@@ -80,7 +91,7 @@ export class BunjaStore {
|
|
|
80
91
|
bunja.relatedBunjas.map((relatedBunja) => [
|
|
81
92
|
relatedBunja,
|
|
82
93
|
this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
|
|
83
|
-
])
|
|
94
|
+
]),
|
|
84
95
|
);
|
|
85
96
|
const args = bunja.deps.map((dep) => {
|
|
86
97
|
if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
|
|
@@ -91,73 +102,74 @@ export class BunjaStore {
|
|
|
91
102
|
() => delete this.#bunjas[bunjaInstanceId],
|
|
92
103
|
bunjaInstanceId,
|
|
93
104
|
relatedBunjaInstanceMap,
|
|
94
|
-
bunja.init.apply(bunja, args)
|
|
105
|
+
bunja.init.apply(bunja, args),
|
|
95
106
|
);
|
|
96
107
|
this.#bunjas[bunjaInstanceId] = bunjaInstance;
|
|
97
108
|
return bunjaInstance;
|
|
98
109
|
}
|
|
99
110
|
#getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
|
|
100
|
-
const scopeInstanceMap =
|
|
101
|
-
this.#scopes.
|
|
111
|
+
const scopeInstanceMap = this.#scopes.get(scope) ??
|
|
112
|
+
this.#scopes.set(scope, new Map()).get(scope)!;
|
|
102
113
|
const init = () =>
|
|
103
114
|
new ScopeInstance(
|
|
104
115
|
() => scopeInstanceMap.delete(value),
|
|
105
116
|
ScopeInstance.counter++,
|
|
106
117
|
scope,
|
|
107
|
-
value
|
|
118
|
+
value,
|
|
108
119
|
);
|
|
109
120
|
return (
|
|
110
121
|
scopeInstanceMap.get(value) ??
|
|
111
|
-
|
|
122
|
+
scopeInstanceMap.set(value, init()).get(value)!
|
|
112
123
|
);
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
export const createBunjaStore = () => new BunjaStore();
|
|
127
|
+
export const createBunjaStore = (): BunjaStore => new BunjaStore();
|
|
117
128
|
|
|
118
129
|
export type BunjaEffectFn = () => () => void;
|
|
119
130
|
export interface BunjaValue {
|
|
120
131
|
[Bunja.effect]?: BunjaEffectFn;
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
export function bunja<T, U>(
|
|
125
|
-
deps: [Dep<U>],
|
|
126
|
-
init: (u: U) => T & BunjaValue
|
|
127
|
-
): Bunja<T>;
|
|
128
|
-
export function bunja<T, U, V>(
|
|
129
|
-
deps: [Dep<U>, Dep<V>],
|
|
130
|
-
init: (u: U, v: V) => T & BunjaValue
|
|
131
|
-
): Bunja<T>;
|
|
132
|
-
export function bunja<T, U, V, W>(
|
|
133
|
-
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
134
|
-
init: (u: U, v: V, w: W) => T & BunjaValue
|
|
135
|
-
): Bunja<T>;
|
|
136
|
-
export function bunja<T, U, V, W, X>(
|
|
137
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
|
|
138
|
-
init: (u: U, v: V, w: W, x: X) => T & BunjaValue
|
|
139
|
-
): Bunja<T>;
|
|
140
|
-
export function bunja<T, U, V, W, X, Y>(
|
|
141
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
|
|
142
|
-
init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue
|
|
143
|
-
): Bunja<T>;
|
|
144
|
-
export function bunja<T, U, V, W, X, Y, Z>(
|
|
145
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
|
|
146
|
-
init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue
|
|
147
|
-
): Bunja<T>;
|
|
148
|
-
export function bunja<T, const U extends any[]>(
|
|
134
|
+
function bunjaImpl<T, const U extends any[]>(
|
|
149
135
|
deps: { [K in keyof U]: Dep<U[K]> },
|
|
150
|
-
init: (...args: U) => T & BunjaValue
|
|
136
|
+
init: (...args: U) => T & BunjaValue,
|
|
151
137
|
): Bunja<T> {
|
|
152
138
|
const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
|
|
153
139
|
const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
|
|
154
140
|
const relatedBunjas = toposort(parents);
|
|
155
141
|
const relatedScopes = Array.from(
|
|
156
|
-
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)])
|
|
142
|
+
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
|
|
157
143
|
);
|
|
158
144
|
return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
|
|
159
145
|
}
|
|
160
|
-
|
|
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;
|
|
161
173
|
|
|
162
174
|
export function createScope<T>(): Scope<T> {
|
|
163
175
|
return new Scope();
|
|
@@ -190,7 +202,7 @@ class BunjaInstance extends RefCounter {
|
|
|
190
202
|
dispose: () => void,
|
|
191
203
|
public instanceId: string,
|
|
192
204
|
public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
|
|
193
|
-
public value: BunjaValue
|
|
205
|
+
public value: BunjaValue,
|
|
194
206
|
) {
|
|
195
207
|
super();
|
|
196
208
|
this.#dispose = () => {
|
|
@@ -198,7 +210,7 @@ class BunjaInstance extends RefCounter {
|
|
|
198
210
|
dispose();
|
|
199
211
|
};
|
|
200
212
|
}
|
|
201
|
-
add() {
|
|
213
|
+
override add() {
|
|
202
214
|
this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
|
|
203
215
|
super.add();
|
|
204
216
|
}
|
|
@@ -213,7 +225,7 @@ class ScopeInstance extends RefCounter {
|
|
|
213
225
|
public dispose: () => void,
|
|
214
226
|
public instanceId: number,
|
|
215
227
|
public scope: Scope<any>,
|
|
216
|
-
public value: any
|
|
228
|
+
public value: any,
|
|
217
229
|
) {
|
|
218
230
|
super();
|
|
219
231
|
}
|