Comparing version
96
bunja.ts
export interface BunjaFn { | ||
<T>(init: () => T): Bunja<T>; | ||
use: BunjaUseFn; | ||
fork: BunjaForkFn; | ||
effect: BunjaEffectFn; | ||
@@ -11,5 +12,10 @@ } | ||
bunjaFn.use = invalidUse as BunjaUseFn; | ||
bunjaFn.fork = invalidFork as BunjaForkFn; | ||
bunjaFn.effect = invalidEffect as BunjaEffectFn; | ||
export type BunjaUseFn = <T>(dep: Dep<T>) => T; | ||
export type BunjaForkFn = <T>( | ||
bunja: Bunja<T>, | ||
scopeValuePairs: ScopeValuePair<any>[], | ||
) => T; | ||
export type BunjaEffectFn = (callback: BunjaEffectCallback) => void; | ||
@@ -22,5 +28,11 @@ export type BunjaEffectCallback = () => (() => void) | void; | ||
export function createBunjaStore(): BunjaStore { | ||
return new BunjaStore(); | ||
export interface CreateBunjaStoreConfig { | ||
wrapInstance?: WrapInstanceFn; | ||
} | ||
export function createBunjaStore(config?: CreateBunjaStoreConfig): BunjaStore { | ||
const { wrapInstance = defaultWrapInstanceFn } = config ?? {}; | ||
const store = new BunjaStore(); | ||
store.wrapInstance = wrapInstance; | ||
return store; | ||
} | ||
@@ -34,2 +46,7 @@ export type Dep<T> = Bunja<T> | Scope<T>; | ||
} | ||
function invalidFork() { | ||
throw new Error( | ||
"`bunja.fork` can only be used inside a bunja init function.", | ||
); | ||
} | ||
function invalidEffect() { | ||
@@ -54,2 +71,5 @@ throw new Error( | ||
export type WrapInstanceFn = <T>(fn: (dispose: () => void) => T) => T; | ||
const defaultWrapInstanceFn: WrapInstanceFn = (fn) => fn(noop); | ||
export class BunjaStore { | ||
@@ -59,2 +79,3 @@ #bunjas: Record<string, BunjaInstance> = {}; | ||
#bakingContext: BunjaBakingContext | undefined; | ||
wrapInstance: WrapInstanceFn = defaultWrapInstanceFn; | ||
dispose(): void { | ||
@@ -81,7 +102,5 @@ for (const instance of Object.values(this.#bunjas)) instance.dispose(); | ||
const unmount = () => { | ||
setTimeout(() => { | ||
bunjaInstanceMap.forEach((instance) => instance.sub()); | ||
bunjaInstance.sub(); | ||
scopeInstanceMap.forEach((instance) => instance.sub()); | ||
}); | ||
bunjaInstanceMap.forEach((instance) => instance.sub()); | ||
bunjaInstance.sub(); | ||
scopeInstanceMap.forEach((instance) => instance.sub()); | ||
}; | ||
@@ -159,2 +178,3 @@ return unmount; | ||
}; | ||
const originalBakingContext = this.#bakingContext; | ||
try { | ||
@@ -165,3 +185,3 @@ this.#bakingContext = { currentBunja: bunja }; | ||
} finally { | ||
this.#bakingContext = undefined; | ||
this.#bakingContext = originalBakingContext; | ||
} | ||
@@ -174,2 +194,3 @@ } | ||
const originalEffect = bunjaFn.effect; | ||
const originalFork = bunjaFn.fork; | ||
const prevBunja = this.#bakingContext?.currentBunja; | ||
@@ -181,2 +202,8 @@ try { | ||
}; | ||
bunjaFn.fork = (b, scopeValuePairs) => { | ||
const readScope = createReadScopeFn(scopeValuePairs, bunjaFn.use); | ||
const { value, mount } = this.get(b, readScope); | ||
bunjaFn.effect(mount); | ||
return value; | ||
}; | ||
if (this.#bakingContext) this.#bakingContext.currentBunja = bunja; | ||
@@ -186,12 +213,17 @@ if (bunja.baked) { | ||
if (id in this.#bunjas) return this.#bunjas[id]; | ||
const bunjaInstanceValue = bunja.init(); | ||
return this.#createBunjaInstance(id, bunjaInstanceValue, effects); | ||
return this.wrapInstance((dispose) => { | ||
const value = bunja.init(); | ||
return this.#createBunjaInstance(id, value, effects, dispose); | ||
}); | ||
} else { | ||
const bunjaInstanceValue = bunja.init(); | ||
bunja.bake(); | ||
const id = bunja.calcInstanceId(scopeInstanceMap); | ||
return this.#createBunjaInstance(id, bunjaInstanceValue, effects); | ||
return this.wrapInstance((dispose) => { | ||
const value = bunja.init(); | ||
bunja.bake(); | ||
const id = bunja.calcInstanceId(scopeInstanceMap); | ||
return this.#createBunjaInstance(id, value, effects, dispose); | ||
}); | ||
} | ||
} finally { | ||
bunjaFn.effect = originalEffect; | ||
bunjaFn.fork = originalFork; | ||
if (this.#bakingContext) this.#bakingContext.currentBunja = prevBunja!; | ||
@@ -214,2 +246,3 @@ } | ||
effects: BunjaEffectCallback[], | ||
dispose: () => void, | ||
): BunjaInstance { | ||
@@ -222,4 +255,6 @@ const effect = () => { | ||
}; | ||
const dispose = () => delete this.#bunjas[id]; | ||
const bunjaInstance = new BunjaInstance(id, value, effect, dispose); | ||
const bunjaInstance = new BunjaInstance(id, value, effect, () => { | ||
dispose(); | ||
delete this.#bunjas[id]; | ||
}); | ||
this.#bunjas[id] = bunjaInstance; | ||
@@ -231,2 +266,14 @@ return bunjaInstance; | ||
export type ReadScope = <T>(scope: Scope<T>) => T; | ||
export function createReadScopeFn( | ||
scopeValuePairs: ScopeValuePair<any>[], | ||
readScope: ReadScope, | ||
): ReadScope { | ||
const map = new Map(scopeValuePairs); | ||
return <T>(scope: Scope<T>) => { | ||
if (map.has(scope as Scope<unknown>)) { | ||
return map.get(scope as Scope<unknown>) as T; | ||
} | ||
return readScope(scope); | ||
}; | ||
} | ||
@@ -239,2 +286,12 @@ export interface BunjaStoreGetResult<T> { | ||
export function delayUnmount( | ||
mount: () => () => void, | ||
ms: number = 0, | ||
): () => () => void { | ||
return () => { | ||
const unmount = mount(); | ||
return () => setTimeout(unmount, ms); | ||
}; | ||
} | ||
export class Bunja<T> { | ||
@@ -317,2 +374,5 @@ private static counter: number = 0; | ||
} | ||
bind(value: T): ScopeValuePair<T> { | ||
return [this, value]; | ||
} | ||
toString(): string { | ||
@@ -325,4 +385,4 @@ const { id, debugLabel } = this; | ||
export type HashFn<T> = (value: T) => unknown; | ||
export type ScopeValuePair<T> = [Scope<T>, T]; | ||
const noop = () => {}; | ||
abstract class RefCounter { | ||
@@ -389,1 +449,3 @@ #count: number = 0; | ||
} | ||
const noop = () => {}; |
{ | ||
"name": "@disjukr/bunja", | ||
"version": "2.0.0-alpha.5", | ||
"version": "2.0.0-alpha.6", | ||
"license": "Zlib", | ||
"exports": { | ||
".": "./bunja.ts", | ||
"./react": "./react.ts" | ||
} | ||
"./react": "./react.ts", | ||
"./solid": "./solid.ts" | ||
}, | ||
"exclude": ["dist"] | ||
} |
@@ -1,2 +0,2 @@ | ||
import { Bunja, BunjaEffectCallback, BunjaEffectFn, BunjaFn, BunjaStore, BunjaStoreGetResult, BunjaUseFn, Dep, HashFn, ReadScope, Scope, bunja, createBunjaStore, createScope } from "./bunja-Bj2Zho1e.js"; | ||
export { Bunja, BunjaEffectCallback, BunjaEffectFn, BunjaFn, BunjaStore, BunjaStoreGetResult, BunjaUseFn, Dep, HashFn, ReadScope, Scope, bunja, createBunjaStore, createScope }; | ||
import { Bunja, BunjaEffectCallback, BunjaEffectFn, BunjaFn, BunjaForkFn, BunjaStore, BunjaStoreGetResult, BunjaUseFn, CreateBunjaStoreConfig, Dep, HashFn, ReadScope, Scope, ScopeValuePair, WrapInstanceFn, bunja, createBunjaStore, createReadScopeFn, createScope, delayUnmount } from "./bunja-CqyhNWOx.js"; | ||
export { Bunja, BunjaEffectCallback, BunjaEffectFn, BunjaFn, BunjaForkFn, BunjaStore, BunjaStoreGetResult, BunjaUseFn, CreateBunjaStoreConfig, Dep, HashFn, ReadScope, Scope, ScopeValuePair, WrapInstanceFn, bunja, createBunjaStore, createReadScopeFn, createScope, delayUnmount }; |
@@ -1,3 +0,3 @@ | ||
import { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createScope } from "./bunja-DsftUL38.js"; | ||
import { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createReadScopeFn, createScope, delayUnmount } from "./bunja-BB7ru8D0.js"; | ||
export { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createScope }; | ||
export { Bunja, BunjaStore, Scope, bunja, createBunjaStore, createReadScopeFn, createScope, delayUnmount }; |
@@ -1,2 +0,2 @@ | ||
import { Bunja, BunjaStore, HashFn, ReadScope, Scope } from "./bunja-Bj2Zho1e.js"; | ||
import { Bunja, BunjaStore, HashFn, Scope, ScopeValuePair } from "./bunja-CqyhNWOx.js"; | ||
import { Context, PropsWithChildren } from "react"; | ||
@@ -12,6 +12,8 @@ | ||
declare function createScopeFromContext<T>(context: Context<T>, hash?: HashFn<T>): Scope<T>; | ||
declare function useBunja<T>(bunja: Bunja<T>, readScope?: ReadScope): T; | ||
type ScopePair<T> = [Scope<T>, T]; | ||
declare function inject<const T extends ScopePair<any>[]>(overrideTable: T): ReadScope; | ||
declare function useBunja<T>(bunja: Bunja<T>, scopeValuePairs?: ScopeValuePair<any>[]): T; | ||
/** | ||
* @deprecated use `scopeValuePairs` parameter directly in `useBunja` instead. | ||
*/ | ||
declare function inject(scopeValuePairs: ScopeValuePair<any>[]): ScopeValuePair<any>[]; | ||
//#endregion | ||
export { BunjaStoreContext, BunjaStoreProvider, ScopePair, bindScope, createScopeFromContext, inject, scopeContextMap, useBunja }; | ||
export { BunjaStoreContext, BunjaStoreProvider, bindScope, createScopeFromContext, inject, scopeContextMap, useBunja }; |
"use client"; | ||
import { createBunjaStore, createScope } from "./bunja-DsftUL38.js"; | ||
import { createBunjaStore, createReadScopeFn, createScope, delayUnmount } from "./bunja-BB7ru8D0.js"; | ||
import { createContext, createElement, use, useEffect, useState } from "react"; | ||
@@ -30,16 +30,17 @@ | ||
}; | ||
function useBunja(bunja, readScope = defaultReadScope) { | ||
function useBunja(bunja, scopeValuePairs) { | ||
const store = use(BunjaStoreContext); | ||
const readScope = scopeValuePairs ? createReadScopeFn(scopeValuePairs, (scope) => { | ||
const context = scopeContextMap.get(scope); | ||
return use(context); | ||
}) : defaultReadScope; | ||
const { value, mount, deps } = store.get(bunja, readScope); | ||
useEffect(mount, deps); | ||
useEffect(delayUnmount(mount), deps); | ||
return value; | ||
} | ||
function inject(overrideTable) { | ||
const map = new Map(overrideTable); | ||
return (scope) => { | ||
if (map.has(scope)) return map.get(scope); | ||
const context = scopeContextMap.get(scope); | ||
if (!context) throw new Error("Unable to read the scope. Please inject the value explicitly or bind scope to the React context."); | ||
return use(context); | ||
}; | ||
/** | ||
* @deprecated use `scopeValuePairs` parameter directly in `useBunja` instead. | ||
*/ | ||
function inject(scopeValuePairs) { | ||
return scopeValuePairs; | ||
} | ||
@@ -46,0 +47,0 @@ |
{ | ||
"name": "bunja", | ||
"type": "module", | ||
"version": "2.0.0-alpha.5", | ||
"version": "2.0.0-alpha.6", | ||
"description": "State Lifetime Manager", | ||
@@ -29,2 +29,12 @@ "main": "dist/bunja.cjs", | ||
} | ||
}, | ||
"./solid": { | ||
"import": { | ||
"types": "./dist/solid.d.ts", | ||
"default": "./dist/solid.js" | ||
}, | ||
"require": { | ||
"types": "./dist/solid.d.cts", | ||
"default": "./dist/solid.cjs" | ||
} | ||
} | ||
@@ -37,2 +47,5 @@ }, | ||
], | ||
"solid": [ | ||
"./dist/solid.d.ts" | ||
], | ||
"*": [ | ||
@@ -44,4 +57,3 @@ "./dist/bunja.d.ts" | ||
"scripts": { | ||
"build": "tsdown", | ||
"check": "tsc --noEmit" | ||
"build": "tsdown" | ||
}, | ||
@@ -57,2 +69,3 @@ "keywords": [ | ||
"react": "^19", | ||
"solid-js": "^1.9.7", | ||
"tsdown": "^0.12.7", | ||
@@ -63,3 +76,4 @@ "typescript": "^5.6.3" | ||
"@types/react": "*", | ||
"react": ">=19" | ||
"react": ">=19", | ||
"solid-js": "^1" | ||
}, | ||
@@ -72,4 +86,7 @@ "peerDependenciesMeta": { | ||
"optional": true | ||
}, | ||
"solid-js": { | ||
"optional": true | ||
} | ||
} | ||
} |
38
react.ts
@@ -16,6 +16,9 @@ "use client"; | ||
createBunjaStore, | ||
createReadScopeFn, | ||
createScope, | ||
delayUnmount, | ||
type HashFn, | ||
type ReadScope, | ||
type Scope, | ||
type ScopeValuePair, | ||
} from "./bunja.ts"; | ||
@@ -56,28 +59,23 @@ | ||
bunja: Bunja<T>, | ||
readScope: ReadScope = defaultReadScope, | ||
scopeValuePairs?: ScopeValuePair<any>[], | ||
): T { | ||
const store = use(BunjaStoreContext); | ||
const readScope = scopeValuePairs | ||
? createReadScopeFn(scopeValuePairs, <T>(scope: Scope<T>) => { | ||
const context = scopeContextMap.get(scope as Scope<unknown>)!; | ||
return use(context) as T; | ||
}) | ||
: defaultReadScope; | ||
const { value, mount, deps } = store.get(bunja, readScope); | ||
useEffect(mount, deps); | ||
useEffect(delayUnmount(mount), deps); | ||
return value; | ||
} | ||
export type ScopePair<T> = [Scope<T>, T]; | ||
export function inject<const T extends ScopePair<any>[]>( | ||
overrideTable: T, | ||
): ReadScope { | ||
const map = new Map(overrideTable); | ||
return <T>(scope: Scope<T>) => { | ||
if (map.has(scope as Scope<unknown>)) { | ||
return map.get(scope as Scope<unknown>) as T; | ||
} | ||
const context = scopeContextMap.get(scope as Scope<unknown>); | ||
if (!context) { | ||
throw new Error( | ||
"Unable to read the scope. Please inject the value explicitly or bind scope to the React context.", | ||
); | ||
} | ||
return use(context) as T; | ||
}; | ||
/** | ||
* @deprecated use `scopeValuePairs` parameter directly in `useBunja` instead. | ||
*/ | ||
export function inject( | ||
scopeValuePairs: ScopeValuePair<any>[], | ||
): ScopeValuePair<any>[] { | ||
return scopeValuePairs; | ||
} |
85
test.ts
import { assertEquals } from "jsr:@std/assert"; | ||
import { assertSpyCalls, spy } from "jsr:@std/testing/mock"; | ||
import { FakeTime } from "jsr:@std/testing/time"; | ||
@@ -12,3 +11,2 @@ import { bunja, createBunjaStore, createScope } from "./bunja.ts"; | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -21,3 +19,2 @@ const myBunjaInstance = {}; | ||
assertEquals(value, myBunjaInstance); | ||
time.tick(); | ||
}, | ||
@@ -29,3 +26,2 @@ }); | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -47,4 +43,2 @@ const mountSpy = spy(); | ||
cleanup(); | ||
assertSpyCalls(unmountSpy, 0); | ||
time.tick(); | ||
assertSpyCalls(unmountSpy, 1); | ||
@@ -57,3 +51,2 @@ }, | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -90,5 +83,2 @@ const [aMountSpy, aUnmountSpy] = [spy(), spy()]; | ||
cleanup(); | ||
assertSpyCalls(aUnmountSpy, 0); | ||
assertSpyCalls(bUnmountSpy, 0); | ||
time.tick(); | ||
assertSpyCalls(aUnmountSpy, 1); | ||
@@ -102,3 +92,2 @@ assertSpyCalls(bUnmountSpy, 1); | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -131,7 +120,5 @@ const [aMountSpy, aUnmountSpy] = [spy(), spy()]; | ||
c1(); | ||
time.tick(); | ||
assertSpyCalls(aUnmountSpy, 0); | ||
assertSpyCalls(bUnmountSpy, 0); | ||
c2(); | ||
time.tick(); | ||
assertSpyCalls(aUnmountSpy, 1); | ||
@@ -145,3 +132,2 @@ assertSpyCalls(bUnmountSpy, 1); | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -174,7 +160,5 @@ const [aMountSpy, aUnmountSpy] = [spy(), spy()]; | ||
c1(); | ||
time.tick(); | ||
assertSpyCalls(aUnmountSpy, 0); | ||
assertSpyCalls(bUnmountSpy, 1); | ||
c2(); | ||
time.tick(); | ||
assertSpyCalls(aUnmountSpy, 1); | ||
@@ -188,3 +172,2 @@ assertSpyCalls(bUnmountSpy, 1); | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -201,3 +184,2 @@ const myScope = createScope<string>(); | ||
assertEquals(scopeValue, "injected value"); | ||
time.tick(); | ||
}, | ||
@@ -209,3 +191,2 @@ }); | ||
fn() { | ||
using time = new FakeTime(); | ||
const store = createBunjaStore(); | ||
@@ -238,4 +219,68 @@ const myScope = createScope<string>(({ length }) => length); | ||
cleanup3(); | ||
time.tick(); | ||
}, | ||
}); | ||
Deno.test({ | ||
name: "fork", | ||
fn() { | ||
const store = createBunjaStore(); | ||
const aaScope = createScope<string>(); | ||
const bbScope = createScope<string>(); | ||
const [aMountSpy, aUnmountSpy] = [spy(), spy()]; | ||
const [bMountSpy, bUnmountSpy] = [spy(), spy()]; | ||
const [cMountSpy, cUnmountSpy] = [spy(), spy()]; | ||
const aBunja = bunja(() => { | ||
bunja.effect(() => (aMountSpy(), aUnmountSpy)); | ||
return {}; | ||
}); | ||
const bBunja = bunja(() => { | ||
const a = bunja.use(aBunja); | ||
const scopeValue = bunja.use(aaScope); | ||
bunja.use(bbScope); | ||
bunja.effect(() => (bMountSpy(), bUnmountSpy)); | ||
return { a, scopeValue }; | ||
}); | ||
const cBunja = bunja(() => { | ||
const foo = bunja.fork(bBunja, [aaScope.bind("foo")]); | ||
const bar = bunja.fork(bBunja, [aaScope.bind("bar")]); | ||
bunja.effect(() => (cMountSpy(), cUnmountSpy)); | ||
return { foo, bar }; | ||
}); | ||
assertSpyCalls(aMountSpy, 0); | ||
assertSpyCalls(bMountSpy, 0); | ||
assertSpyCalls(cMountSpy, 0); | ||
const { value, mount: m1, deps: d1 } = store.get( | ||
cBunja, | ||
<T>() => "abc" as T, | ||
); | ||
assertSpyCalls(aMountSpy, 0); | ||
assertSpyCalls(bMountSpy, 0); | ||
assertSpyCalls(cMountSpy, 0); | ||
const c1 = m1(); | ||
assertEquals(d1, ["abc"]); | ||
assertEquals(value.foo.a, value.bar.a); | ||
assertEquals(value.foo.scopeValue, "foo"); | ||
assertEquals(value.bar.scopeValue, "bar"); | ||
assertSpyCalls(aMountSpy, 1); | ||
assertSpyCalls(bMountSpy, 2); | ||
assertSpyCalls(cMountSpy, 1); | ||
assertSpyCalls(aUnmountSpy, 0); | ||
assertSpyCalls(bUnmountSpy, 0); | ||
assertSpyCalls(cUnmountSpy, 0); | ||
const { mount: m2, deps: d2 } = store.get( | ||
bBunja, | ||
<T>(scope: any) => ((scope === aaScope) ? "foo" : "abc") as T, | ||
); | ||
const c2 = m2(); | ||
assertEquals(d2, ["foo", "abc"]); | ||
assertSpyCalls(bMountSpy, 2); | ||
c1(); | ||
assertSpyCalls(aUnmountSpy, 0); | ||
assertSpyCalls(bUnmountSpy, 1); | ||
assertSpyCalls(cUnmountSpy, 1); | ||
c2(); | ||
assertSpyCalls(aUnmountSpy, 1); | ||
assertSpyCalls(bUnmountSpy, 2); | ||
assertSpyCalls(cUnmountSpy, 1); | ||
}, | ||
}); |
@@ -9,3 +9,2 @@ { | ||
"emitDeclarationOnly": true, | ||
"isolatedDeclarations": true, | ||
"skipLibCheck": true, | ||
@@ -12,0 +11,0 @@ "outDir": "dist" |
import type { Options } from "tsdown"; | ||
const config: Options = { | ||
entry: ["bunja.ts", "react.ts"], | ||
entry: ["bunja.ts", "react.ts", "solid.ts"], | ||
clean: true, | ||
@@ -6,0 +6,0 @@ dts: true, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
1867
16.47%67050
-99.55%3
50%5
25%25
-7.41%2
100%0
-100%1
Infinity%