bunja 2.1.0 → 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 -222
- package/deno.json +1 -1
- package/dist/bunja-BJSmIdkQ.cjs +682 -0
- package/dist/{bunja-BKpQTG04.d.cts → bunja-BxbzuHdH.d.cts} +73 -19
- package/dist/bunja-CtHOS4_Q.js +634 -0
- package/dist/{bunja-B_HNgDan.d.ts → bunja-DEFeIlpt.d.ts} +73 -19
- 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-BOUkMIz6.js +0 -396
- package/dist/bunja-DFFVW7Gi.cjs +0 -444
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,25 +126,178 @@ 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
|
-
if (__DEV__) {
|
|
90
|
-
devtoolsGlobalHook.stores[this.id] = this;
|
|
91
|
-
devtoolsGlobalHook.emit("storeCreated", { storeId: this.id });
|
|
92
|
-
}
|
|
300
|
+
if (__DEV__) devtoolsGlobalHook.emit("storeCreated", { storeId: this.id });
|
|
93
301
|
}
|
|
94
302
|
get _internalState(): InternalState | undefined {
|
|
95
303
|
if (__DEV__) {
|
|
@@ -97,7 +305,7 @@ export class BunjaStore {
|
|
|
97
305
|
bunjas: this.#bunjas,
|
|
98
306
|
scopes: this.#scopes,
|
|
99
307
|
get instantiating() {
|
|
100
|
-
return
|
|
308
|
+
return frameStack.length > 0;
|
|
101
309
|
},
|
|
102
310
|
};
|
|
103
311
|
}
|
|
@@ -105,159 +313,488 @@ export class BunjaStore {
|
|
|
105
313
|
}
|
|
106
314
|
dispose(): void {
|
|
107
315
|
for (const instance of Object.values(this.#bunjas)) instance.dispose();
|
|
108
|
-
for (const instanceMap of
|
|
316
|
+
for (const instanceMap of this.#scopes.values()) {
|
|
109
317
|
for (const instance of instanceMap.values()) instance.dispose();
|
|
110
318
|
}
|
|
111
319
|
this.#bunjas = {};
|
|
320
|
+
this.#bunjaBuckets = new Map();
|
|
112
321
|
this.#scopes = new Map();
|
|
322
|
+
if (__DEV__) devtoolsGlobalHook.emit("storeDisposed", { storeId: this.id });
|
|
323
|
+
}
|
|
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
|
+
};
|
|
113
340
|
if (__DEV__) {
|
|
114
|
-
|
|
115
|
-
|
|
341
|
+
result.bunjaInstance = resolved.instance;
|
|
342
|
+
devtoolsGlobalHook.emit("getCalled", {
|
|
343
|
+
storeId: this.id,
|
|
344
|
+
bunjaInstanceId: resolved.instance.id,
|
|
345
|
+
});
|
|
116
346
|
}
|
|
347
|
+
return result;
|
|
117
348
|
}
|
|
118
|
-
|
|
119
|
-
|
|
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);
|
|
120
378
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
430
|
}
|
|
146
|
-
return
|
|
431
|
+
return this.#createResolvedBunja(
|
|
432
|
+
bunjaRef,
|
|
433
|
+
resolvedReadScope,
|
|
434
|
+
inProgressBunjas,
|
|
435
|
+
seed,
|
|
436
|
+
scopeInstanceMap,
|
|
437
|
+
);
|
|
147
438
|
} finally {
|
|
148
|
-
|
|
439
|
+
inProgressBunjas.delete(bunja);
|
|
149
440
|
}
|
|
150
441
|
}
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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();
|
|
165
515
|
}
|
|
166
|
-
|
|
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
|
+
},
|
|
167
540
|
};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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),
|
|
172
610
|
);
|
|
611
|
+
frame.scopeInstanceMap.set(scope as Scope<unknown>, scopeInstance);
|
|
173
612
|
}
|
|
174
|
-
|
|
175
|
-
return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
|
|
613
|
+
return scopeInstance.value as T;
|
|
176
614
|
}
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
addDep(d);
|
|
188
|
-
if (map.has(d)) return map.get(d)!.value as T;
|
|
189
|
-
const instance = getInstance(d);
|
|
190
|
-
map.set(d, instance);
|
|
191
|
-
return instance.value as T;
|
|
192
|
-
}) 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);
|
|
193
625
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const useBunja = getUse(
|
|
200
|
-
bunjaInstanceMap,
|
|
201
|
-
(dep) => this.#bakingContext!.currentBunja.addParent(dep),
|
|
202
|
-
(dep) => {
|
|
203
|
-
if (dep.baked) {
|
|
204
|
-
for (const scope of dep.relatedScopes) useScope(scope);
|
|
205
|
-
}
|
|
206
|
-
return this.#getBunjaInstance(dep, scopeInstanceMap);
|
|
207
|
-
},
|
|
626
|
+
const resolved = this.#resolveBunjaRef(
|
|
627
|
+
graphRef,
|
|
628
|
+
frame.readScope,
|
|
629
|
+
frame.inProgressBunjas,
|
|
630
|
+
bunjaRef.seed,
|
|
208
631
|
);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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),
|
|
213
663
|
};
|
|
214
|
-
|
|
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);
|
|
215
682
|
try {
|
|
216
|
-
this
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
});
|
|
219
707
|
} finally {
|
|
220
|
-
|
|
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);
|
|
221
772
|
}
|
|
773
|
+
return this.#prebakeBunjaRef(
|
|
774
|
+
bunjaRef,
|
|
775
|
+
frame.readScope,
|
|
776
|
+
frame.prebakeContext,
|
|
777
|
+
);
|
|
222
778
|
}
|
|
223
|
-
#
|
|
224
|
-
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,
|
|
225
789
|
scopeInstanceMap: ScopeInstanceMap,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
};
|
|
235
|
-
bunjaFn.fork = (b, scopeValuePairs) => {
|
|
236
|
-
const readScope = createReadScopeFn(scopeValuePairs, bunjaFn.use);
|
|
237
|
-
const { value, mount } = this.get(b, readScope);
|
|
238
|
-
bunjaFn.effect(mount);
|
|
239
|
-
return value;
|
|
240
|
-
};
|
|
241
|
-
if (this.#bakingContext) this.#bakingContext.currentBunja = bunja;
|
|
242
|
-
if (bunja.baked) {
|
|
243
|
-
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
244
|
-
if (id in this.#bunjas) return this.#bunjas[id];
|
|
245
|
-
return this.wrapInstance((dispose) => {
|
|
246
|
-
const value = bunja.init();
|
|
247
|
-
return this.#createBunjaInstance(id, value, effects, dispose);
|
|
248
|
-
});
|
|
249
|
-
} else {
|
|
250
|
-
return this.wrapInstance((dispose) => {
|
|
251
|
-
const value = bunja.init();
|
|
252
|
-
bunja.bake();
|
|
253
|
-
const id = bunja.calcInstanceId(scopeInstanceMap);
|
|
254
|
-
return this.#createBunjaInstance(id, value, effects, dispose);
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
} finally {
|
|
258
|
-
bunjaFn.effect = originalEffect;
|
|
259
|
-
bunjaFn.fork = originalFork;
|
|
260
|
-
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
|
+
);
|
|
261
798
|
}
|
|
262
799
|
}
|
|
263
800
|
#getScopeInstance(scope: Scope<unknown>, value: unknown): ScopeInstance {
|
|
@@ -281,27 +818,38 @@ export class BunjaStore {
|
|
|
281
818
|
}
|
|
282
819
|
#createBunjaInstance(
|
|
283
820
|
id: string,
|
|
821
|
+
baseId: string,
|
|
284
822
|
value: unknown,
|
|
823
|
+
dependencyMounts: (() => () => void)[],
|
|
285
824
|
effects: BunjaEffectCallback[],
|
|
825
|
+
recipe: BunjaInstanceRecipe,
|
|
286
826
|
dispose: () => void,
|
|
287
827
|
): BunjaInstance {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
);
|
|
304
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);
|
|
305
853
|
if (__DEV__) {
|
|
306
854
|
devtoolsGlobalHook.emit("bunjaInstanceMounted", {
|
|
307
855
|
storeId: this.id,
|
|
@@ -348,6 +896,11 @@ export interface BunjaStoreGetResult<T> {
|
|
|
348
896
|
bunjaInstance?: BunjaInstance;
|
|
349
897
|
}
|
|
350
898
|
|
|
899
|
+
export interface BunjaStorePrebakeResult {
|
|
900
|
+
relatedBunjas: Bunja<any, any>[];
|
|
901
|
+
requiredScopes: Scope<unknown>[];
|
|
902
|
+
}
|
|
903
|
+
|
|
351
904
|
export function delayUnmount(
|
|
352
905
|
mount: () => () => void,
|
|
353
906
|
ms: number = 0,
|
|
@@ -358,12 +911,20 @@ export function delayUnmount(
|
|
|
358
911
|
};
|
|
359
912
|
}
|
|
360
913
|
|
|
361
|
-
export class Bunja<T> {
|
|
914
|
+
export class Bunja<T, Seed = NoSeed> {
|
|
362
915
|
private static counter: number = 0;
|
|
363
916
|
readonly id: string = String(Bunja.counter++);
|
|
364
917
|
debugLabel: string = "";
|
|
365
|
-
#phase: BunjaPhase = {
|
|
366
|
-
|
|
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
|
+
) {
|
|
367
928
|
if (__DEV__) {
|
|
368
929
|
devtoolsGlobalHook.bunjas[this.id] = this;
|
|
369
930
|
devtoolsGlobalHook.emit("bunjaCreated", { bunjaId: this.id });
|
|
@@ -372,21 +933,44 @@ export class Bunja<T> {
|
|
|
372
933
|
get baked(): boolean {
|
|
373
934
|
return this.#phase.baked;
|
|
374
935
|
}
|
|
375
|
-
get
|
|
376
|
-
|
|
377
|
-
|
|
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
|
+
);
|
|
378
945
|
}
|
|
379
|
-
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[] {
|
|
380
953
|
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
381
|
-
return this.#phase.
|
|
954
|
+
return this.#phase.expandedRequiredBunjas;
|
|
382
955
|
}
|
|
383
|
-
get
|
|
956
|
+
get relatedBunjas(): AnyBunja[] {
|
|
384
957
|
if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
|
|
385
|
-
return
|
|
958
|
+
return toposortRelatedBunjas([
|
|
959
|
+
...this.requiredBunjas,
|
|
960
|
+
...this.optionalBunjas,
|
|
961
|
+
]);
|
|
386
962
|
}
|
|
387
|
-
|
|
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 {
|
|
388
968
|
if (this.#phase.baked) return;
|
|
389
|
-
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);
|
|
390
974
|
}
|
|
391
975
|
addScope(scope: Scope<unknown>): void {
|
|
392
976
|
if (this.#phase.baked) return;
|
|
@@ -395,22 +979,40 @@ export class Bunja<T> {
|
|
|
395
979
|
bake(): void {
|
|
396
980
|
if (this.#phase.baked) throw new Error("Bunja is already baked.");
|
|
397
981
|
const scopes = this.#phase.scopes;
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
};
|
|
407
999
|
}
|
|
408
|
-
|
|
409
|
-
|
|
1000
|
+
calcBaseInstanceId(
|
|
1001
|
+
scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>,
|
|
1002
|
+
): string {
|
|
1003
|
+
const scopeInstanceIds = this.requiredScopes.map(
|
|
410
1004
|
(scope) => scopeInstanceMap.get(scope)!.id,
|
|
411
1005
|
);
|
|
412
1006
|
return `${this.id}:${scopeInstanceIds.join(",")}`;
|
|
413
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
|
+
}
|
|
414
1016
|
toString(): string {
|
|
415
1017
|
const { id, debugLabel } = this;
|
|
416
1018
|
return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
|
|
@@ -421,15 +1023,17 @@ type BunjaPhase = BunjaPhaseUnbaked | BunjaPhaseBaked;
|
|
|
421
1023
|
|
|
422
1024
|
interface BunjaPhaseUnbaked {
|
|
423
1025
|
readonly baked: false;
|
|
424
|
-
readonly
|
|
1026
|
+
readonly requiredBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1027
|
+
readonly optionalBunjaRefs: AnyNormalizedBunjaRef[];
|
|
425
1028
|
readonly scopes: Set<Scope<unknown>>;
|
|
426
1029
|
}
|
|
427
1030
|
|
|
428
1031
|
interface BunjaPhaseBaked {
|
|
429
1032
|
readonly baked: true;
|
|
430
|
-
readonly
|
|
431
|
-
readonly
|
|
432
|
-
readonly
|
|
1033
|
+
readonly requiredBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1034
|
+
readonly optionalBunjaRefs: AnyNormalizedBunjaRef[];
|
|
1035
|
+
readonly expandedRequiredBunjas: AnyBunja[];
|
|
1036
|
+
readonly requiredScopes: Scope<unknown>[];
|
|
433
1037
|
}
|
|
434
1038
|
|
|
435
1039
|
export class Scope<T> {
|
|
@@ -474,22 +1078,39 @@ abstract class RefCounter {
|
|
|
474
1078
|
|
|
475
1079
|
class BunjaInstance extends RefCounter {
|
|
476
1080
|
#cleanup: (() => void) | undefined;
|
|
1081
|
+
#disposed = false;
|
|
477
1082
|
constructor(
|
|
478
1083
|
public readonly id: string,
|
|
1084
|
+
public readonly baseId: string,
|
|
479
1085
|
public readonly value: unknown,
|
|
480
|
-
|
|
1086
|
+
private readonly dependencyMounts: (() => () => void)[],
|
|
1087
|
+
private readonly effects: BunjaEffectCallback[],
|
|
1088
|
+
public readonly recipe: BunjaInstanceRecipe,
|
|
481
1089
|
private readonly _dispose: () => void,
|
|
482
1090
|
) {
|
|
483
1091
|
super();
|
|
484
1092
|
}
|
|
485
1093
|
override dispose(): void {
|
|
1094
|
+
if (this.#disposed) return;
|
|
1095
|
+
this.#disposed = true;
|
|
486
1096
|
this.#cleanup?.();
|
|
1097
|
+
this.#cleanup = undefined;
|
|
487
1098
|
this._dispose();
|
|
488
1099
|
}
|
|
489
1100
|
override add(): void {
|
|
490
|
-
this.#cleanup ??= this
|
|
1101
|
+
this.#cleanup ??= this.#mount();
|
|
491
1102
|
super.add();
|
|
492
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
|
+
}
|
|
493
1114
|
}
|
|
494
1115
|
|
|
495
1116
|
class ScopeInstance extends RefCounter {
|
|
@@ -503,28 +1124,33 @@ class ScopeInstance extends RefCounter {
|
|
|
503
1124
|
}
|
|
504
1125
|
}
|
|
505
1126
|
|
|
506
|
-
|
|
507
|
-
parents: Toposortable[];
|
|
508
|
-
}
|
|
509
|
-
function toposort<T extends Toposortable>(nodes: T[]): T[] {
|
|
1127
|
+
function toposort<T>(nodes: T[], getDependencies: (node: T) => T[]): T[] {
|
|
510
1128
|
const visited = new Set<T>();
|
|
511
1129
|
const result: T[] = [];
|
|
512
1130
|
function visit(current: T) {
|
|
513
1131
|
if (visited.has(current)) return;
|
|
514
1132
|
visited.add(current);
|
|
515
|
-
for (const
|
|
1133
|
+
for (const dependency of getDependencies(current)) visit(dependency);
|
|
516
1134
|
result.push(current);
|
|
517
1135
|
}
|
|
518
1136
|
for (const node of nodes) visit(node);
|
|
519
1137
|
return result;
|
|
520
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
|
+
}
|
|
521
1148
|
|
|
522
1149
|
const noop = () => {};
|
|
523
1150
|
|
|
524
1151
|
export interface BunjaDevtoolsGlobalHook {
|
|
525
|
-
bunjas: Record<string, Bunja<any>>;
|
|
1152
|
+
bunjas: Record<string, Bunja<any, any>>;
|
|
526
1153
|
scopes: Record<string, Scope<any>>;
|
|
527
|
-
stores: Record<string, BunjaStore>;
|
|
528
1154
|
listeners: Record<
|
|
529
1155
|
BunjaDevtoolsEventType,
|
|
530
1156
|
Set<(event: any) => void>
|
|
@@ -564,7 +1190,6 @@ if (__DEV__) {
|
|
|
564
1190
|
devtoolsGlobalHook = (globalThis as any).__BUNJA_DEVTOOLS_GLOBAL_HOOK__;
|
|
565
1191
|
} else {
|
|
566
1192
|
devtoolsGlobalHook = {
|
|
567
|
-
stores: {},
|
|
568
1193
|
bunjas: {},
|
|
569
1194
|
scopes: {},
|
|
570
1195
|
listeners: {
|