bunja 2.1.1 → 3.0.0-alpha.3
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 +135 -4
- package/bunja.ts +847 -214
- package/deno.json +1 -1
- package/dist/bunja-BJSmIdkQ.cjs +682 -0
- package/dist/{bunja-BvZKLiEP.d.ts → bunja-BxbzuHdH.d.cts} +73 -18
- package/dist/bunja-CtHOS4_Q.js +634 -0
- package/dist/{bunja-CPUl4ZRK.d.cts → bunja-DEFeIlpt.d.ts} +73 -18
- package/dist/bunja.cjs +1 -1
- package/dist/bunja.d.cts +2 -2
- package/dist/bunja.d.ts +2 -2
- package/dist/bunja.js +1 -1
- package/dist/react.cjs +16 -4
- package/dist/react.d.cts +3 -2
- package/dist/react.d.ts +3 -2
- package/dist/react.js +17 -4
- package/dist/solid.cjs +1 -1
- package/dist/solid.d.cts +2 -2
- package/dist/solid.d.ts +2 -2
- package/dist/solid.js +1 -1
- package/package.json +2 -2
- package/react.ts +34 -8
- package/solid.ts +4 -3
- package/test.ts +434 -4
- package/dist/bunja-Ce8RwebF.cjs +0 -437
- package/dist/bunja-DhBgerdn.js +0 -389
package/bunja.ts
CHANGED
|
@@ -3,24 +3,76 @@
|
|
|
3
3
|
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
4
4
|
|
|
5
5
|
export interface BunjaFn {
|
|
6
|
-
<T>(init: () => T): Bunja<T>;
|
|
6
|
+
<T>(init: () => T): Bunja<T, NoSeed>;
|
|
7
|
+
withSeed: BunjaWithSeedFn;
|
|
7
8
|
use: BunjaUseFn;
|
|
8
|
-
|
|
9
|
+
will: BunjaWillFn;
|
|
9
10
|
effect: BunjaEffectFn;
|
|
10
11
|
}
|
|
11
12
|
export const bunja: BunjaFn = bunjaFn;
|
|
12
|
-
function bunjaFn<T>(init: () => T): Bunja<T> {
|
|
13
|
-
return new Bunja(init);
|
|
13
|
+
function bunjaFn<T>(init: () => T): Bunja<T, NoSeed> {
|
|
14
|
+
return new Bunja(() => init(), NO_SEED);
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
bunjaFn.
|
|
16
|
+
const NO_SEED = Symbol("bunja.noSeed");
|
|
17
|
+
export type NoSeed = typeof NO_SEED;
|
|
18
|
+
bunjaFn.withSeed = function withSeed<Seed, T>(
|
|
19
|
+
defaultSeed: Seed,
|
|
20
|
+
init: (seed: Seed) => T,
|
|
21
|
+
): Bunja<T, Seed> {
|
|
22
|
+
return new Bunja(init, defaultSeed);
|
|
23
|
+
};
|
|
24
|
+
bunjaFn.use =
|
|
25
|
+
((dep: unknown, scopeValuePairs?: ScopeValuePairs) =>
|
|
26
|
+
(getCurrentFrame("`bunja.use`").use as (
|
|
27
|
+
dep: unknown,
|
|
28
|
+
scopeValuePairs?: ScopeValuePairs,
|
|
29
|
+
) => unknown)(
|
|
30
|
+
dep,
|
|
31
|
+
scopeValuePairs,
|
|
32
|
+
)) as BunjaUseFn;
|
|
33
|
+
bunjaFn.will =
|
|
34
|
+
((dep: unknown, scopeValuePairs?: ScopeValuePairs) =>
|
|
35
|
+
(getCurrentFrame("`bunja.will`").will as (
|
|
36
|
+
dep: unknown,
|
|
37
|
+
scopeValuePairs?: ScopeValuePairs,
|
|
38
|
+
) => unknown)(
|
|
39
|
+
dep,
|
|
40
|
+
scopeValuePairs,
|
|
41
|
+
)) as BunjaWillFn;
|
|
42
|
+
bunjaFn.effect =
|
|
43
|
+
((callback: BunjaEffectCallback) =>
|
|
44
|
+
getCurrentFrame("`bunja.effect`").effect(callback)) as BunjaEffectFn;
|
|
18
45
|
|
|
19
|
-
export type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
export type BunjaWithSeedFn = <Seed, T>(
|
|
47
|
+
defaultSeed: Seed,
|
|
48
|
+
init: (seed: Seed) => T,
|
|
49
|
+
) => Bunja<T, Seed>;
|
|
50
|
+
export type ScopeValuePairs = ScopeValuePair<any>[];
|
|
51
|
+
type BunjaRefBase<T, Seed> = {
|
|
52
|
+
bunja: Bunja<T, Seed>;
|
|
53
|
+
with?: ScopeValuePairs;
|
|
54
|
+
};
|
|
55
|
+
export type BunjaGetRef<T, Seed = NoSeed> =
|
|
56
|
+
& BunjaRefBase<T, Seed>
|
|
57
|
+
& ([Seed] extends [NoSeed] ? { seed?: never } : { seed?: Seed });
|
|
58
|
+
export type BunjaRef<T, Seed = NoSeed> = BunjaGetRef<T, Seed>;
|
|
59
|
+
type BunjaPrebakeRef<T, Seed = NoSeed> = BunjaRefBase<T, Seed> & {
|
|
60
|
+
seed?: never;
|
|
61
|
+
};
|
|
62
|
+
export interface BunjaUseFn {
|
|
63
|
+
<T>(dep: Scope<T>): T;
|
|
64
|
+
<T, Seed>(dep: Bunja<T, Seed>): T;
|
|
65
|
+
<T, Seed>(bunja: Bunja<T, Seed>, scopeValuePairs: ScopeValuePairs): T;
|
|
66
|
+
<T, Seed>(ref: BunjaRef<T, Seed>): T;
|
|
67
|
+
}
|
|
68
|
+
export interface BunjaWillFn {
|
|
69
|
+
<T, Seed>(dep: Bunja<T, Seed>): () => T;
|
|
70
|
+
<T, Seed>(
|
|
71
|
+
bunja: Bunja<T, Seed>,
|
|
72
|
+
scopeValuePairs: ScopeValuePairs,
|
|
73
|
+
): () => T;
|
|
74
|
+
<T, Seed>(ref: BunjaRef<T, Seed>): () => T;
|
|
75
|
+
}
|
|
24
76
|
export type BunjaEffectFn = (callback: BunjaEffectCallback) => void;
|
|
25
77
|
export type BunjaEffectCallback = () => (() => void) | void;
|
|
26
78
|
|
|
@@ -38,32 +90,35 @@ export function createBunjaStore(config?: CreateBunjaStoreConfig): BunjaStore {
|
|
|
38
90
|
return store;
|
|
39
91
|
}
|
|
40
92
|
|
|
41
|
-
export type Dep<T> = Bunja<T> | Scope<T>;
|
|
93
|
+
export type Dep<T> = Bunja<T, any> | Scope<T>;
|
|
42
94
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
function invalidEffect() {
|
|
54
|
-
throw new Error(
|
|
55
|
-
"`bunja.effect` can only be used inside a bunja init function.",
|
|
56
|
-
);
|
|
95
|
+
type AnyBunja = Bunja<any, any>;
|
|
96
|
+
type ScopeInstanceMap = Map<Scope<unknown>, ScopeInstance>;
|
|
97
|
+
type BunjaDependencyEdge = "required" | "optional";
|
|
98
|
+
|
|
99
|
+
interface BunjaFrame {
|
|
100
|
+
use: BunjaUseFn;
|
|
101
|
+
will: BunjaWillFn;
|
|
102
|
+
effect: BunjaEffectFn;
|
|
57
103
|
}
|
|
58
104
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
105
|
+
const frameStack: BunjaFrame[] = [];
|
|
106
|
+
function getCurrentFrame(api: string): BunjaFrame {
|
|
107
|
+
const frame = frameStack[frameStack.length - 1];
|
|
108
|
+
if (!frame) {
|
|
109
|
+
throw new Error(`${api} can only be used inside a bunja init function.`);
|
|
110
|
+
}
|
|
111
|
+
return frame;
|
|
63
112
|
}
|
|
64
113
|
|
|
65
|
-
|
|
66
|
-
|
|
114
|
+
function runWithFrame<T>(frame: BunjaFrame, fn: () => T): T {
|
|
115
|
+
frameStack.push(frame);
|
|
116
|
+
try {
|
|
117
|
+
return fn();
|
|
118
|
+
} finally {
|
|
119
|
+
frameStack.pop();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
67
122
|
|
|
68
123
|
interface InternalState {
|
|
69
124
|
bunjas: Record<string, BunjaInstance>;
|
|
@@ -71,19 +126,175 @@ interface InternalState {
|
|
|
71
126
|
instantiating: boolean;
|
|
72
127
|
}
|
|
73
128
|
|
|
74
|
-
interface
|
|
75
|
-
currentBunja:
|
|
129
|
+
interface BunjaInitFrame extends BunjaFrame {
|
|
130
|
+
currentBunja: AnyBunja;
|
|
131
|
+
readScope: ReadScope;
|
|
132
|
+
scopeInstanceMap: ScopeInstanceMap;
|
|
133
|
+
inProgressBunjas: Set<AnyBunja>;
|
|
134
|
+
effects: BunjaEffectCallback[];
|
|
135
|
+
activeDependencyIds: Set<string>;
|
|
136
|
+
activeDependencyRecipes: ActiveDependencyRecipe[];
|
|
137
|
+
activeDependencyMounts: Map<string, () => () => void>;
|
|
138
|
+
activeDependencyDeps: ScopeInstance[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface BunjaPrebakeFrame extends BunjaFrame {
|
|
142
|
+
currentBunja: AnyBunja;
|
|
143
|
+
readScope: ReadScope;
|
|
144
|
+
prebakeContext: BunjaPrebakeContext;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface BunjaPrebakeContext {
|
|
148
|
+
inProgressBunjas: Set<AnyBunja>;
|
|
149
|
+
values: Map<AnyBunja, unknown>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type AnyNormalizedBunjaRef = NormalizedBunjaRef<any, any>;
|
|
153
|
+
interface NormalizedBunjaRef<T, Seed> {
|
|
154
|
+
bunja: Bunja<T, Seed>;
|
|
155
|
+
scopeValuePairs: ScopeValuePairs;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface NormalizedBunjaRuntimeRef<T, Seed>
|
|
159
|
+
extends NormalizedBunjaRef<T, Seed> {
|
|
160
|
+
seed: Seed;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface ActiveDependencyRecipe {
|
|
164
|
+
ref: AnyNormalizedBunjaRef;
|
|
165
|
+
seed: unknown;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface BunjaInstanceRecipe {
|
|
169
|
+
activeDependencies: ActiveDependencyRecipe[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface ResolvedBunja<T> {
|
|
173
|
+
value: T;
|
|
174
|
+
instance: BunjaInstance;
|
|
175
|
+
mount: () => () => void;
|
|
176
|
+
deps: ScopeInstance[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ResolvedActiveDependencyRecipe {
|
|
180
|
+
activeDependencyIds: Set<string>;
|
|
181
|
+
deps: ScopeInstance[];
|
|
76
182
|
}
|
|
77
183
|
|
|
78
184
|
export type WrapInstanceFn = <T>(fn: (dispose: () => void) => T) => T;
|
|
79
185
|
const defaultWrapInstanceFn: WrapInstanceFn = (fn) => fn(noop);
|
|
80
186
|
|
|
187
|
+
function normalizeBunjaRuntimeRef<T, Seed>(
|
|
188
|
+
bunjaOrRef: Bunja<T, Seed> | BunjaRef<T, Seed>,
|
|
189
|
+
scopeValuePairs: ScopeValuePairs = [],
|
|
190
|
+
): NormalizedBunjaRuntimeRef<T, Seed> {
|
|
191
|
+
if (bunjaOrRef instanceof Bunja) {
|
|
192
|
+
return {
|
|
193
|
+
bunja: bunjaOrRef,
|
|
194
|
+
scopeValuePairs,
|
|
195
|
+
seed: bunjaOrRef.defaultSeed,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const { bunja, with: refScopeValuePairs = [] } = bunjaOrRef;
|
|
199
|
+
return {
|
|
200
|
+
bunja,
|
|
201
|
+
scopeValuePairs: refScopeValuePairs,
|
|
202
|
+
seed: "seed" in bunjaOrRef ? (bunjaOrRef.seed as Seed) : bunja.defaultSeed,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeBunjaPrebakeRef<T, Seed>(
|
|
207
|
+
bunjaOrRef: Bunja<T, Seed> | BunjaPrebakeRef<T, Seed>,
|
|
208
|
+
): NormalizedBunjaRef<T, Seed> {
|
|
209
|
+
if (bunjaOrRef instanceof Bunja) {
|
|
210
|
+
return {
|
|
211
|
+
bunja: bunjaOrRef,
|
|
212
|
+
scopeValuePairs: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if ("seed" in bunjaOrRef) {
|
|
216
|
+
throw new Error("A bunja seed cannot be provided to `store.prebake`.");
|
|
217
|
+
}
|
|
218
|
+
const { bunja, with: refScopeValuePairs = [] } = bunjaOrRef;
|
|
219
|
+
return {
|
|
220
|
+
bunja,
|
|
221
|
+
scopeValuePairs: refScopeValuePairs,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function toBunjaGraphRef<T, Seed>(
|
|
226
|
+
bunjaRef: NormalizedBunjaRef<T, Seed>,
|
|
227
|
+
): NormalizedBunjaRef<T, Seed> {
|
|
228
|
+
return {
|
|
229
|
+
bunja: bunjaRef.bunja,
|
|
230
|
+
scopeValuePairs: bunjaRef.scopeValuePairs,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isBunjaRef(value: unknown): value is BunjaRef<any, any> {
|
|
235
|
+
return (
|
|
236
|
+
typeof value === "object" &&
|
|
237
|
+
value !== null &&
|
|
238
|
+
"bunja" in value &&
|
|
239
|
+
(value as { bunja: unknown }).bunja instanceof Bunja
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getBoundScopeSet(
|
|
244
|
+
scopeValuePairs: ScopeValuePairs,
|
|
245
|
+
): Set<Scope<unknown>> {
|
|
246
|
+
return new Set(
|
|
247
|
+
scopeValuePairs.map(([scope]) => scope as Scope<unknown>),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getScopeInstances(
|
|
252
|
+
scopes: Scope<unknown>[],
|
|
253
|
+
scopeInstanceMap: ScopeInstanceMap,
|
|
254
|
+
excludeScopes: Set<Scope<unknown>> = new Set(),
|
|
255
|
+
): ScopeInstance[] {
|
|
256
|
+
return scopes
|
|
257
|
+
.filter((scope) => !excludeScopes.has(scope))
|
|
258
|
+
.map((scope) => scopeInstanceMap.get(scope)!);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function dedupeScopeInstances(
|
|
262
|
+
scopeInstances: ScopeInstance[],
|
|
263
|
+
): ScopeInstance[] {
|
|
264
|
+
const seen = new Set<string>();
|
|
265
|
+
const result: ScopeInstance[] = [];
|
|
266
|
+
for (const scopeInstance of scopeInstances) {
|
|
267
|
+
if (seen.has(scopeInstance.id)) continue;
|
|
268
|
+
seen.add(scopeInstance.id);
|
|
269
|
+
result.push(scopeInstance);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function dedupeBunjas(bunjas: AnyBunja[]): AnyBunja[] {
|
|
275
|
+
return Array.from(new Set(bunjas));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function addUniqueBunjaRef(
|
|
279
|
+
refs: AnyNormalizedBunjaRef[],
|
|
280
|
+
ref: AnyNormalizedBunjaRef,
|
|
281
|
+
): void {
|
|
282
|
+
if (!refs.includes(ref)) refs.push(ref);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createBunjaPrebakeContext(): BunjaPrebakeContext {
|
|
286
|
+
return {
|
|
287
|
+
inProgressBunjas: new Set(),
|
|
288
|
+
values: new Map(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
81
292
|
export class BunjaStore {
|
|
82
293
|
private static counter: number = 0;
|
|
83
294
|
readonly id: string = String(BunjaStore.counter++);
|
|
84
295
|
#bunjas: Record<string, BunjaInstance> = {};
|
|
296
|
+
#bunjaBuckets: Map<string, Set<string>> = new Map();
|
|
85
297
|
#scopes: Map<Scope<unknown>, Map<unknown, ScopeInstance>> = new Map();
|
|
86
|
-
#bakingContext: BunjaBakingContext | undefined;
|
|
87
298
|
wrapInstance: WrapInstanceFn = defaultWrapInstanceFn;
|
|
88
299
|
constructor() {
|
|
89
300
|
if (__DEV__) devtoolsGlobalHook.emit("storeCreated", { storeId: this.id });
|
|
@@ -94,7 +305,7 @@ export class BunjaStore {
|
|
|
94
305
|
bunjas: this.#bunjas,
|
|
95
306
|
scopes: this.#scopes,
|
|
96
307
|
get instantiating() {
|
|
97
|
-
return
|
|
308
|
+
return frameStack.length > 0;
|
|
98
309
|
},
|
|
99
310
|
};
|
|
100
311
|
}
|
|
@@ -102,156 +313,488 @@ export class BunjaStore {
|
|
|
102
313
|
}
|
|
103
314
|
dispose(): void {
|
|
104
315
|
for (const instance of Object.values(this.#bunjas)) instance.dispose();
|
|
105
|
-
for (const instanceMap of
|
|
316
|
+
for (const instanceMap of this.#scopes.values()) {
|
|
106
317
|
for (const instance of instanceMap.values()) instance.dispose();
|
|
107
318
|
}
|
|
108
319
|
this.#bunjas = {};
|
|
320
|
+
this.#bunjaBuckets = new Map();
|
|
109
321
|
this.#scopes = new Map();
|
|
110
322
|
if (__DEV__) devtoolsGlobalHook.emit("storeDisposed", { storeId: this.id });
|
|
111
323
|
}
|
|
112
|
-
get<T>(
|
|
113
|
-
|
|
324
|
+
get<T, Seed>(
|
|
325
|
+
bunjaOrRef: Bunja<T, Seed> | BunjaGetRef<T, Seed>,
|
|
326
|
+
readScope: ReadScope,
|
|
327
|
+
): BunjaStoreGetResult<T> {
|
|
328
|
+
const bunjaRef = normalizeBunjaRuntimeRef(bunjaOrRef);
|
|
329
|
+
const resolved = this.#resolveBunjaRef(
|
|
330
|
+
toBunjaGraphRef(bunjaRef),
|
|
331
|
+
readScope,
|
|
332
|
+
new Set(),
|
|
333
|
+
bunjaRef.seed,
|
|
334
|
+
);
|
|
335
|
+
const result: BunjaStoreGetResult<T> = {
|
|
336
|
+
value: resolved.value,
|
|
337
|
+
mount: resolved.mount,
|
|
338
|
+
deps: resolved.deps.map(({ value }) => value),
|
|
339
|
+
};
|
|
340
|
+
if (__DEV__) {
|
|
341
|
+
result.bunjaInstance = resolved.instance;
|
|
342
|
+
devtoolsGlobalHook.emit("getCalled", {
|
|
343
|
+
storeId: this.id,
|
|
344
|
+
bunjaInstanceId: resolved.instance.id,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
prebake<T, Seed>(
|
|
350
|
+
bunjaOrRef: Bunja<T, Seed> | BunjaPrebakeRef<T, Seed>,
|
|
351
|
+
readScope: ReadScope,
|
|
352
|
+
): BunjaStorePrebakeResult {
|
|
353
|
+
const bunjaRef = normalizeBunjaPrebakeRef(bunjaOrRef);
|
|
354
|
+
this.#prebakeBunjaRef(
|
|
355
|
+
bunjaRef,
|
|
356
|
+
readScope,
|
|
357
|
+
createBunjaPrebakeContext(),
|
|
358
|
+
);
|
|
359
|
+
return {
|
|
360
|
+
relatedBunjas: bunjaRef.bunja.relatedBunjas,
|
|
361
|
+
requiredScopes: bunjaRef.bunja.requiredScopes,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
#resolveBunjaRef<T, Seed>(
|
|
365
|
+
bunjaRef: NormalizedBunjaRef<T, Seed>,
|
|
366
|
+
readScope: ReadScope,
|
|
367
|
+
inProgressBunjas: Set<AnyBunja>,
|
|
368
|
+
seed: Seed = bunjaRef.bunja.defaultSeed,
|
|
369
|
+
): ResolvedBunja<T> {
|
|
370
|
+
const { bunja } = bunjaRef;
|
|
371
|
+
if (inProgressBunjas.has(bunja)) {
|
|
372
|
+
throw new Error("Circular bunja dependency detected.");
|
|
373
|
+
}
|
|
374
|
+
const resolvedReadScope = bunjaRef.scopeValuePairs.length > 0
|
|
375
|
+
? createReadScopeFn(bunjaRef.scopeValuePairs, readScope)
|
|
376
|
+
: readScope;
|
|
377
|
+
inProgressBunjas.add(bunja);
|
|
114
378
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
379
|
+
if (!bunja.baked) {
|
|
380
|
+
return this.#createResolvedBunja(
|
|
381
|
+
bunjaRef,
|
|
382
|
+
resolvedReadScope,
|
|
383
|
+
inProgressBunjas,
|
|
384
|
+
seed,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const scopeInstanceMap = this.#resolveScopeInstanceMap(
|
|
388
|
+
bunja,
|
|
389
|
+
resolvedReadScope,
|
|
390
|
+
);
|
|
391
|
+
const boundScopes = getBoundScopeSet(bunjaRef.scopeValuePairs);
|
|
392
|
+
const scopeInstances = getScopeInstances(
|
|
393
|
+
bunja.requiredScopes,
|
|
394
|
+
scopeInstanceMap,
|
|
395
|
+
);
|
|
396
|
+
const directDeps = getScopeInstances(
|
|
397
|
+
bunja.requiredScopes,
|
|
398
|
+
scopeInstanceMap,
|
|
399
|
+
boundScopes,
|
|
400
|
+
);
|
|
401
|
+
const baseId = bunja.calcBaseInstanceId(scopeInstanceMap);
|
|
402
|
+
const bucket = this.#bunjaBuckets.get(baseId);
|
|
403
|
+
if (bucket) {
|
|
404
|
+
for (const candidateId of Array.from(bucket)) {
|
|
405
|
+
const candidate = this.#bunjas[candidateId];
|
|
406
|
+
if (!candidate) {
|
|
407
|
+
bucket.delete(candidateId);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const activeDeps = this.#resolveActiveDependencyRecipe(
|
|
411
|
+
candidate.recipe,
|
|
412
|
+
resolvedReadScope,
|
|
413
|
+
inProgressBunjas,
|
|
414
|
+
);
|
|
415
|
+
const currentId = bunja.calcInstanceId(
|
|
416
|
+
scopeInstanceMap,
|
|
417
|
+
activeDeps.activeDependencyIds,
|
|
418
|
+
);
|
|
419
|
+
const instance = currentId === candidate.id
|
|
420
|
+
? candidate
|
|
421
|
+
: this.#bunjas[currentId];
|
|
422
|
+
if (!instance) continue;
|
|
423
|
+
return this.#toResolvedBunja(
|
|
424
|
+
instance,
|
|
425
|
+
scopeInstances,
|
|
426
|
+
directDeps,
|
|
427
|
+
activeDeps.deps,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
139
430
|
}
|
|
140
|
-
return
|
|
431
|
+
return this.#createResolvedBunja(
|
|
432
|
+
bunjaRef,
|
|
433
|
+
resolvedReadScope,
|
|
434
|
+
inProgressBunjas,
|
|
435
|
+
seed,
|
|
436
|
+
scopeInstanceMap,
|
|
437
|
+
);
|
|
141
438
|
} finally {
|
|
142
|
-
|
|
439
|
+
inProgressBunjas.delete(bunja);
|
|
143
440
|
}
|
|
144
441
|
}
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
442
|
+
#createResolvedBunja<T, Seed>(
|
|
443
|
+
bunjaRef: NormalizedBunjaRef<T, Seed>,
|
|
444
|
+
readScope: ReadScope,
|
|
445
|
+
inProgressBunjas: Set<AnyBunja>,
|
|
446
|
+
seed: Seed,
|
|
447
|
+
initialScopeInstanceMap: ScopeInstanceMap = new Map(),
|
|
448
|
+
): ResolvedBunja<T> {
|
|
449
|
+
const { bunja } = bunjaRef;
|
|
450
|
+
return this.wrapInstance((dispose) => {
|
|
451
|
+
let instanceCreated = false;
|
|
452
|
+
let disposed = false;
|
|
453
|
+
const disposeOnce = () => {
|
|
454
|
+
if (disposed) return;
|
|
455
|
+
disposed = true;
|
|
456
|
+
dispose();
|
|
457
|
+
};
|
|
458
|
+
try {
|
|
459
|
+
const frame = this.#createInitFrame(
|
|
460
|
+
bunja,
|
|
461
|
+
readScope,
|
|
462
|
+
initialScopeInstanceMap,
|
|
463
|
+
inProgressBunjas,
|
|
464
|
+
);
|
|
465
|
+
const value = runWithFrame(frame, () => bunja.init(seed));
|
|
466
|
+
if (!bunja.baked) bunja.bake();
|
|
467
|
+
this.#ensureRequiredScopeInstances(
|
|
468
|
+
bunja,
|
|
469
|
+
frame.scopeInstanceMap,
|
|
470
|
+
readScope,
|
|
471
|
+
);
|
|
472
|
+
const boundScopes = getBoundScopeSet(bunjaRef.scopeValuePairs);
|
|
473
|
+
const scopeInstances = getScopeInstances(
|
|
474
|
+
bunja.requiredScopes,
|
|
475
|
+
frame.scopeInstanceMap,
|
|
476
|
+
);
|
|
477
|
+
const directDeps = getScopeInstances(
|
|
478
|
+
bunja.requiredScopes,
|
|
479
|
+
frame.scopeInstanceMap,
|
|
480
|
+
boundScopes,
|
|
481
|
+
);
|
|
482
|
+
const baseId = bunja.calcBaseInstanceId(frame.scopeInstanceMap);
|
|
483
|
+
const id = bunja.calcInstanceId(
|
|
484
|
+
frame.scopeInstanceMap,
|
|
485
|
+
frame.activeDependencyIds,
|
|
486
|
+
);
|
|
487
|
+
const existing = this.#bunjas[id];
|
|
488
|
+
if (existing) {
|
|
489
|
+
disposeOnce();
|
|
490
|
+
return this.#toResolvedBunja(
|
|
491
|
+
existing,
|
|
492
|
+
scopeInstances,
|
|
493
|
+
directDeps,
|
|
494
|
+
frame.activeDependencyDeps,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
const instance = this.#createBunjaInstance(
|
|
498
|
+
id,
|
|
499
|
+
baseId,
|
|
500
|
+
value,
|
|
501
|
+
Array.from(frame.activeDependencyMounts.values()),
|
|
502
|
+
frame.effects,
|
|
503
|
+
{ activeDependencies: frame.activeDependencyRecipes },
|
|
504
|
+
dispose,
|
|
505
|
+
);
|
|
506
|
+
instanceCreated = true;
|
|
507
|
+
return this.#toResolvedBunja(
|
|
508
|
+
instance,
|
|
509
|
+
scopeInstances,
|
|
510
|
+
directDeps,
|
|
511
|
+
frame.activeDependencyDeps,
|
|
512
|
+
);
|
|
513
|
+
} finally {
|
|
514
|
+
if (!instanceCreated) disposeOnce();
|
|
159
515
|
}
|
|
160
|
-
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
#toResolvedBunja<T>(
|
|
519
|
+
instance: BunjaInstance,
|
|
520
|
+
scopeInstances: ScopeInstance[],
|
|
521
|
+
directDeps: ScopeInstance[],
|
|
522
|
+
activeDependencyDeps: ScopeInstance[],
|
|
523
|
+
): ResolvedBunja<T> {
|
|
524
|
+
const deps = dedupeScopeInstances([
|
|
525
|
+
...directDeps,
|
|
526
|
+
...activeDependencyDeps,
|
|
527
|
+
]);
|
|
528
|
+
return {
|
|
529
|
+
value: instance.value as T,
|
|
530
|
+
instance,
|
|
531
|
+
deps,
|
|
532
|
+
mount: () => {
|
|
533
|
+
for (const scopeInstance of scopeInstances) scopeInstance.add();
|
|
534
|
+
instance.add();
|
|
535
|
+
return () => {
|
|
536
|
+
instance.sub();
|
|
537
|
+
for (const scopeInstance of scopeInstances) scopeInstance.sub();
|
|
538
|
+
};
|
|
539
|
+
},
|
|
161
540
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
541
|
+
}
|
|
542
|
+
#createInitFrame(
|
|
543
|
+
currentBunja: AnyBunja,
|
|
544
|
+
readScope: ReadScope,
|
|
545
|
+
scopeInstanceMap: ScopeInstanceMap,
|
|
546
|
+
inProgressBunjas: Set<AnyBunja>,
|
|
547
|
+
): BunjaInitFrame {
|
|
548
|
+
const frame = {
|
|
549
|
+
currentBunja,
|
|
550
|
+
readScope,
|
|
551
|
+
scopeInstanceMap,
|
|
552
|
+
inProgressBunjas,
|
|
553
|
+
effects: [] as BunjaEffectCallback[],
|
|
554
|
+
activeDependencyIds: new Set<string>(),
|
|
555
|
+
activeDependencyRecipes: [] as ActiveDependencyRecipe[],
|
|
556
|
+
activeDependencyMounts: new Map<string, () => () => void>(),
|
|
557
|
+
activeDependencyDeps: [] as ScopeInstance[],
|
|
558
|
+
use: ((dep: unknown, scopeValuePairs?: ScopeValuePairs) => {
|
|
559
|
+
if (dep instanceof Scope) {
|
|
560
|
+
return this.#useScopeInFrame(frame, dep as Scope<unknown>);
|
|
561
|
+
}
|
|
562
|
+
if (dep instanceof Bunja || isBunjaRef(dep)) {
|
|
563
|
+
return this.#useBunjaDependencyInFrame(
|
|
564
|
+
frame,
|
|
565
|
+
normalizeBunjaRuntimeRef(dep, scopeValuePairs),
|
|
566
|
+
"required",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
throw new Error("`bunja.use` can only be used with Bunja or Scope.");
|
|
570
|
+
}) as BunjaUseFn,
|
|
571
|
+
will: ((dep: unknown, scopeValuePairs?: ScopeValuePairs) => {
|
|
572
|
+
if (!(dep instanceof Bunja || isBunjaRef(dep))) {
|
|
573
|
+
throw new Error("`bunja.will` can only be used with Bunja.");
|
|
574
|
+
}
|
|
575
|
+
const bunjaRef = normalizeBunjaRuntimeRef(dep, scopeValuePairs);
|
|
576
|
+
currentBunja.addOptionalBunjaRef(toBunjaGraphRef(bunjaRef));
|
|
577
|
+
return () => {
|
|
578
|
+
if (frameStack[frameStack.length - 1] !== frame) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"A thunk returned by `bunja.will` can only be called inside the same bunja init function.",
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return this.#useBunjaDependencyInFrame(
|
|
584
|
+
frame,
|
|
585
|
+
bunjaRef,
|
|
586
|
+
"optional",
|
|
587
|
+
);
|
|
588
|
+
};
|
|
589
|
+
}) as BunjaWillFn,
|
|
590
|
+
effect: ((callback: BunjaEffectCallback) => {
|
|
591
|
+
frame.effects.push(callback);
|
|
592
|
+
}) as BunjaEffectFn,
|
|
593
|
+
} satisfies BunjaInitFrame;
|
|
594
|
+
return frame;
|
|
595
|
+
}
|
|
596
|
+
#useScopeInFrame<T>(frame: BunjaInitFrame, scope: Scope<T>): T {
|
|
597
|
+
if (!frame.currentBunja.baked) {
|
|
598
|
+
frame.currentBunja.addScope(scope as Scope<unknown>);
|
|
599
|
+
}
|
|
600
|
+
let scopeInstance = frame.scopeInstanceMap.get(scope as Scope<unknown>);
|
|
601
|
+
if (!scopeInstance) {
|
|
602
|
+
if (frame.currentBunja.baked) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
"`bunja.use(scope)` cannot introduce a new scope after the bunja is baked.",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
scopeInstance = this.#getScopeInstance(
|
|
608
|
+
scope as Scope<unknown>,
|
|
609
|
+
frame.readScope(scope),
|
|
166
610
|
);
|
|
611
|
+
frame.scopeInstanceMap.set(scope as Scope<unknown>, scopeInstance);
|
|
167
612
|
}
|
|
168
|
-
|
|
169
|
-
return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
|
|
613
|
+
return scopeInstance.value as T;
|
|
170
614
|
}
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
addDep(d);
|
|
182
|
-
if (map.has(d)) return map.get(d)!.value as T;
|
|
183
|
-
const instance = getInstance(d);
|
|
184
|
-
map.set(d, instance);
|
|
185
|
-
return instance.value as T;
|
|
186
|
-
}) as <T>(dep: Dep<T>) => T;
|
|
615
|
+
#useBunjaDependencyInFrame<T, Seed>(
|
|
616
|
+
frame: BunjaInitFrame,
|
|
617
|
+
bunjaRef: NormalizedBunjaRuntimeRef<T, Seed>,
|
|
618
|
+
edge: BunjaDependencyEdge,
|
|
619
|
+
): T {
|
|
620
|
+
const graphRef = toBunjaGraphRef(bunjaRef);
|
|
621
|
+
if (edge === "optional" || graphRef.scopeValuePairs.length > 0) {
|
|
622
|
+
frame.currentBunja.addOptionalBunjaRef(graphRef);
|
|
623
|
+
} else if (edge === "required") {
|
|
624
|
+
frame.currentBunja.addRequiredBunjaRef(graphRef);
|
|
187
625
|
}
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const useBunja = getUse(
|
|
194
|
-
bunjaInstanceMap,
|
|
195
|
-
(dep) => this.#bakingContext!.currentBunja.addParent(dep),
|
|
196
|
-
(dep) => {
|
|
197
|
-
if (dep.baked) {
|
|
198
|
-
for (const scope of dep.relatedScopes) useScope(scope);
|
|
199
|
-
}
|
|
200
|
-
return this.#getBunjaInstance(dep, scopeInstanceMap);
|
|
201
|
-
},
|
|
626
|
+
const resolved = this.#resolveBunjaRef(
|
|
627
|
+
graphRef,
|
|
628
|
+
frame.readScope,
|
|
629
|
+
frame.inProgressBunjas,
|
|
630
|
+
bunjaRef.seed,
|
|
202
631
|
);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
632
|
+
frame.activeDependencyIds.add(resolved.instance.id);
|
|
633
|
+
frame.activeDependencyRecipes.push({
|
|
634
|
+
ref: graphRef,
|
|
635
|
+
seed: bunjaRef.seed,
|
|
636
|
+
});
|
|
637
|
+
if (!frame.activeDependencyMounts.has(resolved.instance.id)) {
|
|
638
|
+
frame.activeDependencyMounts.set(resolved.instance.id, resolved.mount);
|
|
639
|
+
}
|
|
640
|
+
frame.activeDependencyDeps.push(...resolved.deps);
|
|
641
|
+
return resolved.value;
|
|
642
|
+
}
|
|
643
|
+
#resolveActiveDependencyRecipe(
|
|
644
|
+
recipe: BunjaInstanceRecipe,
|
|
645
|
+
readScope: ReadScope,
|
|
646
|
+
inProgressBunjas: Set<AnyBunja>,
|
|
647
|
+
): ResolvedActiveDependencyRecipe {
|
|
648
|
+
const activeDependencyIds = new Set<string>();
|
|
649
|
+
const deps: ScopeInstance[] = [];
|
|
650
|
+
for (const { ref, seed } of recipe.activeDependencies) {
|
|
651
|
+
const resolved = this.#resolveBunjaRef(
|
|
652
|
+
ref,
|
|
653
|
+
readScope,
|
|
654
|
+
inProgressBunjas,
|
|
655
|
+
seed,
|
|
656
|
+
);
|
|
657
|
+
activeDependencyIds.add(resolved.instance.id);
|
|
658
|
+
deps.push(...resolved.deps);
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
activeDependencyIds,
|
|
662
|
+
deps: dedupeScopeInstances(deps),
|
|
207
663
|
};
|
|
208
|
-
|
|
664
|
+
}
|
|
665
|
+
#prebakeBunjaRef<T, Seed>(
|
|
666
|
+
bunjaRef: NormalizedBunjaRef<T, Seed>,
|
|
667
|
+
readScope: ReadScope,
|
|
668
|
+
prebakeContext: BunjaPrebakeContext,
|
|
669
|
+
): T {
|
|
670
|
+
const { bunja } = bunjaRef;
|
|
671
|
+
if (prebakeContext.values.has(bunja)) {
|
|
672
|
+
return prebakeContext.values.get(bunja) as T;
|
|
673
|
+
}
|
|
674
|
+
const { inProgressBunjas } = prebakeContext;
|
|
675
|
+
if (inProgressBunjas.has(bunja)) {
|
|
676
|
+
throw new Error("Circular bunja dependency detected.");
|
|
677
|
+
}
|
|
678
|
+
const resolvedReadScope = bunjaRef.scopeValuePairs.length > 0
|
|
679
|
+
? createReadScopeFn(bunjaRef.scopeValuePairs, readScope)
|
|
680
|
+
: readScope;
|
|
681
|
+
inProgressBunjas.add(bunja);
|
|
209
682
|
try {
|
|
210
|
-
this
|
|
211
|
-
|
|
212
|
-
|
|
683
|
+
return this.wrapInstance((dispose) => {
|
|
684
|
+
try {
|
|
685
|
+
const frame = this.#createPrebakeFrame(
|
|
686
|
+
bunja,
|
|
687
|
+
resolvedReadScope,
|
|
688
|
+
prebakeContext,
|
|
689
|
+
);
|
|
690
|
+
const value = runWithFrame(
|
|
691
|
+
frame,
|
|
692
|
+
() => bunja.init(bunja.defaultSeed),
|
|
693
|
+
);
|
|
694
|
+
if (!bunja.baked) bunja.bake();
|
|
695
|
+
for (
|
|
696
|
+
const ref of [
|
|
697
|
+
...bunja.requiredBunjaRefs,
|
|
698
|
+
...bunja.optionalBunjaRefs,
|
|
699
|
+
]
|
|
700
|
+
) this.#prebakeBunjaRef(ref, resolvedReadScope, prebakeContext);
|
|
701
|
+
prebakeContext.values.set(bunja, value);
|
|
702
|
+
return value;
|
|
703
|
+
} finally {
|
|
704
|
+
dispose();
|
|
705
|
+
}
|
|
706
|
+
});
|
|
213
707
|
} finally {
|
|
214
|
-
|
|
708
|
+
inProgressBunjas.delete(bunja);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
#createPrebakeFrame(
|
|
712
|
+
currentBunja: AnyBunja,
|
|
713
|
+
readScope: ReadScope,
|
|
714
|
+
prebakeContext: BunjaPrebakeContext,
|
|
715
|
+
): BunjaPrebakeFrame {
|
|
716
|
+
const frame = {
|
|
717
|
+
currentBunja,
|
|
718
|
+
readScope,
|
|
719
|
+
prebakeContext,
|
|
720
|
+
use: ((dep: unknown, scopeValuePairs?: ScopeValuePairs) => {
|
|
721
|
+
if (dep instanceof Scope) {
|
|
722
|
+
if (!currentBunja.baked) {
|
|
723
|
+
currentBunja.addScope(dep as Scope<unknown>);
|
|
724
|
+
}
|
|
725
|
+
return readScope(dep as Scope<unknown>);
|
|
726
|
+
}
|
|
727
|
+
if (dep instanceof Bunja || isBunjaRef(dep)) {
|
|
728
|
+
const bunjaRef = normalizeBunjaRuntimeRef(dep, scopeValuePairs);
|
|
729
|
+
return this.#prebakeBunjaDependencyInFrame(
|
|
730
|
+
frame,
|
|
731
|
+
toBunjaGraphRef(bunjaRef),
|
|
732
|
+
"required",
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
throw new Error("`bunja.use` can only be used with Bunja or Scope.");
|
|
736
|
+
}) as BunjaUseFn,
|
|
737
|
+
will: ((dep: unknown, scopeValuePairs?: ScopeValuePairs) => {
|
|
738
|
+
if (!(dep instanceof Bunja || isBunjaRef(dep))) {
|
|
739
|
+
throw new Error("`bunja.will` can only be used with Bunja.");
|
|
740
|
+
}
|
|
741
|
+
const bunjaRef = toBunjaGraphRef(
|
|
742
|
+
normalizeBunjaRuntimeRef(dep, scopeValuePairs),
|
|
743
|
+
);
|
|
744
|
+
currentBunja.addOptionalBunjaRef(bunjaRef);
|
|
745
|
+
const value = this.#prebakeBunjaRef(
|
|
746
|
+
bunjaRef,
|
|
747
|
+
readScope,
|
|
748
|
+
prebakeContext,
|
|
749
|
+
);
|
|
750
|
+
return () => {
|
|
751
|
+
if (frameStack[frameStack.length - 1] !== frame) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"A thunk returned by `bunja.will` can only be called inside the same bunja init function.",
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return value;
|
|
757
|
+
};
|
|
758
|
+
}) as BunjaWillFn,
|
|
759
|
+
effect: noop,
|
|
760
|
+
} satisfies BunjaPrebakeFrame;
|
|
761
|
+
return frame;
|
|
762
|
+
}
|
|
763
|
+
#prebakeBunjaDependencyInFrame<T, Seed>(
|
|
764
|
+
frame: BunjaPrebakeFrame,
|
|
765
|
+
bunjaRef: NormalizedBunjaRef<T, Seed>,
|
|
766
|
+
edge: BunjaDependencyEdge,
|
|
767
|
+
): T {
|
|
768
|
+
if (edge === "optional" || bunjaRef.scopeValuePairs.length > 0) {
|
|
769
|
+
frame.currentBunja.addOptionalBunjaRef(bunjaRef);
|
|
770
|
+
} else if (edge === "required") {
|
|
771
|
+
frame.currentBunja.addRequiredBunjaRef(bunjaRef);
|
|
215
772
|
}
|
|
773
|
+
return this.#prebakeBunjaRef(
|
|
774
|
+
bunjaRef,
|
|
775
|
+
frame.readScope,
|
|
776
|
+
frame.prebakeContext,
|
|
777
|
+
);
|
|
216
778
|
}
|
|
217
|
-
#
|
|
218
|
-
bunja:
|
|
779
|
+
#resolveScopeInstanceMap(
|
|
780
|
+
bunja: AnyBunja,
|
|
781
|
+
readScope: ReadScope,
|
|
782
|
+
): ScopeInstanceMap {
|
|
783
|
+
const scopeInstanceMap: ScopeInstanceMap = new Map();
|
|
784
|
+
this.#ensureRequiredScopeInstances(bunja, scopeInstanceMap, readScope);
|
|
785
|
+
return scopeInstanceMap;
|
|
786
|
+
}
|
|
787
|
+
#ensureRequiredScopeInstances(
|
|
788
|
+
bunja: AnyBunja,
|
|
219
789
|
scopeInstanceMap: ScopeInstanceMap,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
};
|
|
229
|
-
bunjaFn.fork = (b, scopeValuePairs) => {
|
|
230
|
-
const readScope = createReadScopeFn(scopeValuePairs, bunjaFn.use);
|
|
231
|
-
const { value, mount } = this.get(b, readScope);
|
|
232
|
-
bunjaFn.effect(mount);
|
|
233
|
-
return value;
|
|
234
|
-
};
|
|
235
|
-
if (this.#bakingContext) this.#bakingContext.currentBunja = bunja;
|
|
236
|
-
if (bunja.baked) {
|
|
237
|
-
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
238
|
-
if (id in this.#bunjas) return this.#bunjas[id];
|
|
239
|
-
return this.wrapInstance((dispose) => {
|
|
240
|
-
const value = bunja.init();
|
|
241
|
-
return this.#createBunjaInstance(id, value, effects, dispose);
|
|
242
|
-
});
|
|
243
|
-
} else {
|
|
244
|
-
return this.wrapInstance((dispose) => {
|
|
245
|
-
const value = bunja.init();
|
|
246
|
-
bunja.bake();
|
|
247
|
-
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
248
|
-
return this.#createBunjaInstance(id, value, effects, dispose);
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
} finally {
|
|
252
|
-
bunjaFn.effect = originalEffect;
|
|
253
|
-
bunjaFn.fork = originalFork;
|
|
254
|
-
if (this.#bakingContext) this.#bakingContext.currentBunja = prevBunja!;
|
|
790
|
+
readScope: ReadScope,
|
|
791
|
+
): void {
|
|
792
|
+
for (const scope of bunja.requiredScopes) {
|
|
793
|
+
if (scopeInstanceMap.has(scope)) continue;
|
|
794
|
+
scopeInstanceMap.set(
|
|
795
|
+
scope,
|
|
796
|
+
this.#getScopeInstance(scope, readScope(scope)),
|
|
797
|
+
);
|
|
255
798
|
}
|
|
256
799
|
}
|
|
257
800
|
#getScopeInstance(scope: Scope<unknown>, value: unknown): ScopeInstance {
|
|
@@ -275,27 +818,38 @@ export class BunjaStore {
|
|
|
275
818
|
}
|
|
276
819
|
#createBunjaInstance(
|
|
277
820
|
id: string,
|
|
821
|
+
baseId: string,
|
|
278
822
|
value: unknown,
|
|
823
|
+
dependencyMounts: (() => () => void)[],
|
|
279
824
|
effects: BunjaEffectCallback[],
|
|
825
|
+
recipe: BunjaInstanceRecipe,
|
|
280
826
|
dispose: () => void,
|
|
281
827
|
): BunjaInstance {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
828
|
+
const bunjaInstance = new BunjaInstance(
|
|
829
|
+
id,
|
|
830
|
+
baseId,
|
|
831
|
+
value,
|
|
832
|
+
dependencyMounts,
|
|
833
|
+
effects,
|
|
834
|
+
recipe,
|
|
835
|
+
() => {
|
|
836
|
+
if (__DEV__) {
|
|
837
|
+
devtoolsGlobalHook.emit("bunjaInstanceUnmounted", {
|
|
838
|
+
storeId: this.id,
|
|
839
|
+
bunjaInstanceId: id,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
dispose();
|
|
843
|
+
delete this.#bunjas[id];
|
|
844
|
+
const bucket = this.#bunjaBuckets.get(baseId);
|
|
845
|
+
bucket?.delete(id);
|
|
846
|
+
if (bucket?.size === 0) this.#bunjaBuckets.delete(baseId);
|
|
847
|
+
},
|
|
848
|
+
);
|
|
298
849
|
this.#bunjas[id] = bunjaInstance;
|
|
850
|
+
const bucket = this.#bunjaBuckets.get(baseId) ??
|
|
851
|
+
this.#bunjaBuckets.set(baseId, new Set()).get(baseId)!;
|
|
852
|
+
bucket.add(id);
|
|
299
853
|
if (__DEV__) {
|
|
300
854
|
devtoolsGlobalHook.emit("bunjaInstanceMounted", {
|
|
301
855
|
storeId: this.id,
|
|
@@ -342,6 +896,11 @@ export interface BunjaStoreGetResult<T> {
|
|
|
342
896
|
bunjaInstance?: BunjaInstance;
|
|
343
897
|
}
|
|
344
898
|
|
|
899
|
+
export interface BunjaStorePrebakeResult {
|
|
900
|
+
relatedBunjas: Bunja<any, any>[];
|
|
901
|
+
requiredScopes: Scope<unknown>[];
|
|
902
|
+
}
|
|
903
|
+
|
|
345
904
|
export function delayUnmount(
|
|
346
905
|
mount: () => () => void,
|
|
347
906
|
ms: number = 0,
|
|
@@ -352,12 +911,20 @@ export function delayUnmount(
|
|
|
352
911
|
};
|
|
353
912
|
}
|
|
354
913
|
|
|
355
|
-
export class Bunja<T> {
|
|
914
|
+
export class Bunja<T, Seed = NoSeed> {
|
|
356
915
|
private static counter: number = 0;
|
|
357
916
|
readonly id: string = String(Bunja.counter++);
|
|
358
917
|
debugLabel: string = "";
|
|
359
|
-
#phase: BunjaPhase = {
|
|
360
|
-
|
|
918
|
+
#phase: BunjaPhase = {
|
|
919
|
+
baked: false,
|
|
920
|
+
requiredBunjaRefs: [],
|
|
921
|
+
optionalBunjaRefs: [],
|
|
922
|
+
scopes: new Set(),
|
|
923
|
+
};
|
|
924
|
+
constructor(
|
|
925
|
+
public init: (seed: Seed) => T,
|
|
926
|
+
public defaultSeed: Seed,
|
|
927
|
+
) {
|
|
361
928
|
if (__DEV__) {
|
|
362
929
|
devtoolsGlobalHook.bunjas[this.id] = this;
|
|
363
930
|
devtoolsGlobalHook.emit("bunjaCreated", { bunjaId: this.id });
|
|
@@ -366,21 +933,44 @@ export class Bunja<T> {
|
|
|
366
933
|
get baked(): boolean {
|
|
367
934
|
return this.#phase.baked;
|
|
368
935
|
}
|
|
369
|
-
get
|
|
370
|
-
|
|
371
|
-
|
|
936
|
+
get requiredBunjas(): AnyBunja[] {
|
|
937
|
+
return dedupeBunjas(
|
|
938
|
+
this.#phase.requiredBunjaRefs.map(({ bunja }) => bunja),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
get optionalBunjas(): AnyBunja[] {
|
|
942
|
+
return dedupeBunjas(
|
|
943
|
+
this.#phase.optionalBunjaRefs.map(({ bunja }) => bunja),
|
|
944
|
+
);
|
|
372
945
|
}
|
|
373
|
-
get
|
|
946
|
+
get requiredBunjaRefs(): AnyNormalizedBunjaRef[] {
|
|
947
|
+
return this.#phase.requiredBunjaRefs;
|
|
948
|
+
}
|
|
949
|
+
get optionalBunjaRefs(): AnyNormalizedBunjaRef[] {
|
|
950
|
+
return this.#phase.optionalBunjaRefs;
|
|
951
|
+
}
|
|
952
|
+
get expandedRequiredBunjas(): AnyBunja[] {
|
|
374
953
|
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
375
|
-
return this.#phase.
|
|
954
|
+
return this.#phase.expandedRequiredBunjas;
|
|
376
955
|
}
|
|
377
|
-
get
|
|
956
|
+
get relatedBunjas(): AnyBunja[] {
|
|
378
957
|
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
379
|
-
return
|
|
958
|
+
return toposortRelatedBunjas([
|
|
959
|
+
...this.requiredBunjas,
|
|
960
|
+
...this.optionalBunjas,
|
|
961
|
+
]);
|
|
380
962
|
}
|
|
381
|
-
|
|
963
|
+
get requiredScopes(): Scope<unknown>[] {
|
|
964
|
+
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
965
|
+
return this.#phase.requiredScopes;
|
|
966
|
+
}
|
|
967
|
+
addRequiredBunjaRef(ref: AnyNormalizedBunjaRef): void {
|
|
382
968
|
if (this.#phase.baked) return;
|
|
383
|
-
this.#phase.
|
|
969
|
+
addUniqueBunjaRef(this.#phase.requiredBunjaRefs, ref);
|
|
970
|
+
}
|
|
971
|
+
addOptionalBunjaRef(ref: AnyNormalizedBunjaRef): void {
|
|
972
|
+
if (this.#phase.baked) return;
|
|
973
|
+
addUniqueBunjaRef(this.#phase.optionalBunjaRefs, ref);
|
|
384
974
|
}
|
|
385
975
|
addScope(scope: Scope<unknown>): void {
|
|
386
976
|
if (this.#phase.baked) return;
|
|
@@ -389,22 +979,40 @@ export class Bunja<T> {
|
|
|
389
979
|
bake(): void {
|
|
390
980
|
if (this.#phase.baked) throw new Error("Bunja is already baked.");
|
|
391
981
|
const scopes = this.#phase.scopes;
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
982
|
+
const requiredBunjaRefs = this.#phase.requiredBunjaRefs;
|
|
983
|
+
const optionalBunjaRefs = this.#phase.optionalBunjaRefs;
|
|
984
|
+
const requiredBunjas = this.requiredBunjas;
|
|
985
|
+
const expandedRequiredBunjas = toposortRequiredBunjas(requiredBunjas);
|
|
986
|
+
const requiredScopeSet = new Set<Scope<unknown>>();
|
|
987
|
+
for (const bunja of expandedRequiredBunjas) {
|
|
988
|
+
for (const scope of bunja.requiredScopes) requiredScopeSet.add(scope);
|
|
989
|
+
}
|
|
990
|
+
for (const scope of scopes) requiredScopeSet.add(scope);
|
|
991
|
+
const requiredScopes = Array.from(requiredScopeSet);
|
|
992
|
+
this.#phase = {
|
|
993
|
+
baked: true,
|
|
994
|
+
requiredBunjaRefs,
|
|
995
|
+
optionalBunjaRefs,
|
|
996
|
+
expandedRequiredBunjas,
|
|
997
|
+
requiredScopes,
|
|
998
|
+
};
|
|
401
999
|
}
|
|
402
|
-
|
|
403
|
-
|
|
1000
|
+
calcBaseInstanceId(
|
|
1001
|
+
scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>,
|
|
1002
|
+
): string {
|
|
1003
|
+
const scopeInstanceIds = this.requiredScopes.map(
|
|
404
1004
|
(scope) => scopeInstanceMap.get(scope)!.id,
|
|
405
1005
|
);
|
|
406
1006
|
return `${this.id}:${scopeInstanceIds.join(",")}`;
|
|
407
1007
|
}
|
|
1008
|
+
calcInstanceId(
|
|
1009
|
+
scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>,
|
|
1010
|
+
activeDependencyIds: Iterable<string> = [],
|
|
1011
|
+
): string {
|
|
1012
|
+
return `${this.calcBaseInstanceId(scopeInstanceMap)}:${
|
|
1013
|
+
Array.from(activeDependencyIds).join(",")
|
|
1014
|
+
}`;
|
|
1015
|
+
}
|
|
408
1016
|
toString(): string {
|
|
409
1017
|
const { id, debugLabel } = this;
|
|
410
1018
|
return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
@@ -415,15 +1023,17 @@ type BunjaPhase = BunjaPhaseUnbaked | BunjaPhaseBaked;
|
|
|
415
1023
|
|
|
416
1024
|
interface BunjaPhaseUnbaked {
|
|
417
1025
|
readonly baked: false;
|
|
418
|
-
readonly
|
|
1026
|
+
readonly requiredBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1027
|
+
readonly optionalBunjaRefs: AnyNormalizedBunjaRef[];
|
|
419
1028
|
readonly scopes: Set<Scope<unknown>>;
|
|
420
1029
|
}
|
|
421
1030
|
|
|
422
1031
|
interface BunjaPhaseBaked {
|
|
423
1032
|
readonly baked: true;
|
|
424
|
-
readonly
|
|
425
|
-
readonly
|
|
426
|
-
readonly
|
|
1033
|
+
readonly requiredBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1034
|
+
readonly optionalBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1035
|
+
readonly expandedRequiredBunjas: AnyBunja[];
|
|
1036
|
+
readonly requiredScopes: Scope<unknown>[];
|
|
427
1037
|
}
|
|
428
1038
|
|
|
429
1039
|
export class Scope<T> {
|
|
@@ -468,22 +1078,39 @@ abstract class RefCounter {
|
|
|
468
1078
|
|
|
469
1079
|
class BunjaInstance extends RefCounter {
|
|
470
1080
|
#cleanup: (() => void) | undefined;
|
|
1081
|
+
#disposed = false;
|
|
471
1082
|
constructor(
|
|
472
1083
|
public readonly id: string,
|
|
1084
|
+
public readonly baseId: string,
|
|
473
1085
|
public readonly value: unknown,
|
|
474
|
-
|
|
1086
|
+
private readonly dependencyMounts: (() => () => void)[],
|
|
1087
|
+
private readonly effects: BunjaEffectCallback[],
|
|
1088
|
+
public readonly recipe: BunjaInstanceRecipe,
|
|
475
1089
|
private readonly _dispose: () => void,
|
|
476
1090
|
) {
|
|
477
1091
|
super();
|
|
478
1092
|
}
|
|
479
1093
|
override dispose(): void {
|
|
1094
|
+
if (this.#disposed) return;
|
|
1095
|
+
this.#disposed = true;
|
|
480
1096
|
this.#cleanup?.();
|
|
1097
|
+
this.#cleanup = undefined;
|
|
481
1098
|
this._dispose();
|
|
482
1099
|
}
|
|
483
1100
|
override add(): void {
|
|
484
|
-
this.#cleanup ??= this
|
|
1101
|
+
this.#cleanup ??= this.#mount();
|
|
485
1102
|
super.add();
|
|
486
1103
|
}
|
|
1104
|
+
#mount(): () => void {
|
|
1105
|
+
const dependencyCleanups = this.dependencyMounts.map((mount) => mount());
|
|
1106
|
+
const effectCleanups = this.effects
|
|
1107
|
+
.map((effect) => effect())
|
|
1108
|
+
.filter(Boolean) as (() => void)[];
|
|
1109
|
+
return () => {
|
|
1110
|
+
for (const cleanup of dependencyCleanups) cleanup();
|
|
1111
|
+
for (const cleanup of effectCleanups) cleanup();
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
487
1114
|
}
|
|
488
1115
|
|
|
489
1116
|
class ScopeInstance extends RefCounter {
|
|
@@ -497,26 +1124,32 @@ class ScopeInstance extends RefCounter {
|
|
|
497
1124
|
}
|
|
498
1125
|
}
|
|
499
1126
|
|
|
500
|
-
|
|
501
|
-
parents: Toposortable[];
|
|
502
|
-
}
|
|
503
|
-
function toposort<T extends Toposortable>(nodes: T[]): T[] {
|
|
1127
|
+
function toposort<T>(nodes: T[], getDependencies: (node: T) => T[]): T[] {
|
|
504
1128
|
const visited = new Set<T>();
|
|
505
1129
|
const result: T[] = [];
|
|
506
1130
|
function visit(current: T) {
|
|
507
1131
|
if (visited.has(current)) return;
|
|
508
1132
|
visited.add(current);
|
|
509
|
-
for (const
|
|
1133
|
+
for (const dependency of getDependencies(current)) visit(dependency);
|
|
510
1134
|
result.push(current);
|
|
511
1135
|
}
|
|
512
1136
|
for (const node of nodes) visit(node);
|
|
513
1137
|
return result;
|
|
514
1138
|
}
|
|
1139
|
+
function toposortRequiredBunjas(bunjas: AnyBunja[]): AnyBunja[] {
|
|
1140
|
+
return toposort(bunjas, (bunja) => bunja.requiredBunjas);
|
|
1141
|
+
}
|
|
1142
|
+
function toposortRelatedBunjas(bunjas: AnyBunja[]): AnyBunja[] {
|
|
1143
|
+
return toposort(bunjas, (bunja) => [
|
|
1144
|
+
...bunja.requiredBunjas,
|
|
1145
|
+
...bunja.optionalBunjas,
|
|
1146
|
+
]);
|
|
1147
|
+
}
|
|
515
1148
|
|
|
516
1149
|
const noop = () => {};
|
|
517
1150
|
|
|
518
1151
|
export interface BunjaDevtoolsGlobalHook {
|
|
519
|
-
bunjas: Record<string, Bunja<any>>;
|
|
1152
|
+
bunjas: Record<string, Bunja<any, any>>;
|
|
520
1153
|
scopes: Record<string, Scope<any>>;
|
|
521
1154
|
listeners: Record<
|
|
522
1155
|
BunjaDevtoolsEventType,
|