bunja 0.0.6 → 0.0.8
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.md +3 -3
- package/bunja.ts +153 -71
- package/package.json +5 -3
- package/react.ts +34 -0
- package/tsconfig.json +2 -1
package/README.md
CHANGED
|
@@ -33,16 +33,16 @@ You can use `bunja` to define a state with a finite lifetime and use the `useBun
|
|
|
33
33
|
You can define a bunja using the `bunja` function. When you access the defined bunja with the `useBunja` hook, a bunja instance is created.\
|
|
34
34
|
If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed.
|
|
35
35
|
|
|
36
|
-
If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `
|
|
36
|
+
If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `bunja.effect` field.
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
import { bunja,
|
|
39
|
+
import { bunja, useBunja } from "bunja";
|
|
40
40
|
|
|
41
41
|
const countBunja = bunja([], () => {
|
|
42
42
|
const countAtom = atom(0);
|
|
43
43
|
return {
|
|
44
44
|
countAtom,
|
|
45
|
-
[
|
|
45
|
+
[bunja.effect]() {
|
|
46
46
|
console.log("mounted");
|
|
47
47
|
return () => console.log("unmounted");
|
|
48
48
|
},
|
package/bunja.ts
CHANGED
|
@@ -1,33 +1,116 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export type Dep<T> = React.Context<T> | Bunja<T>;
|
|
1
|
+
export type Dep<T> = Bunja<T> | Scope<T>;
|
|
4
2
|
|
|
5
3
|
export class Bunja<T> {
|
|
4
|
+
public static readonly bunjas: Bunja<any>[] = [];
|
|
5
|
+
public readonly id: number;
|
|
6
|
+
public debugLabel: string = "";
|
|
6
7
|
constructor(
|
|
7
|
-
public
|
|
8
|
-
public
|
|
9
|
-
public
|
|
8
|
+
public deps: Dep<any>[], // one depth dependencies
|
|
9
|
+
public parents: Bunja<any>[], // one depth parents
|
|
10
|
+
public relatedBunjas: Bunja<any>[], // toposorted parents without self
|
|
11
|
+
public relatedScopes: Scope<any>[], // deduped
|
|
10
12
|
public init: (...args: any[]) => T & BunjaValue
|
|
11
|
-
) {
|
|
13
|
+
) {
|
|
14
|
+
this.id = Bunja.bunjas.length;
|
|
15
|
+
Bunja.bunjas.push(this);
|
|
16
|
+
}
|
|
12
17
|
static readonly effect = Symbol("Bunja.effect");
|
|
18
|
+
toString() {
|
|
19
|
+
return `[${this.debugLabel} Bunja:${this.id}]`;
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
export class Scope<T> {
|
|
24
|
+
public static readonly scopes: Scope<any>[] = [];
|
|
25
|
+
public readonly id: number;
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = Scope.scopes.length;
|
|
28
|
+
Scope.scopes.push(this);
|
|
29
|
+
}
|
|
30
|
+
toString() {
|
|
31
|
+
return this.id;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ReadScope = <T>(scope: Scope<T>) => T;
|
|
36
|
+
|
|
15
37
|
export class BunjaStore {
|
|
16
38
|
#bunjas: Record<string, BunjaInstance> = {};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
#scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
|
|
40
|
+
get<T>(bunja: Bunja<T>, readScope: ReadScope) {
|
|
41
|
+
const scopeInstanceMap = new Map(
|
|
42
|
+
bunja.relatedScopes.map((scope) => [
|
|
43
|
+
scope,
|
|
44
|
+
this.#getScopeInstance(scope, readScope(scope)),
|
|
45
|
+
])
|
|
46
|
+
);
|
|
47
|
+
const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
|
|
48
|
+
const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
|
|
49
|
+
return {
|
|
50
|
+
value: bunjaInstance.value as T,
|
|
51
|
+
effect() {
|
|
52
|
+
relatedBunjaInstanceMap.forEach((related) => related.add());
|
|
53
|
+
bunjaInstance.add();
|
|
54
|
+
scopeInstanceMap.forEach((scope) => scope.add());
|
|
55
|
+
return () => {
|
|
56
|
+
// concern: reverse order?
|
|
57
|
+
relatedBunjaInstanceMap.forEach((related) => related.sub());
|
|
58
|
+
bunjaInstance.sub();
|
|
59
|
+
scopeInstanceMap.forEach((scope) => scope.sub());
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
#getBunjaInstance(
|
|
65
|
+
bunja: Bunja<any>,
|
|
66
|
+
scopeInstanceMap: Map<Scope<any>, ScopeInstance>
|
|
67
|
+
): BunjaInstance {
|
|
68
|
+
const localScopeInstanceMap = new Map(
|
|
69
|
+
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!])
|
|
70
|
+
);
|
|
71
|
+
const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
|
|
72
|
+
.map(({ instanceId }) => instanceId)
|
|
73
|
+
.sort((a, b) => a - b);
|
|
74
|
+
const bunjaInstanceId = `${bunja.id}:${scopeInstanceIds.join(",")}`;
|
|
75
|
+
if (this.#bunjas[bunjaInstanceId]) return this.#bunjas[bunjaInstanceId];
|
|
76
|
+
const relatedBunjaInstanceMap = new Map(
|
|
77
|
+
bunja.relatedBunjas.map((relatedBunja) => [
|
|
78
|
+
relatedBunja,
|
|
79
|
+
this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
|
|
80
|
+
])
|
|
81
|
+
);
|
|
82
|
+
const args = bunja.deps.map((dep) => {
|
|
83
|
+
if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
|
|
84
|
+
if (dep instanceof Scope) return localScopeInstanceMap.get(dep)!.value;
|
|
85
|
+
throw new Error("Invalid dependency");
|
|
86
|
+
});
|
|
87
|
+
const bunjaInstance = new BunjaInstance(
|
|
88
|
+
() => delete this.#bunjas[bunjaInstanceId],
|
|
89
|
+
bunjaInstanceId,
|
|
90
|
+
relatedBunjaInstanceMap,
|
|
91
|
+
bunja.init.apply(bunja, args)
|
|
92
|
+
);
|
|
93
|
+
this.#bunjas[bunjaInstanceId] = bunjaInstance;
|
|
94
|
+
return bunjaInstance;
|
|
23
95
|
}
|
|
24
|
-
|
|
25
|
-
|
|
96
|
+
#getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
|
|
97
|
+
const scopeInstanceMap =
|
|
98
|
+
this.#scopes.get(scope) ?? this.#scopes.set(scope, new Map()).get(scope)!;
|
|
99
|
+
const init = () =>
|
|
100
|
+
new ScopeInstance(
|
|
101
|
+
() => scopeInstanceMap.delete(value),
|
|
102
|
+
ScopeInstance.counter++,
|
|
103
|
+
scope,
|
|
104
|
+
value
|
|
105
|
+
);
|
|
106
|
+
return (
|
|
107
|
+
scopeInstanceMap.get(value) ??
|
|
108
|
+
scopeInstanceMap.set(value, init()).get(value)!
|
|
109
|
+
);
|
|
26
110
|
}
|
|
27
111
|
}
|
|
28
112
|
|
|
29
113
|
export const createBunjaStore = () => new BunjaStore();
|
|
30
|
-
export const BunjaStoreContext = React.createContext(createBunjaStore());
|
|
31
114
|
|
|
32
115
|
export type BunjaEffectFn = () => () => void;
|
|
33
116
|
export interface BunjaValue {
|
|
@@ -47,58 +130,35 @@ export function bunja<T, U, V, W>(
|
|
|
47
130
|
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
48
131
|
init: (u: U, v: V, w: W) => T & BunjaValue
|
|
49
132
|
): Bunja<T>;
|
|
133
|
+
export function bunja<T, U, V, W, X>(
|
|
134
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
|
|
135
|
+
init: (u: U, v: V, w: W, x: X) => T & BunjaValue
|
|
136
|
+
): Bunja<T>;
|
|
137
|
+
export function bunja<T, U, V, W, X, Y>(
|
|
138
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
|
|
139
|
+
init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue
|
|
140
|
+
): Bunja<T>;
|
|
141
|
+
export function bunja<T, U, V, W, X, Y, Z>(
|
|
142
|
+
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
|
|
143
|
+
init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue
|
|
144
|
+
): Bunja<T>;
|
|
50
145
|
export function bunja<T, const U extends any[]>(
|
|
51
146
|
deps: { [K in keyof U]: Dep<U[K]> },
|
|
52
147
|
init: (...args: U) => T & BunjaValue
|
|
53
148
|
): Bunja<T> {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
new Set([...contexts, ...bunjas.flatMap((def) => def.contexts)])
|
|
149
|
+
const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
|
|
150
|
+
const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
|
|
151
|
+
const relatedBunjas = toposort(parents);
|
|
152
|
+
const relatedScopes = Array.from(
|
|
153
|
+
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)])
|
|
60
154
|
);
|
|
61
|
-
return new Bunja(
|
|
155
|
+
return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
|
|
62
156
|
}
|
|
63
|
-
bunja.
|
|
64
|
-
|
|
65
|
-
export function useBunja<T>(bunja: Bunja<T>): T {
|
|
66
|
-
const { id, deps, contexts } = bunja;
|
|
67
|
-
const store = React.useContext(BunjaStoreContext);
|
|
68
|
-
const tuples = contexts.map((c) => [c, React.useContext(c)] as const);
|
|
69
|
-
const scopes = tuples.map(([context, value]) => getScope(context, value));
|
|
70
|
-
const scopeMap = new Map(tuples);
|
|
71
|
-
const args = deps.map((dep) => {
|
|
72
|
-
if (dep instanceof Bunja) return useBunja(dep);
|
|
73
|
-
return scopeMap.get(dep);
|
|
74
|
-
});
|
|
75
|
-
const biid = `${id}:${scopes
|
|
76
|
-
.map(({ id }) => id)
|
|
77
|
-
.sort()
|
|
78
|
-
.join(",")}`;
|
|
79
|
-
const instance = store.get(bunja, biid, args);
|
|
80
|
-
React.useEffect(() => {
|
|
81
|
-
instance.add();
|
|
82
|
-
return () => instance.sub();
|
|
83
|
-
}, [instance]);
|
|
84
|
-
React.useEffect(() => {
|
|
85
|
-
scopes.forEach((scope) => scope.add());
|
|
86
|
-
return () => scopes.forEach((scope) => scope.sub());
|
|
87
|
-
}, scopes);
|
|
88
|
-
return instance.value as T;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const useRid = () => React.useState(() => useRid.counter++)[0];
|
|
92
|
-
useRid.counter = 0;
|
|
157
|
+
bunja.effect = Bunja.effect;
|
|
93
158
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const m = scopes.get(context) ?? scopes.set(context, new Map()).get(context)!;
|
|
97
|
-
const init = () =>
|
|
98
|
-
new Scope(() => m.delete(value), context, value, getScope.counter++);
|
|
99
|
-
return m.get(value) ?? m.set(value, init()).get(value)!;
|
|
159
|
+
export function createScope<T>(): Scope<T> {
|
|
160
|
+
return new Scope();
|
|
100
161
|
}
|
|
101
|
-
getScope.counter = 0;
|
|
102
162
|
|
|
103
163
|
abstract class RefCounter {
|
|
104
164
|
#disposed = false;
|
|
@@ -116,36 +176,58 @@ abstract class RefCounter {
|
|
|
116
176
|
}
|
|
117
177
|
});
|
|
118
178
|
}
|
|
119
|
-
abstract dispose
|
|
179
|
+
abstract dispose(): void;
|
|
120
180
|
}
|
|
121
181
|
|
|
122
182
|
const noop = () => {};
|
|
123
183
|
class BunjaInstance extends RefCounter {
|
|
124
184
|
#cleanup: (() => void) | undefined;
|
|
185
|
+
#dispose: () => void;
|
|
125
186
|
constructor(
|
|
126
|
-
|
|
127
|
-
public
|
|
187
|
+
dispose: () => void,
|
|
188
|
+
public instanceId: string,
|
|
189
|
+
public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
|
|
128
190
|
public value: BunjaValue
|
|
129
191
|
) {
|
|
130
192
|
super();
|
|
193
|
+
this.#dispose = () => {
|
|
194
|
+
this.#cleanup?.();
|
|
195
|
+
dispose();
|
|
196
|
+
};
|
|
131
197
|
}
|
|
132
198
|
add() {
|
|
133
199
|
this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
|
|
134
200
|
super.add();
|
|
135
201
|
}
|
|
136
|
-
dispose
|
|
137
|
-
this.#
|
|
138
|
-
|
|
139
|
-
};
|
|
202
|
+
dispose() {
|
|
203
|
+
this.#dispose();
|
|
204
|
+
}
|
|
140
205
|
}
|
|
141
206
|
|
|
142
|
-
class
|
|
207
|
+
class ScopeInstance extends RefCounter {
|
|
208
|
+
public static counter = 0;
|
|
143
209
|
constructor(
|
|
144
210
|
public dispose: () => void,
|
|
145
|
-
public
|
|
146
|
-
public
|
|
147
|
-
public
|
|
211
|
+
public instanceId: number,
|
|
212
|
+
public scope: Scope<any>,
|
|
213
|
+
public value: any
|
|
148
214
|
) {
|
|
149
215
|
super();
|
|
150
216
|
}
|
|
151
217
|
}
|
|
218
|
+
|
|
219
|
+
interface Toposortable {
|
|
220
|
+
parents: Toposortable[];
|
|
221
|
+
}
|
|
222
|
+
function toposort<T extends Toposortable>(nodes: T[]): T[] {
|
|
223
|
+
const visited = new Set<T>();
|
|
224
|
+
const result: T[] = [];
|
|
225
|
+
function visit(current: T) {
|
|
226
|
+
if (visited.has(current)) return;
|
|
227
|
+
visited.add(current);
|
|
228
|
+
for (const parent of current.parents) visit(parent as T);
|
|
229
|
+
result.push(current);
|
|
230
|
+
}
|
|
231
|
+
for (const node of nodes) visit(node);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunja",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "State Lifetime Manager
|
|
3
|
+
"version": "0.0.8",
|
|
4
|
+
"description": "State Lifetime Manager",
|
|
5
5
|
"main": "bunja.ts",
|
|
6
6
|
"scripts": {},
|
|
7
7
|
"keywords": [
|
|
8
8
|
"bunja",
|
|
9
|
-
"react",
|
|
10
9
|
"di"
|
|
11
10
|
],
|
|
12
11
|
"author": "JongChan Choi <jong@chan.moe>",
|
|
@@ -22,6 +21,9 @@
|
|
|
22
21
|
"peerDependenciesMeta": {
|
|
23
22
|
"@types/react": {
|
|
24
23
|
"optional": true
|
|
24
|
+
},
|
|
25
|
+
"react": {
|
|
26
|
+
"optional": true
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
}
|
package/react.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Context, createContext, useContext, useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Bunja,
|
|
4
|
+
createBunjaStore,
|
|
5
|
+
createScope,
|
|
6
|
+
ReadScope,
|
|
7
|
+
Scope,
|
|
8
|
+
} from "./bunja";
|
|
9
|
+
|
|
10
|
+
export const BunjaStoreContext = createContext(createBunjaStore());
|
|
11
|
+
|
|
12
|
+
export const scopeContextMap = new Map<Scope<any>, Context<any>>();
|
|
13
|
+
export function bindScope(scope: Scope<any>, context: Context<any>) {
|
|
14
|
+
scopeContextMap.set(scope, context);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createScopeFromContext<T>(context: Context<T>): Scope<T> {
|
|
18
|
+
const scope = createScope();
|
|
19
|
+
bindScope(scope, context);
|
|
20
|
+
return scope;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultReadScope: ReadScope = (scope) => {
|
|
24
|
+
const context = scopeContextMap.get(scope)!;
|
|
25
|
+
console.log({ scopeContextMap, scope, context });
|
|
26
|
+
return useContext(context);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function useBunja<T>(bunja: Bunja<T>, readScope = defaultReadScope): T {
|
|
30
|
+
const store = useContext(BunjaStoreContext);
|
|
31
|
+
const { value, effect } = store.get(bunja, readScope);
|
|
32
|
+
useEffect(effect, []);
|
|
33
|
+
return value;
|
|
34
|
+
}
|