bunja 0.1.0 → 2.0.0-alpha.1

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 CHANGED
@@ -1,3 +1,10 @@
1
+ > [!WARNING]
2
+ > You are viewing the `v2` branch.
3
+ >
4
+ > The current stable version is `1.x.x`.\
5
+ > If you want to view the code for that version,
6
+ > please [switch to the `v1` branch](https://github.com/disjukr/bunja/tree/v1).
7
+
1
8
  # Bunja
2
9
 
3
10
  Bunja is lightweight State Lifetime Manager.\
@@ -33,21 +40,21 @@ You can use `bunja` to define a state with a finite lifetime and use the `useBun
33
40
  You can define a bunja using the `bunja` function. When you access the defined bunja with the `useBunja` hook, a bunja instance is created.\
34
41
  If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed.
35
42
 
36
- If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `bunja.effect` field.
43
+ If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `bunja.effect` function.
37
44
 
38
45
  ```ts
39
46
  import { bunja } from "bunja";
40
47
  import { useBunja } from "bunja/react";
41
48
 
42
- const countBunja = bunja([], () => {
49
+ const countBunja = bunja(() => {
43
50
  const countAtom = atom(0);
44
- return {
45
- countAtom,
46
- [bunja.effect]() {
47
- console.log("mounted");
48
- return () => console.log("unmounted");
49
- },
50
- };
51
+
52
+ bunja.effect(() => {
53
+ console.log("mounted");
54
+ return () => console.log("unmounted");
55
+ });
56
+
57
+ return { countAtom };
51
58
  });
52
59
 
53
60
  function MyComponent() {
@@ -67,7 +74,7 @@ In such a case, you can write the following code.
67
74
 
68
75
  ```ts
69
76
  // To simplify the example, code for buffering and reconnection has been omitted.
70
- const websocketBunja = bunja([], () => {
77
+ const websocketBunja = bunja(() => {
71
78
  let socket;
72
79
  const send = (message) => socket.send(JSON.stringify(message));
73
80
 
@@ -77,35 +84,35 @@ const websocketBunja = bunja([], () => {
77
84
  return () => emitter.off("message", handler);
78
85
  };
79
86
 
80
- return {
81
- send,
82
- on,
83
- [bunja.effect]() {
84
- socket = new WebSocket("...");
85
- socket.onmessage = (e) => emitter.emit("message", JSON.parse(e.data));
86
- return () => socket.close();
87
- },
88
- };
87
+ bunja.effect(() => {
88
+ socket = new WebSocket("...");
89
+ socket.onmessage = (e) => emitter.emit("message", JSON.parse(e.data));
90
+ return () => socket.close();
91
+ });
92
+
93
+ return { send, on };
89
94
  });
90
95
 
91
- const resourceFooBunja = bunja([websocketBunja], ({ send, on }) => {
96
+ const resourceFooBunja = bunja(() => {
97
+ const { send, on } = bunja.use(websocketBunja);
92
98
  const resourceFooAtom = atom();
93
- return {
94
- resourceFooAtom,
95
- [bunja.effect]() {
96
- const off = on((message) => {
97
- if (message.type === "foo") store.set(resourceAtom, message.value);
98
- });
99
- send("subscribe-foo");
100
- return () => {
101
- send("unsubscribe-foo");
102
- off();
103
- };
104
- },
105
- };
99
+
100
+ bunja.effect(() => {
101
+ const off = on((message) => {
102
+ if (message.type === "foo") store.set(resourceAtom, message.value);
103
+ });
104
+ send("subscribe-foo");
105
+ return () => {
106
+ send("unsubscribe-foo");
107
+ off();
108
+ };
109
+ });
110
+
111
+ return { resourceFooAtom };
106
112
  });
107
113
 
108
- const resourceBarBunja = bunja([websocketBunja], ({ send, on }) => {
114
+ const resourceBarBunja = bunja(() => {
115
+ const { send, on } = bunja.use(websocketBunja);
109
116
  const resourceBarAtom = atom();
110
117
  // ...
111
118
  });
@@ -144,11 +151,14 @@ import { bunja, createScope } from "bunja";
144
151
 
145
152
  const UrlScope = createScope();
146
153
 
147
- const fetchBunja = bunja([UrlScope], (url) => {
154
+ const fetchBunja = bunja(() => {
155
+ const url = bunja.use(UrlScope);
156
+
148
157
  const queryAtom = atomWithQuery((get) => ({
149
158
  queryKey: [url],
150
159
  queryFn: async () => (await fetch(url)).json(),
151
160
  }));
161
+
152
162
  return { queryAtom };
153
163
  });
154
164
  ```
@@ -168,23 +178,26 @@ const UrlContext = createContext("https://example.com/");
168
178
  const UrlScope = createScope();
169
179
  bindScope(UrlScope, UrlContext);
170
180
 
171
- const fetchBunja = bunja([UrlScope], (url) => {
181
+ const fetchBunja = bunja(() => {
182
+ const url = bunja.use(UrlScope);
183
+
172
184
  const queryAtom = atomWithQuery((get) => ({
173
185
  queryKey: [url],
174
186
  queryFn: async () => (await fetch(url)).json(),
175
187
  }));
188
+
176
189
  return { queryAtom };
177
190
  });
178
191
 
179
192
  function ParentComponent() {
180
193
  return (
181
194
  <>
182
- <UrlContext.Provider value="https://example.com/foo">
195
+ <UrlContext value="https://example.com/foo">
183
196
  <ChildComponent />
184
- </UrlContext.Provider>
185
- <UrlContext.Provider value="https://example.com/bar">
197
+ </UrlContext>
198
+ <UrlContext value="https://example.com/bar">
186
199
  <ChildComponent />
187
- </UrlContext.Provider>
200
+ </UrlContext>
188
201
  </>
189
202
  );
190
203
  }
package/bunja.ts CHANGED
@@ -1,231 +1,353 @@
1
- export type Dep<T> = Bunja<T> | Scope<T>;
1
+ export interface BunjaFn {
2
+ <T>(init: () => T): Bunja<T>;
3
+ use: BunjaUseFn;
4
+ effect: BunjaEffectFn;
5
+ }
6
+ export const bunja: BunjaFn = bunjaFn;
7
+ function bunjaFn<T>(init: () => T): Bunja<T> {
8
+ return new Bunja(init);
9
+ }
10
+ bunjaFn.use = invalidUse as BunjaUseFn;
11
+ bunjaFn.effect = invalidEffect as BunjaEffectFn;
2
12
 
3
- const bunjaEffectSymbol: unique symbol = Symbol("Bunja.effect");
4
- type BunjaEffectSymbol = typeof bunjaEffectSymbol;
13
+ export type BunjaUseFn = <T>(dep: Dep<T>) => T;
14
+ export type BunjaEffectFn = (callback: BunjaEffectCallback) => void;
15
+ export type BunjaEffectCallback = () => (() => void) | void;
5
16
 
6
- export class Bunja<T> {
7
- public static readonly bunjas: Bunja<any>[] = [];
8
- public readonly id: number;
9
- public debugLabel: string = "";
10
- constructor(
11
- public deps: Dep<any>[], // one depth dependencies
12
- public parents: Bunja<any>[], // one depth parents
13
- public relatedBunjas: Bunja<any>[], // toposorted parents without self
14
- public relatedScopes: Scope<any>[], // deduped
15
- public init: (...args: any[]) => T & BunjaValue,
16
- ) {
17
- this.id = Bunja.bunjas.length;
18
- Bunja.bunjas.push(this);
19
- }
20
- static readonly effect: BunjaEffectSymbol = bunjaEffectSymbol;
21
- toString(): string {
22
- const { id, debugLabel } = this;
23
- return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
24
- }
17
+ export function createScope<T>(hash?: HashFn<T>): Scope<T> {
18
+ return new Scope(hash);
25
19
  }
26
20
 
27
- export class Scope<T> {
28
- public static readonly scopes: Scope<any>[] = [];
29
- public readonly id: number;
30
- public debugLabel: string = "";
31
- constructor() {
32
- this.id = Scope.scopes.length;
33
- Scope.scopes.push(this);
34
- }
35
- toString(): string {
36
- const { id, debugLabel } = this;
37
- return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
38
- }
21
+ export function createBunjaStore(): BunjaStore {
22
+ return new BunjaStore();
39
23
  }
40
24
 
41
- export type ReadScope = <T>(scope: Scope<T>) => T;
25
+ export type Dep<T> = Bunja<T> | Scope<T>;
26
+
27
+ function invalidUse() {
28
+ throw new Error(
29
+ "`bunja.use` can only be used inside a bunja init function.",
30
+ );
31
+ }
32
+ function invalidEffect() {
33
+ throw new Error(
34
+ "`bunja.effect` can only be used inside a bunja init function.",
35
+ );
36
+ }
37
+
38
+ interface BunjaStoreGetContext {
39
+ bunjaInstance: BunjaInstance;
40
+ bunjaInstanceMap: BunjaInstanceMap;
41
+ scopeInstanceMap: ScopeInstanceMap;
42
+ }
43
+
44
+ type BunjaInstanceMap = Map<Bunja<unknown>, BunjaInstance>;
45
+ type ScopeInstanceMap = Map<Scope<unknown>, ScopeInstance>;
46
+
47
+ interface BunjaBakingContext {
48
+ currentBunja: Bunja<unknown>;
49
+ }
42
50
 
43
51
  export class BunjaStore {
44
52
  #bunjas: Record<string, BunjaInstance> = {};
45
- #scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
46
- get<T>(
47
- bunja: Bunja<T>,
48
- readScope: ReadScope,
49
- ): {
50
- value: T;
51
- mount: () => void;
52
- deps: any[];
53
- } {
53
+ #scopes: Map<Scope<unknown>, Map<unknown, ScopeInstance>> = new Map();
54
+ #bakingContext: BunjaBakingContext | undefined;
55
+ dispose(): void {
56
+ for (const instance of Object.values(this.#bunjas)) instance.dispose();
57
+ for (const instanceMap of Object.values(this.#scopes)) {
58
+ for (const instance of instanceMap.values()) instance.dispose();
59
+ }
60
+ this.#bunjas = {};
61
+ this.#scopes = new Map();
62
+ }
63
+ get<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetResult<T> {
64
+ const originalUse = bunjaFn.use;
65
+ try {
66
+ const { bunjaInstance, bunjaInstanceMap, scopeInstanceMap } = bunja.baked
67
+ ? this.#getBaked(bunja, readScope)
68
+ : this.#getUnbaked(bunja, readScope);
69
+ return {
70
+ value: bunjaInstance.value as T,
71
+ mount: () => {
72
+ bunjaInstanceMap.forEach((instance) => instance.add());
73
+ bunjaInstance.add();
74
+ scopeInstanceMap.forEach((instance) => instance.add());
75
+ const unmount = () => {
76
+ setTimeout(() => {
77
+ bunjaInstanceMap.forEach((instance) => instance.sub());
78
+ bunjaInstance.sub();
79
+ scopeInstanceMap.forEach((instance) => instance.sub());
80
+ });
81
+ };
82
+ return unmount;
83
+ },
84
+ deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
85
+ };
86
+ } finally {
87
+ bunjaFn.use = originalUse;
88
+ }
89
+ }
90
+ #getBaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
54
91
  const scopeInstanceMap = new Map(
55
92
  bunja.relatedScopes.map((scope) => [
56
93
  scope,
57
94
  this.#getScopeInstance(scope, readScope(scope)),
58
95
  ]),
59
96
  );
60
- const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
61
- const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
62
- return {
63
- value: bunjaInstance.value as T,
64
- mount() {
65
- relatedBunjaInstanceMap.forEach((related) => related.add());
66
- bunjaInstance.add();
67
- scopeInstanceMap.forEach((scope) => scope.add());
68
- return function unmount(): void {
69
- // concern: reverse order?
70
- relatedBunjaInstanceMap.forEach((related) => related.sub());
71
- bunjaInstance.sub();
72
- scopeInstanceMap.forEach((scope) => scope.sub());
73
- };
74
- },
75
- deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
76
- };
77
- }
78
- #getBunjaInstance(
79
- bunja: Bunja<any>,
80
- scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
81
- ): BunjaInstance {
82
- const localScopeInstanceMap = new Map(
83
- bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
84
- );
85
- const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
86
- .map(({ instanceId }) => instanceId)
87
- .sort((a, b) => a - b);
88
- const bunjaInstanceId = `${bunja.id}:${scopeInstanceIds.join(",")}`;
89
- if (this.#bunjas[bunjaInstanceId]) return this.#bunjas[bunjaInstanceId];
90
- const relatedBunjaInstanceMap = new Map(
97
+ const bunjaInstanceMap = new Map(
91
98
  bunja.relatedBunjas.map((relatedBunja) => [
92
99
  relatedBunja,
93
100
  this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
94
101
  ]),
95
102
  );
96
- const args = bunja.deps.map((dep) => {
97
- if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
98
- if (dep instanceof Scope) return localScopeInstanceMap.get(dep)!.value;
99
- throw new Error("Invalid dependency");
100
- });
101
- const bunjaInstance = new BunjaInstance(
102
- () => delete this.#bunjas[bunjaInstanceId],
103
- bunjaInstanceId,
104
- relatedBunjaInstanceMap,
105
- bunja.init.apply(bunja, args),
103
+ bunjaFn.use = <T>(dep: Dep<T>) => {
104
+ if (dep instanceof Bunja) {
105
+ return bunjaInstanceMap.get(dep as Bunja<unknown>)!.value as T;
106
+ }
107
+ if (dep instanceof Scope) {
108
+ return scopeInstanceMap.get(dep as Scope<unknown>)!.value as T;
109
+ }
110
+ throw new Error("`bunja.use` can only be used with Bunja or Scope.");
111
+ };
112
+ const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
113
+ return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
114
+ }
115
+ #getUnbaked<T>(bunja: Bunja<T>, readScope: ReadScope): BunjaStoreGetContext {
116
+ const bunjaInstanceMap: BunjaInstanceMap = new Map();
117
+ const scopeInstanceMap: ScopeInstanceMap = new Map();
118
+ function getUse<D extends Dep<unknown>, I extends { value: unknown }>(
119
+ map: Map<D, I>,
120
+ addDep: (D: D) => void,
121
+ getInstance: (dep: D) => I,
122
+ ) {
123
+ return ((dep) => {
124
+ const d = dep as D;
125
+ addDep(d);
126
+ if (map.has(d)) return map.get(d)!.value as T;
127
+ const instance = getInstance(d);
128
+ map.set(d, instance);
129
+ return instance.value as T;
130
+ }) as <T>(dep: Dep<T>) => T;
131
+ }
132
+ const useScope = getUse(
133
+ scopeInstanceMap,
134
+ (dep) => this.#bakingContext!.currentBunja.addScope(dep),
135
+ (dep) => this.#getScopeInstance(dep, readScope(dep)),
136
+ );
137
+ const useBunja = getUse(
138
+ bunjaInstanceMap,
139
+ (dep) => this.#bakingContext!.currentBunja.addParent(dep),
140
+ (dep) => {
141
+ if (dep.baked) {
142
+ for (const scope of dep.relatedScopes) useScope(scope);
143
+ }
144
+ return this.#getBunjaInstance(dep, scopeInstanceMap);
145
+ },
106
146
  );
107
- this.#bunjas[bunjaInstanceId] = bunjaInstance;
108
- return bunjaInstance;
147
+ bunjaFn.use = <T>(dep: Dep<T>) => {
148
+ if (dep instanceof Bunja) return useBunja(dep) as T;
149
+ if (dep instanceof Scope) return useScope(dep) as T;
150
+ throw new Error("`bunja.use` can only be used with Bunja or Scope.");
151
+ };
152
+ try {
153
+ this.#bakingContext = { currentBunja: bunja };
154
+ const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
155
+ return { bunjaInstance, bunjaInstanceMap, scopeInstanceMap };
156
+ } finally {
157
+ this.#bakingContext = undefined;
158
+ }
159
+ }
160
+ #getBunjaInstance<T>(
161
+ bunja: Bunja<T>,
162
+ scopeInstanceMap: ScopeInstanceMap,
163
+ ): BunjaInstance {
164
+ const originalEffect = bunjaFn.effect;
165
+ const prevBunja = this.#bakingContext?.currentBunja;
166
+ try {
167
+ const effects: BunjaEffectCallback[] = [];
168
+ bunjaFn.effect = (callback: BunjaEffectCallback) => {
169
+ effects.push(callback);
170
+ };
171
+ if (this.#bakingContext) this.#bakingContext.currentBunja = bunja;
172
+ if (bunja.baked) {
173
+ const id = bunja.calcInstanceId(scopeInstanceMap);
174
+ if (id in this.#bunjas) return this.#bunjas[id];
175
+ const bunjaInstanceValue = bunja.init();
176
+ return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
177
+ } else {
178
+ const bunjaInstanceValue = bunja.init();
179
+ bunja.bake();
180
+ const id = bunja.calcInstanceId(scopeInstanceMap);
181
+ return this.#createBunjaInstance(id, bunjaInstanceValue, effects);
182
+ }
183
+ } finally {
184
+ bunjaFn.effect = originalEffect;
185
+ if (this.#bakingContext) this.#bakingContext.currentBunja = prevBunja!;
186
+ }
109
187
  }
110
- #getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
111
- const scopeInstanceMap = this.#scopes.get(scope) ??
188
+ #getScopeInstance(scope: Scope<unknown>, value: unknown): ScopeInstance {
189
+ const key = scope.hash(value);
190
+ const instanceMap = this.#scopes.get(scope) ??
112
191
  this.#scopes.set(scope, new Map()).get(scope)!;
113
- const init = () =>
114
- new ScopeInstance(
115
- () => scopeInstanceMap.delete(value),
116
- ScopeInstance.counter++,
117
- scope,
118
- value,
119
- );
120
- return (
121
- scopeInstanceMap.get(value) ??
122
- scopeInstanceMap.set(value, init()).get(value)!
192
+ return instanceMap.get(key) ??
193
+ instanceMap.set(
194
+ key,
195
+ new ScopeInstance(value, () => instanceMap.delete(key)),
196
+ ).get(key)!;
197
+ }
198
+ #createBunjaInstance(
199
+ id: string,
200
+ value: unknown,
201
+ effects: BunjaEffectCallback[],
202
+ ): BunjaInstance {
203
+ const effect = () => {
204
+ const cleanups = effects
205
+ .map((effect) => effect())
206
+ .filter(Boolean) as (() => void)[];
207
+ return () => cleanups.forEach((cleanup) => cleanup());
208
+ };
209
+ const dispose = () => delete this.#bunjas[id];
210
+ const bunjaInstance = new BunjaInstance(id, value, effect, dispose);
211
+ this.#bunjas[id] = bunjaInstance;
212
+ return bunjaInstance;
213
+ }
214
+ }
215
+
216
+ export type ReadScope = <T>(scope: Scope<T>) => T;
217
+
218
+ export interface BunjaStoreGetResult<T> {
219
+ value: T;
220
+ mount: () => () => void;
221
+ deps: unknown[];
222
+ }
223
+
224
+ export class Bunja<T> {
225
+ private static counter: number = 0;
226
+ readonly id: string = String(Bunja.counter++);
227
+ debugLabel: string = "";
228
+ #phase: BunjaPhase = { baked: false, parents: new Set(), scopes: new Set() };
229
+ constructor(public init: () => T) {}
230
+ get baked(): boolean {
231
+ return this.#phase.baked;
232
+ }
233
+ get parents(): Bunja<unknown>[] {
234
+ if (this.#phase.baked) return this.#phase.parents;
235
+ return Array.from(this.#phase.parents);
236
+ }
237
+ get relatedBunjas(): Bunja<unknown>[] {
238
+ if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
239
+ return this.#phase.relatedBunjas;
240
+ }
241
+ get relatedScopes(): Scope<unknown>[] {
242
+ if (!this.#phase.baked) throw new Error("Bunja is not baked yet.");
243
+ return this.#phase.relatedScopes;
244
+ }
245
+ addParent(bunja: Bunja<unknown>): void {
246
+ if (this.#phase.baked) return;
247
+ this.#phase.parents.add(bunja);
248
+ }
249
+ addScope(scope: Scope<unknown>): void {
250
+ if (this.#phase.baked) return;
251
+ this.#phase.scopes.add(scope);
252
+ }
253
+ bake(): void {
254
+ if (this.#phase.baked) throw new Error("Bunja is already baked.");
255
+ const scopes = this.#phase.scopes;
256
+ const parents = this.parents;
257
+ const relatedBunjas = toposort(parents);
258
+ const relatedScopes = Array.from(
259
+ new Set([
260
+ ...relatedBunjas.flatMap((bunja) => bunja.relatedScopes),
261
+ ...scopes,
262
+ ]),
263
+ );
264
+ this.#phase = { baked: true, parents, relatedBunjas, relatedScopes };
265
+ }
266
+ calcInstanceId(scopeInstanceMap: Map<Scope<unknown>, ScopeInstance>): string {
267
+ const scopeInstanceIds = this.relatedScopes.map(
268
+ (scope) => scopeInstanceMap.get(scope)!.id,
123
269
  );
270
+ return `${this.id}:${scopeInstanceIds.join(",")}`;
271
+ }
272
+ toString(): string {
273
+ const { id, debugLabel } = this;
274
+ return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
124
275
  }
125
276
  }
126
277
 
127
- export const createBunjaStore = (): BunjaStore => new BunjaStore();
278
+ type BunjaPhase = BunjaPhaseUnbaked | BunjaPhaseBaked;
128
279
 
129
- export type BunjaEffectFn = () => () => void;
130
- export interface BunjaValue {
131
- [Bunja.effect]?: BunjaEffectFn;
280
+ interface BunjaPhaseUnbaked {
281
+ readonly baked: false;
282
+ readonly parents: Set<Bunja<unknown>>;
283
+ readonly scopes: Set<Scope<unknown>>;
132
284
  }
133
285
 
134
- function bunjaImpl<T, const U extends any[]>(
135
- deps: { [K in keyof U]: Dep<U[K]> },
136
- init: (...args: U) => T & BunjaValue,
137
- ): Bunja<T> {
138
- const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
139
- const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
140
- const relatedBunjas = toposort(parents);
141
- const relatedScopes = Array.from(
142
- new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
143
- );
144
- return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
145
- }
146
- bunjaImpl.effect = Bunja.effect;
147
-
148
- export const bunja: {
149
- <T>(deps: [], init: () => T & BunjaValue): Bunja<T>;
150
- <T, U>(deps: [Dep<U>], init: (u: U) => T & BunjaValue): Bunja<T>;
151
- <T, U, V>(
152
- deps: [Dep<U>, Dep<V>],
153
- init: (u: U, v: V) => T & BunjaValue,
154
- ): Bunja<T>;
155
- <T, U, V, W>(
156
- deps: [Dep<U>, Dep<V>, Dep<W>],
157
- init: (u: U, v: V, w: W) => T & BunjaValue,
158
- ): Bunja<T>;
159
- <T, U, V, W, X>(
160
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
161
- init: (u: U, v: V, w: W, x: X) => T & BunjaValue,
162
- ): Bunja<T>;
163
- <T, U, V, W, X, Y>(
164
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
165
- init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue,
166
- ): Bunja<T>;
167
- <T, U, V, W, X, Y, Z>(
168
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
169
- init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue,
170
- ): Bunja<T>;
171
- readonly effect: BunjaEffectSymbol;
172
- } = bunjaImpl;
173
-
174
- export function createScope<T>(): Scope<T> {
175
- return new Scope();
286
+ interface BunjaPhaseBaked {
287
+ readonly baked: true;
288
+ readonly parents: Bunja<unknown>[];
289
+ readonly relatedBunjas: Bunja<unknown>[];
290
+ readonly relatedScopes: Scope<unknown>[];
176
291
  }
177
292
 
178
- abstract class RefCounter {
179
- #disposed = false;
180
- #count = 0;
181
- add() {
182
- this.#count++;
183
- }
184
- sub() {
185
- this.#count--;
186
- setTimeout(() => {
187
- if (this.#disposed) return;
188
- if (this.#count < 1) {
189
- this.#disposed = true;
190
- this.dispose();
191
- }
192
- });
293
+ export class Scope<T> {
294
+ private static counter: number = 0;
295
+ readonly id: string = String(Scope.counter++);
296
+ debugLabel: string = "";
297
+ constructor(public readonly hash: HashFn<T> = Scope.identity) {}
298
+ private static identity<T>(x: T): T {
299
+ return x;
300
+ }
301
+ toString(): string {
302
+ const { id, debugLabel } = this;
303
+ return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
193
304
  }
194
- abstract dispose(): void;
195
305
  }
196
306
 
307
+ export type HashFn<T> = (value: T) => unknown;
308
+
197
309
  const noop = () => {};
310
+ abstract class RefCounter {
311
+ #count: number = 0;
312
+ abstract dispose(): void;
313
+ add(): void {
314
+ ++this.#count;
315
+ }
316
+ sub(): void {
317
+ --this.#count;
318
+ if (this.#count < 1) {
319
+ this.dispose();
320
+ this.dispose = noop;
321
+ }
322
+ }
323
+ }
324
+
198
325
  class BunjaInstance extends RefCounter {
199
326
  #cleanup: (() => void) | undefined;
200
- #dispose: () => void;
201
327
  constructor(
202
- dispose: () => void,
203
- public instanceId: string,
204
- public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
205
- public value: BunjaValue,
328
+ public readonly id: string,
329
+ public readonly value: unknown,
330
+ public readonly effect: BunjaEffectCallback,
331
+ private readonly _dispose: () => void,
206
332
  ) {
207
333
  super();
208
- this.#dispose = () => {
209
- this.#cleanup?.();
210
- dispose();
211
- };
212
334
  }
213
- override add() {
214
- this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
215
- super.add();
335
+ override dispose(): void {
336
+ this.#cleanup?.();
337
+ this._dispose();
216
338
  }
217
- dispose() {
218
- this.#dispose();
339
+ override add(): void {
340
+ this.#cleanup ??= this.effect() ?? noop;
341
+ super.add();
219
342
  }
220
343
  }
221
344
 
222
345
  class ScopeInstance extends RefCounter {
223
- public static counter = 0;
346
+ private static counter: number = 0;
347
+ readonly id: string = String(ScopeInstance.counter++);
224
348
  constructor(
225
- public dispose: () => void,
226
- public instanceId: number,
227
- public scope: Scope<any>,
228
- public value: any,
349
+ public readonly value: unknown,
350
+ public readonly dispose: () => void,
229
351
  ) {
230
352
  super();
231
353
  }