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