bunja 0.0.11 → 0.1.0

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.
@@ -1,4 +1,5 @@
1
1
  {
2
+ "deno.enable": true,
2
3
  "editor.formatOnSave": true,
3
- "editor.defaultFormatter": "esbenp.prettier-vscode"
4
+ "editor.defaultFormatter": "denoland.vscode-deno"
4
5
  }
package/README.md CHANGED
@@ -57,4 +57,170 @@ function MyComponent() {
57
57
  }
58
58
  ```
59
59
 
60
- TODO: context
60
+ ### Defining a Bunja that relies on other Bunja
61
+
62
+ If you want to manage a state with a broad lifetime and another state with a narrower lifetime, you can create a (narrower) bunja that depends on a (broader) bunja.
63
+ For example, you can think of a bunja that manages the WebSocket connection and disconnection, and another bunja that subscribes to a specific resource over the connected WebSocket.
64
+
65
+ In an application composed of multiple pages, you might want to subscribe to the Foo resource on page A and the Bar resource on page B, while using the same WebSocket connection regardless of which page you're on.
66
+ In such a case, you can write the following code.
67
+
68
+ ```ts
69
+ // To simplify the example, code for buffering and reconnection has been omitted.
70
+ const websocketBunja = bunja([], () => {
71
+ let socket;
72
+ const send = (message) => socket.send(JSON.stringify(message));
73
+
74
+ const emitter = new EventEmitter();
75
+ const on = (handler) => {
76
+ emitter.on("message", handler);
77
+ return () => emitter.off("message", handler);
78
+ };
79
+
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
+ };
89
+ });
90
+
91
+ const resourceFooBunja = bunja([websocketBunja], ({ send, on }) => {
92
+ 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
+ };
106
+ });
107
+
108
+ const resourceBarBunja = bunja([websocketBunja], ({ send, on }) => {
109
+ const resourceBarAtom = atom();
110
+ // ...
111
+ });
112
+
113
+ function PageA() {
114
+ const { resourceFooAtom } = useBunja(resourceFooBunja);
115
+ const resourceFoo = useAtomValue(resourceFooAtom);
116
+ // ...
117
+ }
118
+
119
+ function PageB() {
120
+ const { resourceBarAtom } = useBunja(resourceBarBunja);
121
+ const resourceBar = useAtomValue(resourceBarAtom);
122
+ // ...
123
+ }
124
+ ```
125
+
126
+ Notice that `websocketBunja` is not directly `useBunja`-ed.
127
+ When you `useBunja` either `resourceFooBunja` or `resourceBarBunja`, since they depend on `websocketBunja`,
128
+ it has the same effect as if `websocketBunja` were also `useBunja`-ed.
129
+
130
+ > [!NOTE]
131
+ > When a bunja starts, the initialization effect of the bunja with a broader lifetime is called first.\
132
+ > Similarly, when a bunja ends, the cleanup effect of the bunja with the broader lifetime is called first.\
133
+ > This behavior is aligned with how React's `useEffect` cleanup function is invoked, where the parent’s cleanup is executed before the child’s in the render tree.
134
+ >
135
+ > See: <https://github.com/facebook/react/issues/16728>
136
+
137
+ ### Dependency injection using Scope
138
+
139
+ You can use a bunja for local state management.\
140
+ When you specify a scope as a dependency of the bunja, separate bunja instances are created based on the values injected into the scope.
141
+
142
+ ```ts
143
+ import { bunja, createScope } from "bunja";
144
+
145
+ const UrlScope = createScope();
146
+
147
+ const fetchBunja = bunja([UrlScope], (url) => {
148
+ const queryAtom = atomWithQuery((get) => ({
149
+ queryKey: [url],
150
+ queryFn: async () => (await fetch(url)).json(),
151
+ }));
152
+ return { queryAtom };
153
+ });
154
+ ```
155
+
156
+ #### Injecting dependencies via React context
157
+
158
+ If you bind a scope to a React context, bunjas that depend on the scope can retrieve values from the corresponding React context.
159
+
160
+ In the example below, there are two React instances (`<ChildComponent />`) that reference the same `fetchBunja`, but since each looks at a different context value, two separate bunja instances are also created.
161
+
162
+ ```tsx
163
+ import { createContext } from "react";
164
+ import { bunja, createScope } from "bunja";
165
+ import { bindScope } from "bunja/react";
166
+
167
+ const UrlContext = createContext("https://example.com/");
168
+ const UrlScope = createScope();
169
+ bindScope(UrlScope, UrlContext);
170
+
171
+ const fetchBunja = bunja([UrlScope], (url) => {
172
+ const queryAtom = atomWithQuery((get) => ({
173
+ queryKey: [url],
174
+ queryFn: async () => (await fetch(url)).json(),
175
+ }));
176
+ return { queryAtom };
177
+ });
178
+
179
+ function ParentComponent() {
180
+ return (
181
+ <>
182
+ <UrlContext.Provider value="https://example.com/foo">
183
+ <ChildComponent />
184
+ </UrlContext.Provider>
185
+ <UrlContext.Provider value="https://example.com/bar">
186
+ <ChildComponent />
187
+ </UrlContext.Provider>
188
+ </>
189
+ );
190
+ }
191
+
192
+ function ChildComponent() {
193
+ const { queryAtom } = useBunja(fetchBunja);
194
+ const { data, isPending, isError } = useAtomValue(queryAtom);
195
+ // Your component logic here
196
+ }
197
+ ```
198
+
199
+ You can use the `createScopeFromContext` function to handle both the creation of the scope and the binding to the context in one step.
200
+
201
+ ```ts
202
+ import { createContext } from "react";
203
+ import { createScopeFromContext } from "bunja/react";
204
+
205
+ const UrlContext = createContext("https://example.com/");
206
+ const UrlScope = createScopeFromContext(UrlContext);
207
+ ```
208
+
209
+ #### Injecting dependencies directly into the scope
210
+
211
+ You might want to use a bunja directly within a React component where the values to be injected into the scope are created.
212
+
213
+ In such cases, you can use the inject function to inject values into the scope without wrapping the context separately.
214
+
215
+ ```tsx
216
+ import { inject } from "bunja/react";
217
+
218
+ function MyComponent() {
219
+ const { queryAtom } = useBunja(
220
+ fetchBunja,
221
+ inject([[UrlScope, "https://example.com/"]])
222
+ );
223
+ const { data, isPending, isError } = useAtomValue(queryAtom);
224
+ // Your component logic here
225
+ }
226
+ ```
package/bunja.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export type Dep<T> = Bunja<T> | Scope<T>;
2
2
 
3
+ const bunjaEffectSymbol: unique symbol = Symbol("Bunja.effect");
4
+ type BunjaEffectSymbol = typeof bunjaEffectSymbol;
5
+
3
6
  export class Bunja<T> {
4
7
  public static readonly bunjas: Bunja<any>[] = [];
5
8
  public readonly id: number;
@@ -9,13 +12,13 @@ export class Bunja<T> {
9
12
  public parents: Bunja<any>[], // one depth parents
10
13
  public relatedBunjas: Bunja<any>[], // toposorted parents without self
11
14
  public relatedScopes: Scope<any>[], // deduped
12
- public init: (...args: any[]) => T & BunjaValue
15
+ public init: (...args: any[]) => T & BunjaValue,
13
16
  ) {
14
17
  this.id = Bunja.bunjas.length;
15
18
  Bunja.bunjas.push(this);
16
19
  }
17
- static readonly effect = Symbol("Bunja.effect");
18
- toString() {
20
+ static readonly effect: BunjaEffectSymbol = bunjaEffectSymbol;
21
+ toString(): string {
19
22
  const { id, debugLabel } = this;
20
23
  return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
21
24
  }
@@ -29,7 +32,7 @@ export class Scope<T> {
29
32
  this.id = Scope.scopes.length;
30
33
  Scope.scopes.push(this);
31
34
  }
32
- toString() {
35
+ toString(): string {
33
36
  const { id, debugLabel } = this;
34
37
  return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
35
38
  }
@@ -40,12 +43,19 @@ export type ReadScope = <T>(scope: Scope<T>) => T;
40
43
  export class BunjaStore {
41
44
  #bunjas: Record<string, BunjaInstance> = {};
42
45
  #scopes: Map<Scope<any>, Map<any, ScopeInstance>> = new Map();
43
- get<T>(bunja: Bunja<T>, readScope: ReadScope) {
46
+ get<T>(
47
+ bunja: Bunja<T>,
48
+ readScope: ReadScope,
49
+ ): {
50
+ value: T;
51
+ mount: () => void;
52
+ deps: any[];
53
+ } {
44
54
  const scopeInstanceMap = new Map(
45
55
  bunja.relatedScopes.map((scope) => [
46
56
  scope,
47
57
  this.#getScopeInstance(scope, readScope(scope)),
48
- ])
58
+ ]),
49
59
  );
50
60
  const bunjaInstance = this.#getBunjaInstance(bunja, scopeInstanceMap);
51
61
  const { relatedBunjaInstanceMap } = bunjaInstance; // toposorted
@@ -55,21 +65,22 @@ export class BunjaStore {
55
65
  relatedBunjaInstanceMap.forEach((related) => related.add());
56
66
  bunjaInstance.add();
57
67
  scopeInstanceMap.forEach((scope) => scope.add());
58
- return function unmount() {
68
+ return function unmount(): void {
59
69
  // concern: reverse order?
60
70
  relatedBunjaInstanceMap.forEach((related) => related.sub());
61
71
  bunjaInstance.sub();
62
72
  scopeInstanceMap.forEach((scope) => scope.sub());
63
73
  };
64
74
  },
75
+ deps: Array.from(scopeInstanceMap.values()).map(({ value }) => value),
65
76
  };
66
77
  }
67
78
  #getBunjaInstance(
68
79
  bunja: Bunja<any>,
69
- scopeInstanceMap: Map<Scope<any>, ScopeInstance>
80
+ scopeInstanceMap: Map<Scope<any>, ScopeInstance>,
70
81
  ): BunjaInstance {
71
82
  const localScopeInstanceMap = new Map(
72
- bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!])
83
+ bunja.relatedScopes.map((scope) => [scope, scopeInstanceMap.get(scope)!]),
73
84
  );
74
85
  const scopeInstanceIds = Array.from(localScopeInstanceMap.values())
75
86
  .map(({ instanceId }) => instanceId)
@@ -80,7 +91,7 @@ export class BunjaStore {
80
91
  bunja.relatedBunjas.map((relatedBunja) => [
81
92
  relatedBunja,
82
93
  this.#getBunjaInstance(relatedBunja, scopeInstanceMap),
83
- ])
94
+ ]),
84
95
  );
85
96
  const args = bunja.deps.map((dep) => {
86
97
  if (dep instanceof Bunja) return relatedBunjaInstanceMap.get(dep)!.value;
@@ -91,73 +102,74 @@ export class BunjaStore {
91
102
  () => delete this.#bunjas[bunjaInstanceId],
92
103
  bunjaInstanceId,
93
104
  relatedBunjaInstanceMap,
94
- bunja.init.apply(bunja, args)
105
+ bunja.init.apply(bunja, args),
95
106
  );
96
107
  this.#bunjas[bunjaInstanceId] = bunjaInstance;
97
108
  return bunjaInstance;
98
109
  }
99
110
  #getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
100
- const scopeInstanceMap =
101
- this.#scopes.get(scope) ?? this.#scopes.set(scope, new Map()).get(scope)!;
111
+ const scopeInstanceMap = this.#scopes.get(scope) ??
112
+ this.#scopes.set(scope, new Map()).get(scope)!;
102
113
  const init = () =>
103
114
  new ScopeInstance(
104
115
  () => scopeInstanceMap.delete(value),
105
116
  ScopeInstance.counter++,
106
117
  scope,
107
- value
118
+ value,
108
119
  );
109
120
  return (
110
121
  scopeInstanceMap.get(value) ??
111
- scopeInstanceMap.set(value, init()).get(value)!
122
+ scopeInstanceMap.set(value, init()).get(value)!
112
123
  );
113
124
  }
114
125
  }
115
126
 
116
- export const createBunjaStore = () => new BunjaStore();
127
+ export const createBunjaStore = (): BunjaStore => new BunjaStore();
117
128
 
118
129
  export type BunjaEffectFn = () => () => void;
119
130
  export interface BunjaValue {
120
131
  [Bunja.effect]?: BunjaEffectFn;
121
132
  }
122
133
 
123
- export function bunja<T>(deps: [], init: () => T & BunjaValue): Bunja<T>;
124
- export function bunja<T, U>(
125
- deps: [Dep<U>],
126
- init: (u: U) => T & BunjaValue
127
- ): Bunja<T>;
128
- export function bunja<T, U, V>(
129
- deps: [Dep<U>, Dep<V>],
130
- init: (u: U, v: V) => T & BunjaValue
131
- ): Bunja<T>;
132
- export function bunja<T, U, V, W>(
133
- deps: [Dep<U>, Dep<V>, Dep<W>],
134
- init: (u: U, v: V, w: W) => T & BunjaValue
135
- ): Bunja<T>;
136
- export function bunja<T, U, V, W, X>(
137
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>],
138
- init: (u: U, v: V, w: W, x: X) => T & BunjaValue
139
- ): Bunja<T>;
140
- export function bunja<T, U, V, W, X, Y>(
141
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>],
142
- init: (u: U, v: V, w: W, x: X, y: Y) => T & BunjaValue
143
- ): Bunja<T>;
144
- export function bunja<T, U, V, W, X, Y, Z>(
145
- deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>],
146
- init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue
147
- ): Bunja<T>;
148
- export function bunja<T, const U extends any[]>(
134
+ function bunjaImpl<T, const U extends any[]>(
149
135
  deps: { [K in keyof U]: Dep<U[K]> },
150
- init: (...args: U) => T & BunjaValue
136
+ init: (...args: U) => T & BunjaValue,
151
137
  ): Bunja<T> {
152
138
  const parents = deps.filter((dep) => dep instanceof Bunja) as Bunja<any>[];
153
139
  const scopes = deps.filter((dep) => dep instanceof Scope) as Scope<any>[];
154
140
  const relatedBunjas = toposort(parents);
155
141
  const relatedScopes = Array.from(
156
- new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)])
142
+ new Set([...scopes, ...parents.flatMap((parent) => parent.relatedScopes)]),
157
143
  );
158
144
  return new Bunja(deps, parents, relatedBunjas, relatedScopes, init as any);
159
145
  }
160
- bunja.effect = Bunja.effect;
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;
161
173
 
162
174
  export function createScope<T>(): Scope<T> {
163
175
  return new Scope();
@@ -190,7 +202,7 @@ class BunjaInstance extends RefCounter {
190
202
  dispose: () => void,
191
203
  public instanceId: string,
192
204
  public relatedBunjaInstanceMap: Map<Bunja<any>, BunjaInstance>,
193
- public value: BunjaValue
205
+ public value: BunjaValue,
194
206
  ) {
195
207
  super();
196
208
  this.#dispose = () => {
@@ -198,7 +210,7 @@ class BunjaInstance extends RefCounter {
198
210
  dispose();
199
211
  };
200
212
  }
201
- add() {
213
+ override add() {
202
214
  this.#cleanup ??= this.value[Bunja.effect]?.() ?? noop;
203
215
  super.add();
204
216
  }
@@ -213,7 +225,7 @@ class ScopeInstance extends RefCounter {
213
225
  public dispose: () => void,
214
226
  public instanceId: number,
215
227
  public scope: Scope<any>,
216
- public value: any
228
+ public value: any,
217
229
  ) {
218
230
  super();
219
231
  }
package/deno.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@disjukr/bunja",
3
+ "version": "0.1.0",
4
+ "license": "Zlib",
5
+ "exports": {
6
+ ".": "./bunja.ts",
7
+ "./react": "./react.ts"
8
+ }
9
+ }