bunja 0.0.7 → 0.0.9

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
@@ -33,16 +33,16 @@ You can use `bunja` to define a state with a finite lifetime and use the `useBun
33
33
  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
34
  If all components in the render tree that refer to the bunja disappear, the bunja instance is automatically destroyed.
35
35
 
36
- If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `Bunja.effect` field.
36
+ If you want to trigger effects when the lifetime of a bunja starts and ends, you can use the `bunja.effect` field.
37
37
 
38
38
  ```ts
39
- import { bunja, Bunja, useBunja } from "bunja";
39
+ import { bunja, useBunja } from "bunja";
40
40
 
41
41
  const countBunja = bunja([], () => {
42
42
  const countAtom = atom(0);
43
43
  return {
44
44
  countAtom,
45
- [Bunja.effect]() {
45
+ [bunja.effect]() {
46
46
  console.log("mounted");
47
47
  return () => console.log("unmounted");
48
48
  },
package/bunja.ts CHANGED
@@ -1,33 +1,116 @@
1
- import * as React from "react";
2
-
3
- export type Dep<T> = React.Context<T> | Bunja<T>;
1
+ export type Dep<T> = Bunja<T> | Scope<T>;
4
2
 
5
3
  export class Bunja<T> {
4
+ public static readonly bunjas: Bunja<any>[] = [];
5
+ public readonly id: number;
6
+ public debugLabel: string = "";
6
7
  constructor(
7
- public id: number,
8
- public deps: Dep<any>[],
9
- public contexts: React.Context<any>[],
8
+ public deps: Dep<any>[], // one depth dependencies
9
+ public parents: Bunja<any>[], // one depth parents
10
+ public relatedBunjas: Bunja<any>[], // toposorted parents without self
11
+ public relatedScopes: Scope<any>[], // deduped
10
12
  public init: (...args: any[]) => T & BunjaValue
11
- ) {}
13
+ ) {
14
+ this.id = Bunja.bunjas.length;
15
+ Bunja.bunjas.push(this);
16
+ }
12
17
  static readonly effect = Symbol("Bunja.effect");
18
+ toString() {
19
+ return `[${this.debugLabel} Bunja:${this.id}]`;
20
+ }
21
+ }
22
+
23
+ export class Scope<T> {
24
+ public static readonly scopes: Scope<any>[] = [];
25
+ public readonly id: number;
26
+ constructor() {
27
+ this.id = Scope.scopes.length;
28
+ Scope.scopes.push(this);
29
+ }
30
+ toString() {
31
+ return this.id;
32
+ }
13
33
  }
14
34
 
35
+ export type ReadScope = <T>(scope: Scope<T>) => T;
36
+
15
37
  export class BunjaStore {
16
38
  #bunjas: Record<string, BunjaInstance> = {};
17
- get(bunja: Bunja<any>, biid: string, args: any[]) {
18
- return (this.#bunjas[biid] ??= new BunjaInstance(
19
- this,
20
- biid,
21
- bunja.init(...args)
22
- ));
39
+ #scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
40
+ get<T>(bunja: Bunja<T>, readScope: ReadScope) {
41
+ const scopeInstanceMap = new Map(
42
+ bunja.relatedScopes.map((scope) => [
43
+ scope,
44
+ this.#getScopeInstance(scope, readScope(scope)),
45
+ ])
46
+ );
47
+ const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
48
+ const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
49
+ return {
50
+ value: bunjaInstance.value as T,
51
+ effect() {
52
+ relatedBunjaInstanceMap.forEach((related) => related.add());
53
+ bunjaInstance.add();
54
+ scopeInstanceMap.forEach((scope) => scope.add());
55
+ return () => {
56
+ // concern: reverse order?
57
+ relatedBunjaInstanceMap.forEach((related) => related.sub());
58
+ bunjaInstance.sub();
59
+ scopeInstanceMap.forEach((scope) => scope.sub());
60
+ };
61
+ },
62
+ };
23
63
  }
24
- delete(biid: string) {
25
- delete this.#bunjas[biid];
64
+ #getBunjaInstance(
65
+ bunja: Bunja<any>,
66
+ scopeInstanceMap: Map<Scope<any>, ScopeInstance>
67
+ ): BunjaInstance {
68
+ const localScopeInstanceMap = new Map(
69
+ bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!])
70
+ );
71
+ const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
72
+ .map(({ instanceId }) => instanceId)
73
+ .sort((a, b) => a - b);
74
+ const bunjaInstanceId = `${bunja.id}:${scopeInstanceIds.join(",")}`;
75
+ if (this.#bunjas[bunjaInstanceId]) return this.#bunjas[bunjaInstanceId];
76
+ const relatedBunjaInstanceMap = new Map(
77
+ bunja.relatedBunjas.map((relatedBunja) => [
78
+ relatedBunja,
79
+ this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
80
+ ])
81
+ );
82
+ const args = bunja.deps.map((dep) => {
83
+ if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
84
+ if (dep instanceof Scope) return localScopeInstanceMap.get(dep)!.value;
85
+ throw new Error("Invalid dependency");
86
+ });
87
+ const bunjaInstance = new BunjaInstance(
88
+ () => delete this.#bunjas[bunjaInstanceId],
89
+ bunjaInstanceId,
90
+ relatedBunjaInstanceMap,
91
+ bunja.init.apply(bunja, args)
92
+ );
93
+ this.#bunjas[bunjaInstanceId] = bunjaInstance;
94
+ return bunjaInstance;
95
+ }
96
+ #getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
97
+ const scopeInstanceMap =
98
+ this.#scopes.get(scope) ?? this.#scopes.set(scope, new Map()).get(scope)!;
99
+ const init = () =>
100
+ new ScopeInstance(
101
+ () => scopeInstanceMap.delete(value),
102
+ ScopeInstance.counter++,
103
+ scope,
104
+ value
105
+ );
106
+ return (
107
+ scopeInstanceMap.get(value) ??
108
+ scopeInstanceMap.set(value, init()).get(value)!
109
+ );
26
110
  }
27
111
  }
28
112
 
29
113
  export const createBunjaStore = () => new BunjaStore();
30
- export const BunjaStoreContext = React.createContext(createBunjaStore());
31
114
 
32
115
  export type BunjaEffectFn = () => () => void;
33
116
  export interface BunjaValue {
@@ -63,55 +146,20 @@ export function bunja<T, const U extends any[]>(
63
146
  deps: { [K in keyof U]: Dep<U[K]> },
64
147
  init: (...args: U) => T & BunjaValue
65
148
  ): Bunja<T> {
66
- const contexts = deps.filter(
67
- (dep) => !(dep instanceof Bunja)
68
- ) as React.Context<any>[];
69
- const bunjas = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
70
- const dedupedContexts = Array.from(
71
- new Set([...contexts, ...bunjas.flatMap((def) => def.contexts)])
149
+ const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
150
+ const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
151
+ const relatedBunjas = toposort(parents);
152
+ const relatedScopes = Array.from(
153
+ new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)])
72
154
  );
73
- return new Bunja(bunja.counter++, deps, dedupedContexts, init as any);
155
+ return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
74
156
  }
75
- bunja.counter = 0;
157
+ bunja.effect = Bunja.effect;
76
158
 
77
- export function useBunja<T>(bunja: Bunja<T>): T {
78
- const { id, deps, contexts } = bunja;
79
- const store = React.useContext(BunjaStoreContext);
80
- const tuples = contexts.map((c) => [c, React.useContext(c)] as const);
81
- const scopes = tuples.map(([context, value]) => getScope(context, value));
82
- const scopeMap = new Map(tuples);
83
- const args = deps.map((dep) => {
84
- if (dep instanceof Bunja) return useBunja(dep);
85
- return scopeMap.get(dep);
86
- });
87
- const biid = `${id}:${scopes
88
- .map(({ id }) => id)
89
- .sort()
90
- .join(",")}`;
91
- const instance = store.get(bunja, biid, args);
92
- React.useEffect(() => {
93
- instance.add();
94
- return () => instance.sub();
95
- }, [instance]);
96
- React.useEffect(() => {
97
- scopes.forEach((scope) => scope.add());
98
- return () => scopes.forEach((scope) => scope.sub());
99
- }, scopes);
100
- return instance.value as T;
159
+ export function createScope<T>(): Scope<T> {
160
+ return new Scope();
101
161
  }
102
162
 
103
- const useRid = () => React.useState(() => useRid.counter++)[0];
104
- useRid.counter = 0;
105
-
106
- const scopes = new WeakMap<React.Context<any>, Map<any, Scope>>();
107
- function getScope(context: React.Context<any>, value: any) {
108
- const m = scopes.get(context) ?? scopes.set(context, new Map()).get(context)!;
109
- const init = () =>
110
- new Scope(() => m.delete(value), context, value, getScope.counter++);
111
- return m.get(value) ?? m.set(value, init()).get(value)!;
112
- }
113
- getScope.counter = 0;
114
-
115
163
  abstract class RefCounter {
116
164
  #disposed = false;
117
165
  #count = 0;
@@ -128,36 +176,58 @@ abstract class RefCounter {
128
176
  }
129
177
  });
130
178
  }
131
- abstract dispose: () => void;
179
+ abstract dispose(): void;
132
180
  }
133
181
 
134
182
  const noop = () => {};
135
183
  class BunjaInstance extends RefCounter {
136
184
  #cleanup: (() => void) | undefined;
185
+ #dispose: () => void;
137
186
  constructor(
138
- public store: BunjaStore,
139
- public biid: string,
187
+ dispose: () => void,
188
+ public instanceId: string,
189
+ public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
140
190
  public value: BunjaValue
141
191
  ) {
142
192
  super();
193
+ this.#dispose = () => {
194
+ this.#cleanup?.();
195
+ dispose();
196
+ };
143
197
  }
144
198
  add() {
145
199
  this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
146
200
  super.add();
147
201
  }
148
- dispose = () => {
149
- this.#cleanup?.();
150
- this.store.delete(this.biid);
151
- };
202
+ dispose() {
203
+ this.#dispose();
204
+ }
152
205
  }
153
206
 
154
- class Scope extends RefCounter {
207
+ class ScopeInstance extends RefCounter {
208
+ public static counter = 0;
155
209
  constructor(
156
210
  public dispose: () => void,
157
- public context: React.Context<any>,
158
- public value: any,
159
- public id: number
211
+ public instanceId: number,
212
+ public scope: Scope<any>,
213
+ public value: any
160
214
  ) {
161
215
  super();
162
216
  }
163
217
  }
218
+
219
+ interface Toposortable {
220
+ parents: Toposortable[];
221
+ }
222
+ function toposort<T extends Toposortable>(nodes: T[]): T[] {
223
+ const visited = new Set<T>();
224
+ const result: T[] = [];
225
+ function visit(current: T) {
226
+ if (visited.has(current)) return;
227
+ visited.add(current);
228
+ for (const parent of current.parents) visit(parent as T);
229
+ result.push(current);
230
+ }
231
+ for (const node of nodes) visit(node);
232
+ return result;
233
+ }
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "bunja",
3
- "version": "0.0.7",
4
- "description": "State Lifetime Manager for React",
3
+ "version": "0.0.9",
4
+ "description": "State Lifetime Manager",
5
5
  "main": "bunja.ts",
6
6
  "scripts": {},
7
7
  "keywords": [
8
8
  "bunja",
9
- "react",
10
9
  "di"
11
10
  ],
12
11
  "author": "JongChan Choi <jong@chan.moe>",
@@ -22,6 +21,9 @@
22
21
  "peerDependenciesMeta": {
23
22
  "@types/react": {
24
23
  "optional": true
24
+ },
25
+ "react": {
26
+ "optional": true
25
27
  }
26
28
  }
27
29
  }
package/react.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Context, createContext, useContext, useEffect } from "react";
2
+ import {
3
+ Bunja,
4
+ createBunjaStore,
5
+ createScope,
6
+ ReadScope,
7
+ Scope,
8
+ } from "./bunja";
9
+
10
+ export const BunjaStoreContext = createContext(createBunjaStore());
11
+
12
+ export const scopeContextMap = new Map<Scope<any>, Context<any>>();
13
+ export function bindScope(scope: Scope<any>, context: Context<any>) {
14
+ scopeContextMap.set(scope, context);
15
+ }
16
+
17
+ export function createScopeFromContext<T>(context: Context<T>): Scope<T> {
18
+ const scope = createScope();
19
+ bindScope(scope, context);
20
+ return scope;
21
+ }
22
+
23
+ const defaultReadScope: ReadScope = (scope) => {
24
+ const context = scopeContextMap.get(scope)!;
25
+ return useContext(context);
26
+ };
27
+
28
+ export function useBunja<T>(bunja: Bunja<T>, readScope = defaultReadScope): T {
29
+ const store = useContext(BunjaStoreContext);
30
+ const { value, effect } = store.get(bunja, readScope);
31
+ useEffect(effect, []);
32
+ return value;
33
+ }
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
- "strict": true
4
+ "strict": true,
5
+ "moduleResolution": "Node"
5
6
  }
6
7
  }