bunja 1.0.0 → 2.0.0-alpha.2
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 +53 -40
- package/bunja.ts +300 -182
- package/codemod/bunja-v1-to-v2.js +118 -0
- package/compat/bunja-v1.ts +48 -0
- package/deno.json +1 -1
- package/deno.lock +174 -83
- package/dist/bunja-bUA1rGXy.cjs +305 -0
- package/dist/bunja-wcx846sL.js +274 -0
- package/dist/bunja.cjs +1 -1
- package/dist/bunja.d.cts +51 -42
- package/dist/bunja.d.ts +51 -42
- package/dist/bunja.js +1 -1
- package/dist/react.cjs +14 -5
- package/dist/react.d.cts +5 -4
- package/dist/react.d.ts +5 -4
- package/dist/react.js +14 -6
- package/package.json +4 -4
- package/react.ts +32 -14
- package/test.ts +53 -32
- package/dist/bunja-Q0ZusYIM.cjs +0 -189
- package/dist/bunja-fHIhQAuL.js +0 -158
package/bunja.ts
CHANGED
|
@@ -1,235 +1,353 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface BunjaFn {
|
|
2
|
+
<T>(init: () => T): Bunja<T>;
|
|
3
|
+
use: BunjaUseFn;
|
|
4
|
+
effect: BunjaEffectFn;
|
|
5
|
+
}
|
|
6
|
+
export const bunja: BunjaFn = bunjaFn;
|
|
7
|
+
function bunjaFn<T>(init: () => T): Bunja<T> {
|
|
8
|
+
return new Bunja(init);
|
|
9
|
+
}
|
|
10
|
+
bunjaFn.use = invalidUse as BunjaUseFn;
|
|
11
|
+
bunjaFn.effect = invalidEffect as BunjaEffectFn;
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
type
|
|
13
|
+
export type BunjaUseFn = <T>(dep: Dep<T>) => T;
|
|
14
|
+
export type BunjaEffectFn = (callback: BunjaEffectCallback) => void;
|
|
15
|
+
export type BunjaEffectCallback = () => (() => void) | void;
|
|
5
16
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
public readonly id: number;
|
|
9
|
-
public debugLabel: string = "";
|
|
10
|
-
constructor(
|
|
11
|
-
public deps: Dep<any>[], // one depth dependencies
|
|
12
|
-
public parents: Bunja<any>[], // one depth parents
|
|
13
|
-
public relatedBunjas: Bunja<any>[], // toposorted parents without self
|
|
14
|
-
public relatedScopes: Scope<any>[], // deduped
|
|
15
|
-
public init: (...args: any[]) => T & BunjaValue,
|
|
16
|
-
) {
|
|
17
|
-
this.id = Bunja.bunjas.length;
|
|
18
|
-
Bunja.bunjas.push(this);
|
|
19
|
-
}
|
|
20
|
-
static readonly effect: BunjaEffectSymbol = bunjaEffectSymbol;
|
|
21
|
-
toString(): string {
|
|
22
|
-
const { id, debugLabel } = this;
|
|
23
|
-
return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
24
|
-
}
|
|
17
|
+
export function createScope<T>(hash?: HashFn<T>): Scope<T> {
|
|
18
|
+
return new Scope(hash);
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
export
|
|
21
|
+
export function createBunjaStore(): BunjaStore {
|
|
22
|
+
return new BunjaStore();
|
|
23
|
+
}
|
|
28
24
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
25
|
+
export type Dep<T> = Bunja<T> | Scope<T>;
|
|
26
|
+
|
|
27
|
+
function invalidUse() {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"`bunja.use` can only be used inside a bunja init function.",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function invalidEffect() {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"`bunja.effect` can only be used inside a bunja init function.",
|
|
35
|
+
);
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
interface BunjaStoreGetContext {
|
|
39
|
+
bunjaInstance: BunjaInstance;
|
|
40
|
+
bunjaInstanceMap: BunjaInstanceMap;
|
|
41
|
+
scopeInstanceMap: ScopeInstanceMap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type BunjaInstanceMap = Map<Bunja<unknown>, BunjaInstance>;
|
|
45
|
+
type ScopeInstanceMap = Map<Scope<unknown>, ScopeInstance>;
|
|
46
|
+
|
|
47
|
+
interface BunjaBakingContext {
|
|
48
|
+
currentBunja: Bunja<unknown>;
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
export class BunjaStore {
|
|
46
52
|
#bunjas: Record<string, BunjaInstance> = {};
|
|
47
|
-
#scopes: Map<Scope<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
#scopes: Map<Scope<unknown>, Map<unknown, ScopeInstance>> = new Map();
|
|
54
|
+
#bakingContext: BunjaBakingContext | undefined;
|
|
55
|
+
dispose(): void {
|
|
56
|
+
for (const instance of Object.values(this.#bunjas)) instance.dispose();
|
|
57
|
+
for (const instanceMap of Object.values(this.#scopes)) {
|
|
58
|
+
for (const instance of instanceMap.values()) instance.dispose();
|
|
59
|
+
}
|
|
60
|
+
this.#bunjas = {};
|
|
61
|
+
this.#scopes = new Map();
|
|
62
|
+
}
|
|
63
|
+
get<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetResult<T> {
|
|
64
|
+
const originalUse = bunjaFn.use;
|
|
65
|
+
try {
|
|
66
|
+
const { bunjaInstance, bunjaInstanceMap, scopeInstanceMap } = bunja.baked
|
|
67
|
+
? this.#getBaked(bunja, readScope)
|
|
68
|
+
: this.#getUnbaked(bunja, readScope);
|
|
69
|
+
return {
|
|
70
|
+
value: bunjaInstance.value as T,
|
|
71
|
+
mount: () => {
|
|
72
|
+
bunjaInstanceMap.forEach((instance) => instance.add());
|
|
73
|
+
bunjaInstance.add();
|
|
74
|
+
scopeInstanceMap.forEach((instance) => instance.add());
|
|
75
|
+
const unmount = () => {
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
bunjaInstanceMap.forEach((instance) => instance.sub());
|
|
78
|
+
bunjaInstance.sub();
|
|
79
|
+
scopeInstanceMap.forEach((instance) => instance.sub());
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
return unmount;
|
|
83
|
+
},
|
|
84
|
+
deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
|
|
85
|
+
};
|
|
86
|
+
} finally {
|
|
87
|
+
bunjaFn.use = originalUse;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
#getBaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
|
|
56
91
|
const scopeInstanceMap = new Map(
|
|
57
92
|
bunja.relatedScopes.map((scope) => [
|
|
58
93
|
scope,
|
|
59
94
|
this.#getScopeInstance(scope, readScope(scope)),
|
|
60
95
|
]),
|
|
61
96
|
);
|
|
62
|
-
const
|
|
63
|
-
const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
|
|
64
|
-
return {
|
|
65
|
-
value: bunjaInstance.value as T,
|
|
66
|
-
mount() {
|
|
67
|
-
relatedBunjaInstanceMap.forEach((related) => related.add());
|
|
68
|
-
bunjaInstance.add();
|
|
69
|
-
scopeInstanceMap.forEach((scope) => scope.add());
|
|
70
|
-
return function unmount(): void {
|
|
71
|
-
// concern: reverse order?
|
|
72
|
-
relatedBunjaInstanceMap.forEach((related) => related.sub());
|
|
73
|
-
bunjaInstance.sub();
|
|
74
|
-
scopeInstanceMap.forEach((scope) => scope.sub());
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
#getBunjaInstance(
|
|
81
|
-
bunja: Bunja<any>,
|
|
82
|
-
scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
|
|
83
|
-
): BunjaInstance {
|
|
84
|
-
const localScopeInstanceMap = new Map(
|
|
85
|
-
bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
|
|
86
|
-
);
|
|
87
|
-
const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
|
|
88
|
-
.map(({ instanceId }) => instanceId)
|
|
89
|
-
.sort((a, b) => a - b);
|
|
90
|
-
const bunjaInstanceId = `${bunja.id}:${scopeInstanceIds.join(",")}`;
|
|
91
|
-
if (this.#bunjas[bunjaInstanceId]) return this.#bunjas[bunjaInstanceId];
|
|
92
|
-
const relatedBunjaInstanceMap = new Map(
|
|
97
|
+
const bunjaInstanceMap = new Map(
|
|
93
98
|
bunja.relatedBunjas.map((relatedBunja) => [
|
|
94
99
|
relatedBunja,
|
|
95
100
|
this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
|
|
96
101
|
]),
|
|
97
102
|
);
|
|
98
|
-
|
|
99
|
-
if (dep instanceof Bunja)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
bunjaFn.use = <T>(dep: Dep<T>) => {
|
|
104
|
+
if (dep instanceof Bunja) {
|
|
105
|
+
return bunjaInstanceMap.get(dep as Bunja<unknown>)!.value as T;
|
|
106
|
+
}
|
|
107
|
+
if (dep instanceof Scope) {
|
|
108
|
+
return scopeInstanceMap.get(dep as Scope<unknown>)!.value as T;
|
|
109
|
+
}
|
|
110
|
+
throw new Error("`bunja.use` can only be used with Bunja or Scope.");
|
|
111
|
+
};
|
|
112
|
+
const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
|
|
113
|
+
return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
|
|
114
|
+
}
|
|
115
|
+
#getUnbaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
|
|
116
|
+
const bunjaInstanceMap: BunjaInstanceMap = new Map();
|
|
117
|
+
const scopeInstanceMap: ScopeInstanceMap = new Map();
|
|
118
|
+
function getUse<D extends Dep<unknown>, I extends { value: unknown }>(
|
|
119
|
+
map: Map<D, I>,
|
|
120
|
+
addDep: (D: D) => void,
|
|
121
|
+
getInstance: (dep: D) => I,
|
|
122
|
+
) {
|
|
123
|
+
return ((dep) => {
|
|
124
|
+
const d = dep as D;
|
|
125
|
+
addDep(d);
|
|
126
|
+
if (map.has(d)) return map.get(d)!.value as T;
|
|
127
|
+
const instance = getInstance(d);
|
|
128
|
+
map.set(d, instance);
|
|
129
|
+
return instance.value as T;
|
|
130
|
+
}) as <T>(dep: Dep<T>) => T;
|
|
131
|
+
}
|
|
132
|
+
const useScope = getUse(
|
|
133
|
+
scopeInstanceMap,
|
|
134
|
+
(dep) => this.#bakingContext!.currentBunja.addScope(dep),
|
|
135
|
+
(dep) => this.#getScopeInstance(dep, readScope(dep)),
|
|
136
|
+
);
|
|
137
|
+
const useBunja = getUse(
|
|
138
|
+
bunjaInstanceMap,
|
|
139
|
+
(dep) => this.#bakingContext!.currentBunja.addParent(dep),
|
|
140
|
+
(dep) => {
|
|
141
|
+
if (dep.baked) {
|
|
142
|
+
for (const scope of dep.relatedScopes) useScope(scope);
|
|
143
|
+
}
|
|
144
|
+
return this.#getBunjaInstance(dep, scopeInstanceMap);
|
|
145
|
+
},
|
|
108
146
|
);
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
bunjaFn.use = <T>(dep: Dep<T>) => {
|
|
148
|
+
if (dep instanceof Bunja) return useBunja(dep) as T;
|
|
149
|
+
if (dep instanceof Scope) return useScope(dep) as T;
|
|
150
|
+
throw new Error("`bunja.use` can only be used with Bunja or Scope.");
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
this.#bakingContext = { currentBunja: bunja };
|
|
154
|
+
const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
|
|
155
|
+
return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
|
|
156
|
+
} finally {
|
|
157
|
+
this.#bakingContext = undefined;
|
|
158
|
+
}
|
|
111
159
|
}
|
|
112
|
-
#
|
|
160
|
+
#getBunjaInstance<T>(
|
|
161
|
+
bunja: Bunja<T>,
|
|
162
|
+
scopeInstanceMap: ScopeInstanceMap,
|
|
163
|
+
): BunjaInstance {
|
|
164
|
+
const originalEffect = bunjaFn.effect;
|
|
165
|
+
const prevBunja = this.#bakingContext?.currentBunja;
|
|
166
|
+
try {
|
|
167
|
+
const effects: BunjaEffectCallback[] = [];
|
|
168
|
+
bunjaFn.effect = (callback: BunjaEffectCallback) => {
|
|
169
|
+
effects.push(callback);
|
|
170
|
+
};
|
|
171
|
+
if (this.#bakingContext) this.#bakingContext.currentBunja = bunja;
|
|
172
|
+
if (bunja.baked) {
|
|
173
|
+
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
174
|
+
if (id in this.#bunjas) return this.#bunjas[id];
|
|
175
|
+
const bunjaInstanceValue = bunja.init();
|
|
176
|
+
return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
|
|
177
|
+
} else {
|
|
178
|
+
const bunjaInstanceValue = bunja.init();
|
|
179
|
+
bunja.bake();
|
|
180
|
+
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
181
|
+
return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
bunjaFn.effect = originalEffect;
|
|
185
|
+
if (this.#bakingContext) this.#bakingContext.currentBunja = prevBunja!;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
#getScopeInstance(scope: Scope<unknown>, value: unknown): ScopeInstance {
|
|
113
189
|
const key = scope.hash(value);
|
|
114
|
-
const
|
|
190
|
+
const instanceMap = this.#scopes.get(scope) ??
|
|
115
191
|
this.#scopes.set(scope, new Map()).get(scope)!;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ScopeInstance.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
192
|
+
return instanceMap.get(key) ??
|
|
193
|
+
instanceMap.set(
|
|
194
|
+
key,
|
|
195
|
+
new ScopeInstance(value, () => instanceMap.delete(key)),
|
|
196
|
+
).get(key)!;
|
|
197
|
+
}
|
|
198
|
+
#createBunjaInstance(
|
|
199
|
+
id: string,
|
|
200
|
+
value: unknown,
|
|
201
|
+
effects: BunjaEffectCallback[],
|
|
202
|
+
): BunjaInstance {
|
|
203
|
+
const effect = () => {
|
|
204
|
+
const cleanups = effects
|
|
205
|
+
.map((effect) => effect())
|
|
206
|
+
.filter(Boolean) as (() => void)[];
|
|
207
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
208
|
+
};
|
|
209
|
+
const dispose = () => delete this.#bunjas[id];
|
|
210
|
+
const bunjaInstance = new BunjaInstance(id, value, effect, dispose);
|
|
211
|
+
this.#bunjas[id] = bunjaInstance;
|
|
212
|
+
return bunjaInstance;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type ReadScope = <T>(scope: Scope<T>) => T;
|
|
217
|
+
|
|
218
|
+
export interface BunjaStoreGetResult<T> {
|
|
219
|
+
value: T;
|
|
220
|
+
mount: () => () => void;
|
|
221
|
+
deps: unknown[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export class Bunja<T> {
|
|
225
|
+
private static counter: number = 0;
|
|
226
|
+
readonly id: string = String(Bunja.counter++);
|
|
227
|
+
debugLabel: string = "";
|
|
228
|
+
#phase: BunjaPhase = { baked: false, parents: new Set(), scopes: new Set() };
|
|
229
|
+
constructor(public init: () => T) {}
|
|
230
|
+
get baked(): boolean {
|
|
231
|
+
return this.#phase.baked;
|
|
232
|
+
}
|
|
233
|
+
get parents(): Bunja<unknown>[] {
|
|
234
|
+
if (this.#phase.baked) return this.#phase.parents;
|
|
235
|
+
return Array.from(this.#phase.parents);
|
|
236
|
+
}
|
|
237
|
+
get relatedBunjas(): Bunja<unknown>[] {
|
|
238
|
+
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
239
|
+
return this.#phase.relatedBunjas;
|
|
240
|
+
}
|
|
241
|
+
get relatedScopes(): Scope<unknown>[] {
|
|
242
|
+
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
243
|
+
return this.#phase.relatedScopes;
|
|
244
|
+
}
|
|
245
|
+
addParent(bunja: Bunja<unknown>): void {
|
|
246
|
+
if (this.#phase.baked) return;
|
|
247
|
+
this.#phase.parents.add(bunja);
|
|
248
|
+
}
|
|
249
|
+
addScope(scope: Scope<unknown>): void {
|
|
250
|
+
if (this.#phase.baked) return;
|
|
251
|
+
this.#phase.scopes.add(scope);
|
|
252
|
+
}
|
|
253
|
+
bake(): void {
|
|
254
|
+
if (this.#phase.baked) throw new Error("Bunja is already baked.");
|
|
255
|
+
const scopes = this.#phase.scopes;
|
|
256
|
+
const parents = this.parents;
|
|
257
|
+
const relatedBunjas = toposort(parents);
|
|
258
|
+
const relatedScopes = Array.from(
|
|
259
|
+
new Set([
|
|
260
|
+
...relatedBunjas.flatMap((bunja) => bunja.relatedScopes),
|
|
261
|
+
...scopes,
|
|
262
|
+
]),
|
|
263
|
+
);
|
|
264
|
+
this.#phase = { baked: true, parents, relatedBunjas, relatedScopes };
|
|
265
|
+
}
|
|
266
|
+
calcInstanceId(scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>): string {
|
|
267
|
+
const scopeInstanceIds = this.relatedScopes.map(
|
|
268
|
+
(scope) => scopeInstanceMap.get(scope)!.id,
|
|
126
269
|
);
|
|
270
|
+
return `${this.id}:${scopeInstanceIds.join(",")}`;
|
|
271
|
+
}
|
|
272
|
+
toString(): string {
|
|
273
|
+
const { id, debugLabel } = this;
|
|
274
|
+
return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
127
275
|
}
|
|
128
276
|
}
|
|
129
277
|
|
|
130
|
-
|
|
278
|
+
type BunjaPhase = BunjaPhaseUnbaked | BunjaPhaseBaked;
|
|
131
279
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
280
|
+
interface BunjaPhaseUnbaked {
|
|
281
|
+
readonly baked: false;
|
|
282
|
+
readonly parents: Set<Bunja<unknown>>;
|
|
283
|
+
readonly scopes: Set<Scope<unknown>>;
|
|
135
284
|
}
|
|
136
285
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
|
|
143
|
-
const relatedBunjas = toposort(parents);
|
|
144
|
-
const relatedScopes = Array.from(
|
|
145
|
-
new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
|
|
146
|
-
);
|
|
147
|
-
return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
|
|
148
|
-
}
|
|
149
|
-
bunjaImpl.effect = Bunja.effect;
|
|
150
|
-
|
|
151
|
-
export const bunja: {
|
|
152
|
-
<T>(deps: [], init: () => T & BunjaValue): Bunja<T>;
|
|
153
|
-
<T, U>(deps: [Dep<U>], init: (u: U) => T & BunjaValue): Bunja<T>;
|
|
154
|
-
<T, U, V>(
|
|
155
|
-
deps: [Dep<U>, Dep<V>],
|
|
156
|
-
init: (u: U, v: V) => T & BunjaValue,
|
|
157
|
-
): Bunja<T>;
|
|
158
|
-
<T, U, V, W>(
|
|
159
|
-
deps: [Dep<U>, Dep<V>, Dep<W>],
|
|
160
|
-
init: (u: U, v: V, w: W) => T & BunjaValue,
|
|
161
|
-
): Bunja<T>;
|
|
162
|
-
<T, U, V, W, X>(
|
|
163
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
|
|
164
|
-
init: (u: U, v: V, w: W, x: X) => T & BunjaValue,
|
|
165
|
-
): Bunja<T>;
|
|
166
|
-
<T, U, V, W, X, Y>(
|
|
167
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
|
|
168
|
-
init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue,
|
|
169
|
-
): Bunja<T>;
|
|
170
|
-
<T, U, V, W, X, Y, Z>(
|
|
171
|
-
deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
|
|
172
|
-
init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue,
|
|
173
|
-
): Bunja<T>;
|
|
174
|
-
readonly effect: BunjaEffectSymbol;
|
|
175
|
-
} = bunjaImpl;
|
|
176
|
-
|
|
177
|
-
export function createScope<T>(hash?: HashFn): Scope<T> {
|
|
178
|
-
return new Scope(hash);
|
|
286
|
+
interface BunjaPhaseBaked {
|
|
287
|
+
readonly baked: true;
|
|
288
|
+
readonly parents: Bunja<unknown>[];
|
|
289
|
+
readonly relatedBunjas: Bunja<unknown>[];
|
|
290
|
+
readonly relatedScopes: Scope<unknown>[];
|
|
179
291
|
}
|
|
180
292
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.#disposed = true;
|
|
193
|
-
this.dispose();
|
|
194
|
-
}
|
|
195
|
-
});
|
|
293
|
+
export class Scope<T> {
|
|
294
|
+
private static counter: number = 0;
|
|
295
|
+
readonly id: string = String(Scope.counter++);
|
|
296
|
+
debugLabel: string = "";
|
|
297
|
+
constructor(public readonly hash: HashFn<T> = Scope.identity) {}
|
|
298
|
+
private static identity<T>(x: T): T {
|
|
299
|
+
return x;
|
|
300
|
+
}
|
|
301
|
+
toString(): string {
|
|
302
|
+
const { id, debugLabel } = this;
|
|
303
|
+
return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
196
304
|
}
|
|
197
|
-
abstract dispose(): void;
|
|
198
305
|
}
|
|
199
306
|
|
|
200
|
-
|
|
307
|
+
export type HashFn<T> = (value: T) => unknown;
|
|
308
|
+
|
|
201
309
|
const noop = () => {};
|
|
310
|
+
abstract class RefCounter {
|
|
311
|
+
#count: number = 0;
|
|
312
|
+
abstract dispose(): void;
|
|
313
|
+
add(): void {
|
|
314
|
+
++this.#count;
|
|
315
|
+
}
|
|
316
|
+
sub(): void {
|
|
317
|
+
--this.#count;
|
|
318
|
+
if (this.#count < 1) {
|
|
319
|
+
this.dispose();
|
|
320
|
+
this.dispose = noop;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
202
325
|
class BunjaInstance extends RefCounter {
|
|
203
326
|
#cleanup: (() => void) | undefined;
|
|
204
|
-
#dispose: () => void;
|
|
205
327
|
constructor(
|
|
206
|
-
|
|
207
|
-
public
|
|
208
|
-
public
|
|
209
|
-
|
|
328
|
+
public readonly id: string,
|
|
329
|
+
public readonly value: unknown,
|
|
330
|
+
public readonly effect: BunjaEffectCallback,
|
|
331
|
+
private readonly _dispose: () => void,
|
|
210
332
|
) {
|
|
211
333
|
super();
|
|
212
|
-
this.#dispose = () => {
|
|
213
|
-
this.#cleanup?.();
|
|
214
|
-
dispose();
|
|
215
|
-
};
|
|
216
334
|
}
|
|
217
|
-
override
|
|
218
|
-
this.#cleanup
|
|
219
|
-
|
|
335
|
+
override dispose(): void {
|
|
336
|
+
this.#cleanup?.();
|
|
337
|
+
this._dispose();
|
|
220
338
|
}
|
|
221
|
-
|
|
222
|
-
this.#
|
|
339
|
+
override add(): void {
|
|
340
|
+
this.#cleanup ??= this.effect() ?? noop;
|
|
341
|
+
super.add();
|
|
223
342
|
}
|
|
224
343
|
}
|
|
225
344
|
|
|
226
345
|
class ScopeInstance extends RefCounter {
|
|
227
|
-
|
|
346
|
+
private static counter: number = 0;
|
|
347
|
+
readonly id: string = String(ScopeInstance.counter++);
|
|
228
348
|
constructor(
|
|
229
|
-
public
|
|
230
|
-
public
|
|
231
|
-
public scope: Scope<any>,
|
|
232
|
-
public value: any,
|
|
349
|
+
public readonly value: unknown,
|
|
350
|
+
public readonly dispose: () => void,
|
|
233
351
|
) {
|
|
234
352
|
super();
|
|
235
353
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module.exports = function (fileInfo, api) {
|
|
2
|
+
const j = api.jscodeshift;
|
|
3
|
+
const root = j(fileInfo.source);
|
|
4
|
+
let modified = false;
|
|
5
|
+
|
|
6
|
+
root.find(j.CallExpression, { callee: { name: "bunja" } }).forEach((path) => {
|
|
7
|
+
const { node } = path;
|
|
8
|
+
const args = node.arguments;
|
|
9
|
+
if (args.length !== 2) return;
|
|
10
|
+
|
|
11
|
+
const [depsArray, initFn] = args;
|
|
12
|
+
if (depsArray.type !== "ArrayExpression") return;
|
|
13
|
+
if (
|
|
14
|
+
initFn.type !== "ArrowFunctionExpression" &&
|
|
15
|
+
initFn.type !== "FunctionExpression"
|
|
16
|
+
) return;
|
|
17
|
+
const params = initFn.params;
|
|
18
|
+
|
|
19
|
+
const bodyStatements = initFn.body.type === "BlockStatement"
|
|
20
|
+
? [...initFn.body.body]
|
|
21
|
+
: [{ type: "ReturnStatement", argument: initFn.body }];
|
|
22
|
+
|
|
23
|
+
const useStatements = depsArray.elements.map((dep, index) => {
|
|
24
|
+
if (index < params.length) {
|
|
25
|
+
return {
|
|
26
|
+
type: "VariableDeclaration",
|
|
27
|
+
kind: "const",
|
|
28
|
+
declarations: [{
|
|
29
|
+
type: "VariableDeclarator",
|
|
30
|
+
id: params[index],
|
|
31
|
+
init: {
|
|
32
|
+
type: "CallExpression",
|
|
33
|
+
callee: {
|
|
34
|
+
type: "MemberExpression",
|
|
35
|
+
object: { type: "Identifier", name: "bunja" },
|
|
36
|
+
property: { type: "Identifier", name: "use" },
|
|
37
|
+
computed: false,
|
|
38
|
+
},
|
|
39
|
+
arguments: [dep],
|
|
40
|
+
},
|
|
41
|
+
}],
|
|
42
|
+
};
|
|
43
|
+
} else {
|
|
44
|
+
return {
|
|
45
|
+
type: "ExpressionStatement",
|
|
46
|
+
expression: {
|
|
47
|
+
type: "CallExpression",
|
|
48
|
+
callee: {
|
|
49
|
+
type: "MemberExpression",
|
|
50
|
+
object: { type: "Identifier", name: "bunja" },
|
|
51
|
+
property: { type: "Identifier", name: "use" },
|
|
52
|
+
computed: false,
|
|
53
|
+
},
|
|
54
|
+
arguments: [dep],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < bodyStatements.length; ++i) {
|
|
61
|
+
const statement = bodyStatements[i];
|
|
62
|
+
if (!statement || statement.type !== "ReturnStatement") continue;
|
|
63
|
+
if (statement.argument?.type !== "ObjectExpression") continue;
|
|
64
|
+
const returnObj = statement.argument;
|
|
65
|
+
const props = returnObj.properties;
|
|
66
|
+
|
|
67
|
+
const effectPropIndex = props.findIndex((prop) =>
|
|
68
|
+
prop?.computed &&
|
|
69
|
+
prop.key?.type === "MemberExpression" &&
|
|
70
|
+
prop.key?.object?.name === "bunja" &&
|
|
71
|
+
prop.key?.property?.name === "effect"
|
|
72
|
+
);
|
|
73
|
+
if (effectPropIndex === -1) continue;
|
|
74
|
+
|
|
75
|
+
const prop = props[effectPropIndex];
|
|
76
|
+
let effectFn = prop.value;
|
|
77
|
+
|
|
78
|
+
if (effectFn.type === "FunctionExpression") {
|
|
79
|
+
effectFn = {
|
|
80
|
+
type: "ArrowFunctionExpression",
|
|
81
|
+
params: effectFn.params,
|
|
82
|
+
body: effectFn.body,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const effectStatement = {
|
|
87
|
+
type: "ExpressionStatement",
|
|
88
|
+
expression: {
|
|
89
|
+
type: "CallExpression",
|
|
90
|
+
callee: {
|
|
91
|
+
type: "MemberExpression",
|
|
92
|
+
object: { type: "Identifier", name: "bunja" },
|
|
93
|
+
property: { type: "Identifier", name: "effect" },
|
|
94
|
+
computed: false,
|
|
95
|
+
},
|
|
96
|
+
arguments: [effectFn],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
props.splice(effectPropIndex, 1);
|
|
101
|
+
bodyStatements.splice(i, 0, effectStatement);
|
|
102
|
+
++i;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
node.arguments = [{
|
|
106
|
+
type: "ArrowFunctionExpression",
|
|
107
|
+
params: [],
|
|
108
|
+
body: {
|
|
109
|
+
type: "BlockStatement",
|
|
110
|
+
body: [...useStatements, ...bodyStatements],
|
|
111
|
+
},
|
|
112
|
+
}];
|
|
113
|
+
|
|
114
|
+
modified = true;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return modified ? root.toSource() : fileInfo.source;
|
|
118
|
+
};
|