bunja 0.1.0 → 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/bunja.ts CHANGED
@@ -24,11 +24,13 @@ export class Bunja<T> {
24
24
  }
25
25
  }
26
26
 
27
+ export type HashFn<T = any, U = any> = (value: T) => U;
28
+
27
29
  export class Scope<T> {
28
30
  public static readonly scopes: Scope<any>[] = [];
29
31
  public readonly id: number;
30
32
  public debugLabel: string = "";
31
- constructor() {
33
+ constructor(public readonly hash: HashFn = id) {
32
34
  this.id = Scope.scopes.length;
33
35
  Scope.scopes.push(this);
34
36
  }
@@ -48,7 +50,7 @@ export class BunjaStore {
48
50
  readScope: ReadScope,
49
51
  ): {
50
52
  value: T;
51
- mount: () => void;
53
+ mount: () => () => void;
52
54
  deps: any[];
53
55
  } {
54
56
  const scopeInstanceMap = new Map(
@@ -108,18 +110,19 @@ export class BunjaStore {
108
110
  return bunjaInstance;
109
111
  }
110
112
  #getScopeInstance(scope: Scope<any>, value: any): ScopeInstance {
113
+ const key = scope.hash(value);
111
114
  const scopeInstanceMap = this.#scopes.get(scope) ??
112
115
  this.#scopes.set(scope, new Map()).get(scope)!;
113
116
  const init = () =>
114
117
  new ScopeInstance(
115
- () => scopeInstanceMap.delete(value),
118
+ () => scopeInstanceMap.delete(key),
116
119
  ScopeInstance.counter++,
117
120
  scope,
118
121
  value,
119
122
  );
120
123
  return (
121
- scopeInstanceMap.get(value) ??
122
- scopeInstanceMap.set(value, init()).get(value)!
124
+ scopeInstanceMap.get(key) ??
125
+ scopeInstanceMap.set(key, init()).get(key)!
123
126
  );
124
127
  }
125
128
  }
@@ -171,8 +174,8 @@ export const bunja: {
171
174
  readonly effect: BunjaEffectSymbol;
172
175
  } = bunjaImpl;
173
176
 
174
- export function createScope<T>(): Scope<T> {
175
- return new Scope();
177
+ export function createScope<T>(hash?: HashFn): Scope<T> {
178
+ return new Scope(hash);
176
179
  }
177
180
 
178
181
  abstract class RefCounter {
@@ -194,6 +197,7 @@ abstract class RefCounter {
194
197
  abstract dispose(): void;
195
198
  }
196
199
 
200
+ const id = <T>(x: T): T => x;
197
201
  const noop = () => {};
198
202
  class BunjaInstance extends RefCounter {
199
203
  #cleanup: (() => void) | undefined;
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@disjukr/bunja",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "license": "Zlib",
5
5
  "exports": {
6
6
  ".": "./bunja.ts",
package/deno.lock CHANGED
@@ -1,11 +1,49 @@
1
1
  {
2
2
  "version": "4",
3
3
  "specifiers": {
4
+ "jsr:@std/assert@*": "1.0.8",
5
+ "jsr:@std/assert@^1.0.8": "1.0.8",
6
+ "jsr:@std/async@^1.0.8": "1.0.8",
7
+ "jsr:@std/data-structures@^1.0.4": "1.0.4",
8
+ "jsr:@std/internal@^1.0.4": "1.0.5",
9
+ "jsr:@std/internal@^1.0.5": "1.0.5",
10
+ "jsr:@std/testing@*": "1.0.5",
4
11
  "npm:@types/react@18": "18.3.12",
5
12
  "npm:react@18": "18.3.1",
6
13
  "npm:tsdown@~0.2.17": "0.2.17_typescript@5.6.3",
7
14
  "npm:typescript@^5.6.3": "5.6.3"
8
15
  },
16
+ "jsr": {
17
+ "@std/assert@1.0.6": {
18
+ "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
19
+ "dependencies": [
20
+ "jsr:@std/internal@^1.0.4"
21
+ ]
22
+ },
23
+ "@std/assert@1.0.8": {
24
+ "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b",
25
+ "dependencies": [
26
+ "jsr:@std/internal@^1.0.5"
27
+ ]
28
+ },
29
+ "@std/async@1.0.8": {
30
+ "integrity": "c057c5211a0f1d12e7dcd111ab430091301b8d64b4250052a79d277383bc3ba7"
31
+ },
32
+ "@std/data-structures@1.0.4": {
33
+ "integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
34
+ },
35
+ "@std/internal@1.0.5": {
36
+ "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
37
+ },
38
+ "@std/testing@1.0.5": {
39
+ "integrity": "6e693cbec94c81a1ad3df668685c7ba8e20742bb10305bc7137faa5cf16d2ec4",
40
+ "dependencies": [
41
+ "jsr:@std/assert@^1.0.8",
42
+ "jsr:@std/async",
43
+ "jsr:@std/data-structures"
44
+ ]
45
+ }
46
+ },
9
47
  "npm": {
10
48
  "@antfu/utils@0.7.10": {
11
49
  "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="
@@ -17,21 +17,22 @@ var Bunja = class Bunja {
17
17
  }
18
18
  static effect = bunjaEffectSymbol;
19
19
  toString() {
20
- const { id, debugLabel } = this;
21
- return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
20
+ const { id: id$1, debugLabel } = this;
21
+ return `[Bunja:${id$1}${debugLabel && ` - ${debugLabel}`}]`;
22
22
  }
23
23
  };
24
24
  var Scope = class Scope {
25
25
  static scopes = [];
26
26
  id;
27
27
  debugLabel = "";
28
- constructor() {
28
+ constructor(hash = id) {
29
+ this.hash = hash;
29
30
  this.id = Scope.scopes.length;
30
31
  Scope.scopes.push(this);
31
32
  }
32
33
  toString() {
33
- const { id, debugLabel } = this;
34
- return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
34
+ const { id: id$1, debugLabel } = this;
35
+ return `[Scope:${id$1}${debugLabel && ` - ${debugLabel}`}]`;
35
36
  }
36
37
  };
37
38
  var BunjaStore = class {
@@ -72,9 +73,10 @@ var BunjaStore = class {
72
73
  return bunjaInstance;
73
74
  }
74
75
  #getScopeInstance(scope, value) {
76
+ const key = scope.hash(value);
75
77
  const scopeInstanceMap = this.#scopes.get(scope) ?? this.#scopes.set(scope, new Map()).get(scope);
76
- const init = () => new ScopeInstance(() => scopeInstanceMap.delete(value), ScopeInstance.counter++, scope, value);
77
- return scopeInstanceMap.get(value) ?? scopeInstanceMap.set(value, init()).get(value);
78
+ const init = () => new ScopeInstance(() => scopeInstanceMap.delete(key), ScopeInstance.counter++, scope, value);
79
+ return scopeInstanceMap.get(key) ?? scopeInstanceMap.set(key, init()).get(key);
78
80
  }
79
81
  };
80
82
  const createBunjaStore = () => new BunjaStore();
@@ -87,8 +89,8 @@ function bunjaImpl(deps, init) {
87
89
  }
88
90
  bunjaImpl.effect = Bunja.effect;
89
91
  const bunja = bunjaImpl;
90
- function createScope() {
91
- return new Scope();
92
+ function createScope(hash) {
93
+ return new Scope(hash);
92
94
  }
93
95
  var RefCounter = class {
94
96
  #disposed = false;
@@ -107,6 +109,7 @@ var RefCounter = class {
107
109
  });
108
110
  }
109
111
  };
112
+ const id = (x) => x;
110
113
  const noop = () => {};
111
114
  var BunjaInstance = class extends RefCounter {
112
115
  #cleanup;
@@ -16,21 +16,22 @@ var Bunja = class Bunja {
16
16
  }
17
17
  static effect = bunjaEffectSymbol;
18
18
  toString() {
19
- const { id, debugLabel } = this;
20
- return `[Bunja:${id}${debugLabel && ` - ${debugLabel}`}]`;
19
+ const { id: id$1, debugLabel } = this;
20
+ return `[Bunja:${id$1}${debugLabel && ` - ${debugLabel}`}]`;
21
21
  }
22
22
  };
23
23
  var Scope = class Scope {
24
24
  static scopes = [];
25
25
  id;
26
26
  debugLabel = "";
27
- constructor() {
27
+ constructor(hash = id) {
28
+ this.hash = hash;
28
29
  this.id = Scope.scopes.length;
29
30
  Scope.scopes.push(this);
30
31
  }
31
32
  toString() {
32
- const { id, debugLabel } = this;
33
- return `[Scope:${id}${debugLabel && ` - ${debugLabel}`}]`;
33
+ const { id: id$1, debugLabel } = this;
34
+ return `[Scope:${id$1}${debugLabel && ` - ${debugLabel}`}]`;
34
35
  }
35
36
  };
36
37
  var BunjaStore = class {
@@ -71,9 +72,10 @@ var BunjaStore = class {
71
72
  return bunjaInstance;
72
73
  }
73
74
  #getScopeInstance(scope, value) {
75
+ const key = scope.hash(value);
74
76
  const scopeInstanceMap = this.#scopes.get(scope) ?? this.#scopes.set(scope, new Map()).get(scope);
75
- const init = () => new ScopeInstance(() => scopeInstanceMap.delete(value), ScopeInstance.counter++, scope, value);
76
- return scopeInstanceMap.get(value) ?? scopeInstanceMap.set(value, init()).get(value);
77
+ const init = () => new ScopeInstance(() => scopeInstanceMap.delete(key), ScopeInstance.counter++, scope, value);
78
+ return scopeInstanceMap.get(key) ?? scopeInstanceMap.set(key, init()).get(key);
77
79
  }
78
80
  };
79
81
  const createBunjaStore = () => new BunjaStore();
@@ -86,8 +88,8 @@ function bunjaImpl(deps, init) {
86
88
  }
87
89
  bunjaImpl.effect = Bunja.effect;
88
90
  const bunja = bunjaImpl;
89
- function createScope() {
90
- return new Scope();
91
+ function createScope(hash) {
92
+ return new Scope(hash);
91
93
  }
92
94
  var RefCounter = class {
93
95
  #disposed = false;
@@ -106,6 +108,7 @@ var RefCounter = class {
106
108
  });
107
109
  }
108
110
  };
111
+ const id = (x) => x;
109
112
  const noop = () => {};
110
113
  var BunjaInstance = class extends RefCounter {
111
114
  #cleanup;
package/dist/bunja.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const require_bunja = require('./bunja-QjknYXY-.cjs');
2
+ const require_bunja = require('./bunja-Q0ZusYIM.cjs');
3
3
 
4
4
  exports.Bunja = require_bunja.Bunja
5
5
  exports.BunjaStore = require_bunja.BunjaStore
package/dist/bunja.d.cts CHANGED
@@ -18,11 +18,13 @@ export declare class Bunja<T> {
18
18
  static readonly effect: BunjaEffectSymbol;
19
19
  toString(): string;
20
20
  }
21
+ export type HashFn<T = any, U = any> = (value: T) => U;
21
22
  export declare class Scope<T> {
23
+ readonly hash: HashFn;
22
24
  static readonly scopes: Scope<any>[];
23
25
  readonly id: number;
24
26
  debugLabel: string;
25
- constructor();
27
+ constructor(hash?: HashFn);
26
28
  toString(): string;
27
29
  }
28
30
  export type ReadScope = <T>(scope: Scope<T>) => T;
@@ -30,7 +32,7 @@ export declare class BunjaStore {
30
32
  #private;
31
33
  get<T>(bunja: Bunja<T>, readScope: ReadScope): {
32
34
  value: T;
33
- mount: () => void;
35
+ mount: () => () => void;
34
36
  deps: any[];
35
37
  };
36
38
  }
@@ -49,5 +51,5 @@ export declare const bunja: {
49
51
  <T, U, V, W, X, Y, Z>(deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>], init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue): Bunja<T>;
50
52
  readonly effect: BunjaEffectSymbol;
51
53
  };
52
- export declare function createScope<T>(): Scope<T>;
54
+ export declare function createScope<T>(hash?: HashFn): Scope<T>;
53
55
  export {};
package/dist/bunja.d.ts CHANGED
@@ -18,11 +18,13 @@ export declare class Bunja<T> {
18
18
  static readonly effect: BunjaEffectSymbol;
19
19
  toString(): string;
20
20
  }
21
+ export type HashFn<T = any, U = any> = (value: T) => U;
21
22
  export declare class Scope<T> {
23
+ readonly hash: HashFn;
22
24
  static readonly scopes: Scope<any>[];
23
25
  readonly id: number;
24
26
  debugLabel: string;
25
- constructor();
27
+ constructor(hash?: HashFn);
26
28
  toString(): string;
27
29
  }
28
30
  export type ReadScope = <T>(scope: Scope<T>) => T;
@@ -30,7 +32,7 @@ export declare class BunjaStore {
30
32
  #private;
31
33
  get<T>(bunja: Bunja<T>, readScope: ReadScope): {
32
34
  value: T;
33
- mount: () => void;
35
+ mount: () => () => void;
34
36
  deps: any[];
35
37
  };
36
38
  }
@@ -49,5 +51,5 @@ export declare const bunja: {
49
51
  <T, U, V, W, X, Y, Z>(deps: [Dep<U>, Dep<V>, Dep<W>, Dep<X>, Dep<Y>, Dep<Z>], init: (u: U, v: V, w: W, x: X, y: Y, z: Z) => T & BunjaValue): Bunja<T>;
50
52
  readonly effect: BunjaEffectSymbol;
51
53
  };
52
- export declare function createScope<T>(): Scope<T>;
54
+ export declare function createScope<T>(hash?: HashFn): Scope<T>;
53
55
  export {};
package/dist/bunja.js CHANGED
@@ -1,3 +1,3 @@
1
- import { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createScope } from "./bunja-3B0nQ1vI.js";
1
+ import { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createScope } from "./bunja-fHIhQAuL.js";
2
2
 
3
3
  export { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createScope };
package/dist/react.cjs CHANGED
@@ -22,7 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
 
24
24
  //#endregion
25
- const require_bunja = require('./bunja-QjknYXY-.cjs');
25
+ const require_bunja = require('./bunja-Q0ZusYIM.cjs');
26
26
  const { createContext, useContext, useEffect } = __toESM(require("react"));
27
27
 
28
28
  //#region react.ts
@@ -31,8 +31,8 @@ const scopeContextMap = new Map();
31
31
  function bindScope(scope, context) {
32
32
  scopeContextMap.set(scope, context);
33
33
  }
34
- function createScopeFromContext(context) {
35
- const scope = require_bunja.createScope();
34
+ function createScopeFromContext(context, hash) {
35
+ const scope = require_bunja.createScope(hash);
36
36
  bindScope(scope, context);
37
37
  return scope;
38
38
  }
package/dist/react.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type Context } from "react";
2
- import { type Bunja, type BunjaStore, type ReadScope, type Scope } from "./bunja.ts";
2
+ import { type Bunja, type BunjaStore, type HashFn, type ReadScope, type Scope } from "./bunja.ts";
3
3
  export declare const BunjaStoreContext: Context<BunjaStore>;
4
4
  export declare const scopeContextMap: Map<Scope<any>, Context<any>>;
5
5
  export declare function bindScope(scope: Scope<any>, context: Context<any>): void;
6
- export declare function createScopeFromContext<T>(context: Context<T>): Scope<T>;
6
+ export declare function createScopeFromContext<T>(context: Context<T>, hash?: HashFn<T>): Scope<T>;
7
7
  export declare function useBunja<T>(bunja: Bunja<T>, readScope?: ReadScope): T;
8
8
  export type ScopePair<T> = [Scope<T>, T];
9
9
  export declare function inject<const T extends ScopePair<any>[]>(overrideTable: T): ReadScope;
package/dist/react.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type Context } from "react";
2
- import { type Bunja, type BunjaStore, type ReadScope, type Scope } from "./bunja.ts";
2
+ import { type Bunja, type BunjaStore, type HashFn, type ReadScope, type Scope } from "./bunja.ts";
3
3
  export declare const BunjaStoreContext: Context<BunjaStore>;
4
4
  export declare const scopeContextMap: Map<Scope<any>, Context<any>>;
5
5
  export declare function bindScope(scope: Scope<any>, context: Context<any>): void;
6
- export declare function createScopeFromContext<T>(context: Context<T>): Scope<T>;
6
+ export declare function createScopeFromContext<T>(context: Context<T>, hash?: HashFn<T>): Scope<T>;
7
7
  export declare function useBunja<T>(bunja: Bunja<T>, readScope?: ReadScope): T;
8
8
  export type ScopePair<T> = [Scope<T>, T];
9
9
  export declare function inject<const T extends ScopePair<any>[]>(overrideTable: T): ReadScope;
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createBunjaStore, createScope } from "./bunja-3B0nQ1vI.js";
1
+ import { createBunjaStore, createScope } from "./bunja-fHIhQAuL.js";
2
2
  import { createContext, useContext, useEffect } from "react";
3
3
 
4
4
  //#region react.ts
@@ -7,8 +7,8 @@ const scopeContextMap = new Map();
7
7
  function bindScope(scope, context) {
8
8
  scopeContextMap.set(scope, context);
9
9
  }
10
- function createScopeFromContext(context) {
11
- const scope = createScope();
10
+ function createScopeFromContext(context, hash) {
11
+ const scope = createScope(hash);
12
12
  bindScope(scope, context);
13
13
  return scope;
14
14
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunja",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "1.0.0",
5
5
  "description": "State Lifetime Manager",
6
6
  "main": "dist/bunja.cjs",
7
7
  "module": "dist/bunja.js",
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ # Presentations
2
+
3
+ - React에서 불필요한 웹소켓 재연결을 줄이는 방법 (How to reduce unnecessary
4
+ WebSocket reconnections in React)
5
+ - Conference: 2024 블록체인 진흥주간 x 웹 3.0 컨퍼런스 (2024 Blockchain
6
+ Promotion Week x Web 3.0 Conference)
7
+ - Date: 2024-11-28
8
+ - Slide: [Korean](./2024-11-28.pdf), [English](./2024-11-28-en.pdf)
9
+ - Video(YouTube): [Link](https://www.youtube.com/watch?v=KejJgClvVic)
package/react.ts CHANGED
@@ -4,20 +4,25 @@ import {
4
4
  type BunjaStore,
5
5
  createBunjaStore,
6
6
  createScope,
7
+ type HashFn,
7
8
  type ReadScope,
8
9
  type Scope,
9
10
  } from "./bunja.ts";
10
11
 
11
- export const BunjaStoreContext: Context<BunjaStore> =
12
- createContext(createBunjaStore());
12
+ export const BunjaStoreContext: Context<BunjaStore> = createContext(
13
+ createBunjaStore(),
14
+ );
13
15
 
14
16
  export const scopeContextMap: Map<Scope<any>, Context<any>> = new Map();
15
17
  export function bindScope(scope: Scope<any>, context: Context<any>): void {
16
18
  scopeContextMap.set(scope, context);
17
19
  }
18
20
 
19
- export function createScopeFromContext<T>(context: Context<T>): Scope<T> {
20
- const scope = createScope();
21
+ export function createScopeFromContext<T>(
22
+ context: Context<T>,
23
+ hash?: HashFn<T>,
24
+ ): Scope<T> {
25
+ const scope = createScope(hash);
21
26
  bindScope(scope, context);
22
27
  return scope;
23
28
  }
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
+ });