Skip to content

Commit b5659be

Browse files
committed
utils: add clamp & createStaticStore
1 parent 25d2683 commit b5659be

File tree

3 files changed

+142
-10
lines changed

3 files changed

+142
-10
lines changed

packages/utils/src/index.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getOwner, onCleanup, createSignal, Accessor, DEV, untrack } from "solid-js";
2-
import type { BaseOptions } from "solid-js/types/reactive/signal";
1+
import { getOwner, onCleanup, createSignal, Accessor, DEV, untrack, batch } from "solid-js";
2+
import type { BaseOptions, Signal } from "solid-js/types/reactive/signal";
33
import { isServer } from "solid-js/web";
44
import type {
55
AnyClass,
@@ -10,7 +10,9 @@ import type {
1010
Trigger,
1111
TriggerCache,
1212
AnyObject,
13-
AnyFunction
13+
AnyFunction,
14+
SetterValue,
15+
AnyStatic
1416
} from "./types";
1517

1618
export * from "./types";
@@ -45,6 +47,8 @@ export function isObject(value: any): value is AnyObject {
4547

4648
export const compare = (a: any, b: any): number => (a < b ? -1 : a > b ? 1 : 0);
4749

50+
export const clamp = (n:number, min: number, max: number) => Math.min(Math.max(n, min), max)
51+
4852
/**
4953
* Accesses the value of a MaybeAccessor
5054
* @example
@@ -303,3 +307,63 @@ export function createTriggerCache<T>(options?: BaseOptions): TriggerCache<T> {
303307
}
304308
};
305309
}
310+
311+
export type StaticStoreSetter<T extends Readonly<AnyStatic>> = {
312+
(setter: (prev: T) => Partial<T>): T;
313+
(state: Partial<T>): T;
314+
<K extends keyof T>(key: K, state: SetterValue<T[K]>): T;
315+
};
316+
317+
/**
318+
* A shallowly wrapped reactive store object. It behaves similarly to the creatStore, but with limited features to keep it simple. Designed to be used for reactive objects with static keys, but dynamic values, like reactive Event State, location, etc.
319+
* @param init initial value of the store
320+
* @returns
321+
* ```ts
322+
* [access: Readonly<T>, write: StaticStoreSetter<T>]
323+
* ```
324+
*/
325+
export function createStaticStore<T extends Readonly<AnyStatic>>(
326+
init: T
327+
): [access: T, write: StaticStoreSetter<T>] {
328+
const copy = { ...init };
329+
const store = {} as T;
330+
const cache = new Map<PropertyKey, Signal<any>>();
331+
332+
const getValue = <K extends keyof T>(key: K): T[K] => {
333+
const saved = cache.get(key);
334+
if (saved) return saved[0]();
335+
const signal = createSignal<any>(copy[key], {
336+
name: typeof key === "string" ? key : undefined
337+
});
338+
cache.set(key, signal);
339+
delete copy[key];
340+
return signal[0]();
341+
};
342+
343+
const setValue = <K extends keyof T>(key: K, value: SetterValue<any>): void => {
344+
const saved = cache.get(key);
345+
if (saved) return saved[1](value);
346+
if (key in copy) copy[key] = accessWith(value, [copy[key]]);
347+
};
348+
349+
for (const key of keys(init)) {
350+
store[key] = undefined as any;
351+
Object.defineProperty(store, key, {
352+
get: getValue.bind(void 0, key)
353+
});
354+
}
355+
356+
const setter = (a: ((prev: T) => Partial<T>) | Partial<T> | keyof T, b?: SetterValue<any>) => {
357+
if (isObject(a))
358+
untrack(() => {
359+
batch(() => {
360+
for (const [key, value] of entries(accessWith(a, store) as Partial<T>))
361+
setValue(key as keyof T, () => value);
362+
});
363+
});
364+
else setValue(a, b);
365+
return store;
366+
};
367+
368+
return [store, setter];
369+
}

packages/utils/test/index.test.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,65 @@
1-
import { createComputed, createEffect, createRoot } from "solid-js";
2-
import { createStore } from "solid-js/store";
3-
import { destore } from "../src";
4-
import { test } from "uvu";
1+
import { createComputed, createRoot } from "solid-js";
2+
import { createStaticStore } from "../src";
3+
import { suite } from "uvu";
4+
import * as assert from "uvu/assert";
5+
6+
const tss = suite("createStaticStore");
7+
8+
tss("individual keys only update when changed", () =>
9+
createRoot(dispose => {
10+
const _shape = {
11+
a: 1,
12+
b: 2,
13+
c: 3,
14+
d: [0, 1, 2]
15+
};
16+
const [state, setState] = createStaticStore(_shape);
17+
18+
assert.equal(state, _shape);
19+
assert.equal(_shape, { a: 1, b: 2, c: 3, d: [0, 1, 2] }, "original input shouldn't be mutated");
20+
21+
setState({
22+
a: 9,
23+
d: [3, 2, 1]
24+
});
25+
26+
assert.equal(state, { a: 9, b: 2, c: 3, d: [3, 2, 1] });
27+
assert.equal(_shape, { a: 1, b: 2, c: 3, d: [0, 1, 2] }, "original input shouldn't be mutated");
28+
29+
let aUpdates = -1;
30+
createComputed(() => {
31+
state.a;
32+
aUpdates++;
33+
});
34+
assert.is(aUpdates, 0);
35+
36+
setState({
37+
b: 3
38+
});
39+
assert.is(aUpdates, 0);
40+
setState("a", 4);
41+
assert.is(aUpdates, 1);
42+
43+
dispose();
44+
})
45+
);
46+
47+
// tss("able to listen to key, not yet added", () =>
48+
// createRoot(dispose => {
49+
// const _shape = {};
50+
// const [state, setState] = createShallowStore<{ a?: number }>(_shape);
51+
52+
// let captured: any[] = [];
53+
// createComputed(() => captured.push(state.a));
54+
55+
// assert.equal(captured, [undefined]);
56+
57+
// setState("a", 1);
58+
59+
// assert.equal(captured, [undefined, 1]);
60+
61+
// dispose();
62+
// })
63+
// );
64+
65+
tss.run();

packages/utils/tsconfig.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
22
"extends": "../../tsconfig.json",
3-
"include": ["./src"],
4-
"exclude": ["node_modules", "./dist"]
5-
}
3+
"include": [
4+
"./src",
5+
"./test",
6+
"./dev"
7+
],
8+
"exclude": [
9+
"node_modules",
10+
"./dist"
11+
]
12+
}

0 commit comments

Comments
 (0)