bunja 0.0.12 → 1.0.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.
package/react.ts CHANGED
@@ -1,21 +1,28 @@
1
- import { Context, createContext, useContext, useEffect } from "react";
1
+ import { type Context, createContext, useContext, useEffect } from "react";
2
2
  import {
3
- Bunja,
3
+ type Bunja,
4
+ type BunjaStore,
4
5
  createBunjaStore,
5
6
  createScope,
6
- ReadScope,
7
- Scope,
8
- } from "./bunja";
7
+ type HashFn,
8
+ type ReadScope,
9
+ type Scope,
10
+ } from "./bunja.ts";
9
11
 
10
- export const BunjaStoreContext = createContext(createBunjaStore());
12
+ export const BunjaStoreContext: Context<BunjaStore> = createContext(
13
+ createBunjaStore(),
14
+ );
11
15
 
12
- export const scopeContextMap = new Map<Scope<any>, Context<any>>();
13
- export function bindScope(scope: Scope<any>, context: Context<any>) {
16
+ export const scopeContextMap: Map<Scope<any>, Context<any>> = new Map();
17
+ export function bindScope(scope: Scope<any>, context: Context<any>): void {
14
18
  scopeContextMap.set(scope, context);
15
19
  }
16
20
 
17
- export function createScopeFromContext<T>(context: Context<T>): Scope<T> {
18
- const scope = createScope();
21
+ export function createScopeFromContext<T>(
22
+ context: Context<T>,
23
+ hash?: HashFn<T>,
24
+ ): Scope<T> {
25
+ const scope = createScope(hash);
19
26
  bindScope(scope, context);
20
27
  return scope;
21
28
  }
@@ -25,7 +32,10 @@ const defaultReadScope: ReadScope = (scope) => {
25
32
  return useContext(context);
26
33
  };
27
34
 
28
- export function useBunja<T>(bunja: Bunja<T>, readScope = defaultReadScope): T {
35
+ export function useBunja<T>(
36
+ bunja: Bunja<T>,
37
+ readScope: ReadScope = defaultReadScope,
38
+ ): T {
29
39
  const store = useContext(BunjaStoreContext);
30
40
  const { value, mount, deps } = store.get(bunja, readScope);
31
41
  useEffect(mount, deps);
@@ -35,7 +45,7 @@ export function useBunja<T>(bunja: Bunja<T>, readScope = defaultReadScope): T {
35
45
  export type ScopePair<T> = [Scope<T>, T];
36
46
 
37
47
  export function inject<const T extends ScopePair<any>[]>(
38
- overrideTable: T
48
+ overrideTable: T,
39
49
  ): ReadScope {
40
50
  const map = new Map(overrideTable);
41
51
  return (scope) => {
@@ -43,7 +53,7 @@ export function inject<const T extends ScopePair<any>[]>(
43
53
  const context = scopeContextMap.get(scope);
44
54
  if (!context) {
45
55
  throw new Error(
46
- "Unable to read the scope. Please inject the value explicitly or bind scope to the React context."
56
+ "Unable to read the scope. Please inject the value explicitly or bind scope to the React context.",
47
57
  );
48
58
  }
49
59
  return useContext(context);
package/test.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { assertEquals } from "jsr:@std/assert";
2
+ import { assertSpyCalls, spy } from "jsr:@std/testing/mock";
3
+ import { FakeTime } from "jsr:@std/testing/time";
4
+
5
+ import { createBunjaStore, createScope } from "./bunja.ts";
6
+ import { bunja } from "./bunja.ts";
7
+
8
+ const readNull = () => (null as any);
9
+
10
+ Deno.test({
11
+ name: "basic",
12
+ fn() {
13
+ using time = new FakeTime();
14
+ const store = createBunjaStore();
15
+ const myBunjaInstance = {};
16
+ const myBunja = bunja([], () => myBunjaInstance);
17
+ const { value, mount } = store.get(myBunja, readNull);
18
+ const cleanup = mount();
19
+ cleanup();
20
+ assertEquals(value, myBunjaInstance);
21
+ time.tick();
22
+ },
23
+ });
24
+
25
+ Deno.test({
26
+ name: "basic effect",
27
+ fn() {
28
+ using time = new FakeTime();
29
+ const store = createBunjaStore();
30
+ const mountSpy = spy();
31
+ const unmountSpy = spy();
32
+ const myBunja = bunja([], () => ({
33
+ [bunja.effect]() {
34
+ mountSpy();
35
+ return unmountSpy;
36
+ },
37
+ }));
38
+ assertSpyCalls(mountSpy, 0);
39
+ const { mount } = store.get(myBunja, readNull);
40
+ assertSpyCalls(mountSpy, 0);
41
+ const cleanup = mount();
42
+ assertSpyCalls(mountSpy, 1);
43
+ assertSpyCalls(unmountSpy, 0);
44
+ cleanup();
45
+ assertSpyCalls(unmountSpy, 0);
46
+ time.tick();
47
+ assertSpyCalls(unmountSpy, 1);
48
+ },
49
+ });
50
+
51
+ Deno.test({
52
+ name: "bunja that depend on other bunja",
53
+ fn() {
54
+ using time = new FakeTime();
55
+ const store = createBunjaStore();
56
+ const [aMountSpy, aUnmountSpy] = [spy(), spy()];
57
+ const [bMountSpy, bUnmountSpy] = [spy(), spy()];
58
+ const aBunjaInstance = {
59
+ [bunja.effect]() {
60
+ aMountSpy();
61
+ return aUnmountSpy;
62
+ },
63
+ };
64
+ const aBunja = bunja([], () => aBunjaInstance);
65
+ const bBunja = bunja([aBunja], (a) => ({
66
+ a,
67
+ [bunja.effect]() {
68
+ bMountSpy();
69
+ return bUnmountSpy;
70
+ },
71
+ }));
72
+ assertSpyCalls(aMountSpy, 0);
73
+ assertSpyCalls(bMountSpy, 0);
74
+ const { value, mount } = store.get(bBunja, readNull);
75
+ assertEquals(value.a, aBunjaInstance);
76
+ assertSpyCalls(aMountSpy, 0);
77
+ assertSpyCalls(bMountSpy, 0);
78
+ const cleanup = mount();
79
+ assertSpyCalls(aMountSpy, 1);
80
+ assertSpyCalls(bMountSpy, 1);
81
+ assertSpyCalls(aUnmountSpy, 0);
82
+ assertSpyCalls(bUnmountSpy, 0);
83
+ cleanup();
84
+ assertSpyCalls(aUnmountSpy, 0);
85
+ assertSpyCalls(bUnmountSpy, 0);
86
+ time.tick();
87
+ assertSpyCalls(aUnmountSpy, 1);
88
+ assertSpyCalls(bUnmountSpy, 1);
89
+ },
90
+ });
91
+
92
+ Deno.test({
93
+ name: "A mount first, B mount later & A unmount first, B unmount later",
94
+ fn() {
95
+ using time = new FakeTime();
96
+ const store = createBunjaStore();
97
+ const [aMountSpy, aUnmountSpy] = [spy(), spy()];
98
+ const [bMountSpy, bUnmountSpy] = [spy(), spy()];
99
+ const aBunja = bunja([], () => ({
100
+ [bunja.effect]: () => (aMountSpy(), aUnmountSpy),
101
+ }));
102
+ const bBunja = bunja([aBunja], () => ({
103
+ [bunja.effect]: () => (bMountSpy(), bUnmountSpy),
104
+ }));
105
+ const { mount: m1 } = store.get(aBunja, readNull);
106
+ const c1 = m1();
107
+ assertSpyCalls(aMountSpy, 1);
108
+ assertSpyCalls(bMountSpy, 0);
109
+ const { mount: m2 } = store.get(bBunja, readNull);
110
+ const c2 = m2();
111
+ assertSpyCalls(aMountSpy, 1);
112
+ assertSpyCalls(bMountSpy, 1);
113
+ assertSpyCalls(aUnmountSpy, 0);
114
+ assertSpyCalls(bUnmountSpy, 0);
115
+ c1();
116
+ time.tick();
117
+ assertSpyCalls(aUnmountSpy, 0);
118
+ assertSpyCalls(bUnmountSpy, 0);
119
+ c2();
120
+ time.tick();
121
+ assertSpyCalls(aUnmountSpy, 1);
122
+ assertSpyCalls(bUnmountSpy, 1);
123
+ },
124
+ });
125
+
126
+ Deno.test({
127
+ name: "B mount first, A mount later & B unmount first, A unmount later",
128
+ fn() {
129
+ using time = new FakeTime();
130
+ const store = createBunjaStore();
131
+ const [aMountSpy, aUnmountSpy] = [spy(), spy()];
132
+ const [bMountSpy, bUnmountSpy] = [spy(), spy()];
133
+ const aBunja = bunja([], () => ({
134
+ [bunja.effect]: () => (aMountSpy(), aUnmountSpy),
135
+ }));
136
+ const bBunja = bunja([aBunja], () => ({
137
+ [bunja.effect]: () => (bMountSpy(), bUnmountSpy),
138
+ }));
139
+ const { mount: m1 } = store.get(bBunja, readNull);
140
+ const c1 = m1();
141
+ assertSpyCalls(aMountSpy, 1);
142
+ assertSpyCalls(bMountSpy, 1);
143
+ const { mount: m2 } = store.get(aBunja, readNull);
144
+ const c2 = m2();
145
+ assertSpyCalls(aMountSpy, 1);
146
+ assertSpyCalls(bMountSpy, 1);
147
+ assertSpyCalls(aUnmountSpy, 0);
148
+ assertSpyCalls(bUnmountSpy, 0);
149
+ c1();
150
+ time.tick();
151
+ assertSpyCalls(aUnmountSpy, 0);
152
+ assertSpyCalls(bUnmountSpy, 1);
153
+ c2();
154
+ time.tick();
155
+ assertSpyCalls(aUnmountSpy, 1);
156
+ assertSpyCalls(bUnmountSpy, 1);
157
+ },
158
+ });
159
+
160
+ Deno.test({
161
+ name: "injecting values into a scope when calling store.get",
162
+ fn() {
163
+ using time = new FakeTime();
164
+ const store = createBunjaStore();
165
+ const myScope = createScope<string>();
166
+ const myBunja = bunja([myScope], (scopeValue) => ({ scopeValue }));
167
+ const readScope = <T>(): T => "injected value" as T;
168
+ const { value: { scopeValue }, mount } = store.get(myBunja, readScope);
169
+ const cleanup = mount();
170
+ cleanup();
171
+ assertEquals(scopeValue, "injected value");
172
+ time.tick();
173
+ },
174
+ });
175
+
176
+ Deno.test({
177
+ name: "scope value deduplication using hash function",
178
+ fn() {
179
+ using time = new FakeTime();
180
+ const store = createBunjaStore();
181
+ const myScope = createScope<string>(({ length }) => length);
182
+ const myBunja = bunja([myScope], (scopeValue) => ({ scopeValue }));
183
+ const { value: { scopeValue: scopeValue1 }, mount: mount1 } = store.get(
184
+ myBunja,
185
+ <T>() => "foo" as T,
186
+ );
187
+ const cleanup1 = mount1();
188
+ const { value: { scopeValue: scopeValue2 }, mount: mount2 } = store.get(
189
+ myBunja,
190
+ <T>() => "bar" as T,
191
+ );
192
+ const cleanup2 = mount2();
193
+ const { value: { scopeValue: scopeValue3 }, mount: mount3 } = store.get(
194
+ myBunja,
195
+ <T>() => "baaz" as T,
196
+ );
197
+ const cleanup3 = mount3();
198
+ assertEquals(scopeValue1, "foo");
199
+ assertEquals(scopeValue2, "foo");
200
+ assertEquals(scopeValue3, "baaz");
201
+ cleanup1();
202
+ cleanup2();
203
+ cleanup3();
204
+ time.tick();
205
+ },
206
+ });
package/tsconfig.json CHANGED
@@ -2,6 +2,12 @@
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
4
  "strict": true,
5
- "moduleResolution": "Node"
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "declaration": true,
8
+ "emitDeclarationOnly": true,
9
+ "isolatedDeclarations": true,
10
+ "skipLibCheck": true,
11
+ "outDir": "dist"
6
12
  }
7
13
  }
@@ -0,0 +1,10 @@
1
+ import type { Options } from "tsdown";
2
+
3
+ const config: Options = {
4
+ entry: ["bunja.ts", "react.ts"],
5
+ clean: true,
6
+ dts: true,
7
+ format: ["esm", "cjs"],
8
+ };
9
+
10
+ export default config;