stunk 2.1.0 → 2.2.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/LICENSE +27 -27
- package/README.md +43 -43
- package/dist/{core → src/core}/asyncChunk.d.ts +6 -0
- package/dist/{core → src/core}/asyncChunk.js +6 -1
- package/dist/{core → src/core}/computed.d.ts +5 -0
- package/dist/src/core/computed.js +58 -0
- package/dist/{core → src/core}/core.d.ts +4 -0
- package/dist/{core → src/core}/core.js +41 -33
- package/dist/src/core/selector.d.ts +17 -0
- package/dist/src/core/selector.js +45 -0
- package/dist/{use-react → src/use-react}/hooks/useDerive.js +8 -2
- package/dist/{utils.d.ts → src/utils.d.ts} +1 -0
- package/dist/{utils.js → src/utils.js} +34 -0
- package/dist/tests/async-chunk.test.d.ts +1 -0
- package/dist/tests/async-chunk.test.js +164 -0
- package/dist/tests/batch-chunk.test.d.ts +1 -0
- package/dist/tests/batch-chunk.test.js +89 -0
- package/dist/tests/chunk.test.d.ts +1 -0
- package/dist/tests/chunk.test.js +215 -0
- package/dist/tests/computed.test.d.ts +1 -0
- package/dist/tests/computed.test.js +192 -0
- package/dist/tests/diamond-dep.test.d.ts +1 -0
- package/dist/tests/diamond-dep.test.js +74 -0
- package/dist/tests/history.test.d.ts +1 -0
- package/dist/tests/history.test.js +73 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/middleware.test.js +30 -0
- package/dist/tests/persist.test.d.ts +1 -0
- package/dist/tests/persist.test.js +43 -0
- package/dist/tests/select-chunk.test.d.ts +1 -0
- package/dist/tests/select-chunk.test.js +167 -0
- package/package.json +97 -91
- package/dist/core/computed.js +0 -54
- package/dist/core/selector.d.ts +0 -2
- package/dist/core/selector.js +0 -9
- /package/dist/{core → src/core}/types.d.ts +0 -0
- /package/dist/{core → src/core}/types.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{middleware → src/middleware}/history.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/history.js +0 -0
- /package/dist/{middleware → src/middleware}/index.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/index.js +0 -0
- /package/dist/{middleware → src/middleware}/logger.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/logger.js +0 -0
- /package/dist/{middleware → src/middleware}/persistence.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/persistence.js +0 -0
- /package/dist/{middleware → src/middleware}/validator.d.ts +0 -0
- /package/dist/{middleware → src/middleware}/validator.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useAsyncChunk.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunk.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunk.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkProperty.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValue.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValue.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValues.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useChunkValues.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useComputed.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useComputed.js +0 -0
- /package/dist/{use-react → src/use-react}/hooks/useDerive.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/index.d.ts +0 -0
- /package/dist/{use-react → src/use-react}/index.js +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { chunk } from '../src/core/core';
|
|
3
|
+
import { computed } from '../src/core/computed';
|
|
4
|
+
describe('computed > diamond dependency pattern', () => {
|
|
5
|
+
it('should handle diamond dependency pattern correctly', () => {
|
|
6
|
+
// Create the root chunk (A)
|
|
7
|
+
const root = chunk(1);
|
|
8
|
+
// Create two intermediate chunks (B and C) that depend on root
|
|
9
|
+
const left = computed([root], value => value * 2);
|
|
10
|
+
const right = computed([root], value => value + 5);
|
|
11
|
+
// Create a final computed chunk (D) that depends on both B and C
|
|
12
|
+
const final = computed([left, right], (leftValue, rightValue) => `${leftValue}-${rightValue}`);
|
|
13
|
+
// Verify initial computed values before subscribing
|
|
14
|
+
expect(root.get()).toBe(1);
|
|
15
|
+
expect(left.get()).toBe(2); // 1 * 2
|
|
16
|
+
expect(right.get()).toBe(6); // 1 + 5
|
|
17
|
+
expect(final.get()).toBe('2-6');
|
|
18
|
+
// Setup a spy to track updates
|
|
19
|
+
const subscriber = vi.fn();
|
|
20
|
+
const unsubscribe = final.subscribe(subscriber);
|
|
21
|
+
expect(subscriber).toHaveBeenCalledWith('2-6');
|
|
22
|
+
subscriber.mockClear();
|
|
23
|
+
// Update the root
|
|
24
|
+
root.set(4);
|
|
25
|
+
expect(root.get()).toBe(4);
|
|
26
|
+
expect(left.get()).toBe(8); // 4 * 2
|
|
27
|
+
expect(right.get()).toBe(9); // 4 + 5
|
|
28
|
+
expect(final.get()).toBe('8-9');
|
|
29
|
+
expect(subscriber).toHaveBeenCalledWith('8-9');
|
|
30
|
+
// Clean up
|
|
31
|
+
unsubscribe();
|
|
32
|
+
});
|
|
33
|
+
it('should handle complex update chains in diamond patterns', () => {
|
|
34
|
+
const root = chunk(10);
|
|
35
|
+
// First level of dependencies
|
|
36
|
+
const pathA = computed([root], val => val + 5);
|
|
37
|
+
const pathB = computed([root], val => val * 2);
|
|
38
|
+
// Second level - depends on both branches
|
|
39
|
+
const merged = computed([pathA, pathB], (a, b) => ({ sum: a + b, product: a * b }));
|
|
40
|
+
// Verify initial values before subscribing
|
|
41
|
+
expect(pathA.get()).toBe(15); // 10 + 5
|
|
42
|
+
expect(pathB.get()).toBe(20); // 10 * 2
|
|
43
|
+
expect(merged.get()).toEqual({ sum: 35, product: 300 });
|
|
44
|
+
// Set up capture for last values only
|
|
45
|
+
let lastUpdate = null;
|
|
46
|
+
const updates = [];
|
|
47
|
+
const unsubscribe = merged.subscribe(val => {
|
|
48
|
+
lastUpdate = { ...val };
|
|
49
|
+
});
|
|
50
|
+
// First update
|
|
51
|
+
root.set(20);
|
|
52
|
+
if (lastUpdate)
|
|
53
|
+
updates.push(lastUpdate);
|
|
54
|
+
// Verify values after first update
|
|
55
|
+
expect(pathA.get()).toBe(25); // 20 + 5
|
|
56
|
+
expect(pathB.get()).toBe(40); // 20 * 2
|
|
57
|
+
expect(merged.get()).toEqual({ sum: 65, product: 1000 });
|
|
58
|
+
// Second update
|
|
59
|
+
root.set(0);
|
|
60
|
+
if (lastUpdate)
|
|
61
|
+
updates.push(lastUpdate);
|
|
62
|
+
// Verify values after second update
|
|
63
|
+
expect(pathA.get()).toBe(5); // 0 + 5
|
|
64
|
+
expect(pathB.get()).toBe(0); // 0 * 2
|
|
65
|
+
expect(merged.get()).toEqual({ sum: 5, product: 0 });
|
|
66
|
+
// Check that we received both values, ignoring intermediate ones
|
|
67
|
+
expect(updates).toEqual([
|
|
68
|
+
{ sum: 65, product: 1000 },
|
|
69
|
+
{ sum: 5, product: 0 }
|
|
70
|
+
]);
|
|
71
|
+
// Clean up
|
|
72
|
+
unsubscribe();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { chunk } from "../src/core/core";
|
|
3
|
+
import { withHistory } from "../src/middleware/history";
|
|
4
|
+
describe('Chunk with History', () => {
|
|
5
|
+
it('should maintain history of changes', () => {
|
|
6
|
+
const baseChunk = chunk(0);
|
|
7
|
+
const historyChunk = withHistory(baseChunk);
|
|
8
|
+
historyChunk.set(1);
|
|
9
|
+
historyChunk.set(2);
|
|
10
|
+
historyChunk.set(3);
|
|
11
|
+
expect(historyChunk.getHistory()).toEqual([0, 1, 2, 3]);
|
|
12
|
+
});
|
|
13
|
+
it('should handle undo and redo operations', () => {
|
|
14
|
+
const baseChunk = chunk(0);
|
|
15
|
+
const historyChunk = withHistory(baseChunk);
|
|
16
|
+
const callback = vi.fn();
|
|
17
|
+
historyChunk.subscribe(callback);
|
|
18
|
+
callback.mockClear(); // Clear initial subscription call
|
|
19
|
+
historyChunk.set(1);
|
|
20
|
+
historyChunk.set(2);
|
|
21
|
+
expect(historyChunk.get()).toBe(2);
|
|
22
|
+
historyChunk.undo();
|
|
23
|
+
expect(historyChunk.get()).toBe(1);
|
|
24
|
+
historyChunk.undo();
|
|
25
|
+
expect(historyChunk.get()).toBe(0);
|
|
26
|
+
historyChunk.redo();
|
|
27
|
+
expect(historyChunk.get()).toBe(1);
|
|
28
|
+
historyChunk.redo();
|
|
29
|
+
expect(historyChunk.get()).toBe(2);
|
|
30
|
+
expect(callback).toHaveBeenCalledTimes(6); // 2 sets + 2 undos + 2 redos
|
|
31
|
+
});
|
|
32
|
+
it('should handle branching history', () => {
|
|
33
|
+
const baseChunk = chunk(0);
|
|
34
|
+
const historyChunk = withHistory(baseChunk);
|
|
35
|
+
historyChunk.set(1);
|
|
36
|
+
historyChunk.set(2);
|
|
37
|
+
historyChunk.undo();
|
|
38
|
+
historyChunk.set(3); // This should create a new branch
|
|
39
|
+
expect(historyChunk.getHistory()).toEqual([0, 1, 3]);
|
|
40
|
+
expect(historyChunk.get()).toBe(3);
|
|
41
|
+
});
|
|
42
|
+
it('should respect maxHistory limit', () => {
|
|
43
|
+
const baseChunk = chunk(0);
|
|
44
|
+
const historyChunk = withHistory(baseChunk, { maxHistory: 3 });
|
|
45
|
+
historyChunk.set(1);
|
|
46
|
+
historyChunk.set(2);
|
|
47
|
+
historyChunk.set(3);
|
|
48
|
+
historyChunk.set(4);
|
|
49
|
+
expect(historyChunk.getHistory()).toEqual([2, 3, 4]);
|
|
50
|
+
});
|
|
51
|
+
it('should handle canUndo and canRedo correctly', () => {
|
|
52
|
+
const baseChunk = chunk(0);
|
|
53
|
+
const historyChunk = withHistory(baseChunk);
|
|
54
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
55
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
56
|
+
historyChunk.set(1);
|
|
57
|
+
expect(historyChunk.canUndo()).toBe(true);
|
|
58
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
59
|
+
historyChunk.undo();
|
|
60
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
61
|
+
expect(historyChunk.canRedo()).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('should clear history properly', () => {
|
|
64
|
+
const baseChunk = chunk(0);
|
|
65
|
+
const historyChunk = withHistory(baseChunk);
|
|
66
|
+
historyChunk.set(1);
|
|
67
|
+
historyChunk.set(2);
|
|
68
|
+
historyChunk.clearHistory();
|
|
69
|
+
expect(historyChunk.getHistory()).toEqual([2]);
|
|
70
|
+
expect(historyChunk.canUndo()).toBe(false);
|
|
71
|
+
expect(historyChunk.canRedo()).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, beforeEach, vi, afterEach, test, expect } from "vitest";
|
|
2
|
+
import { chunk } from "../src/core/core";
|
|
3
|
+
import { logger } from "../src/middleware/logger";
|
|
4
|
+
import { nonNegativeValidator } from "../src/middleware/validator";
|
|
5
|
+
describe("Middleware Tests", () => {
|
|
6
|
+
let consoleSpy;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
consoleSpy = vi.spyOn(console, "log");
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks(); // Restores all spies
|
|
12
|
+
vi.clearAllTimers(); // Clears any lingering timers
|
|
13
|
+
});
|
|
14
|
+
test("Logger middleware should log updates", () => {
|
|
15
|
+
const count = chunk(0, [logger]);
|
|
16
|
+
const unsubscribe = count.subscribe(() => { }); // Subscribe to capture updates
|
|
17
|
+
try {
|
|
18
|
+
count.set(5); // Should log: "Setting value: 5"
|
|
19
|
+
expect(consoleSpy).toHaveBeenCalledWith("Setting value:", 5);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
unsubscribe(); // Ensure cleanup after test
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
test("Non-negative validator middleware should prevent negative values", () => {
|
|
26
|
+
const count = chunk(0, [nonNegativeValidator]);
|
|
27
|
+
expect(() => count.set(-5)).toThrow("Value must be non-negative!");
|
|
28
|
+
expect(count.get()).toBe(0); // Value should remain unchanged
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, beforeEach, it, expect, vi } from 'vitest';
|
|
2
|
+
import { chunk } from '../src/core/core';
|
|
3
|
+
import { withPersistence } from "../src/middleware/persistence";
|
|
4
|
+
describe('withPersistence', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
sessionStorage.clear();
|
|
8
|
+
});
|
|
9
|
+
it('should persist state to localStorage', () => {
|
|
10
|
+
const baseChunk = chunk({ count: 0 });
|
|
11
|
+
const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
|
|
12
|
+
persistedChunk.set({ count: 1 });
|
|
13
|
+
expect(JSON.parse(localStorage.getItem('test-key'))).toEqual({ count: 1 });
|
|
14
|
+
});
|
|
15
|
+
it('should load persisted state on initialization', () => {
|
|
16
|
+
localStorage.setItem('test-key', JSON.stringify({ count: 5 }));
|
|
17
|
+
const baseChunk = chunk({ count: 0 });
|
|
18
|
+
const persistedChunk = withPersistence(baseChunk, { key: 'test-key' });
|
|
19
|
+
expect(persistedChunk.get()).toEqual({ count: 5 });
|
|
20
|
+
});
|
|
21
|
+
it('should use custom storage', () => {
|
|
22
|
+
const mockStorage = {
|
|
23
|
+
getItem: vi.fn(),
|
|
24
|
+
setItem: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
const baseChunk = chunk({ count: 0 });
|
|
27
|
+
withPersistence(baseChunk, {
|
|
28
|
+
key: 'test-key',
|
|
29
|
+
storage: mockStorage
|
|
30
|
+
});
|
|
31
|
+
expect(mockStorage.getItem).toHaveBeenCalledWith('test-key');
|
|
32
|
+
});
|
|
33
|
+
it('should use custom serializer/deserializer', () => {
|
|
34
|
+
const baseChunk = chunk({ count: 0 });
|
|
35
|
+
const persistedChunk = withPersistence(baseChunk, {
|
|
36
|
+
key: 'test-key',
|
|
37
|
+
serialize: value => btoa(JSON.stringify(value)),
|
|
38
|
+
deserialize: value => JSON.parse(atob(value))
|
|
39
|
+
});
|
|
40
|
+
persistedChunk.set({ count: 1 });
|
|
41
|
+
expect(localStorage.getItem('test-key')).toBe(btoa(JSON.stringify({ count: 1 })));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { chunk } from '../src/core/core';
|
|
3
|
+
import { select } from '../src/core/selector';
|
|
4
|
+
describe('select', () => {
|
|
5
|
+
it('should create a selector that initially returns the correct value', () => {
|
|
6
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
7
|
+
const nameSelector = select(source, user => user.name);
|
|
8
|
+
expect(nameSelector.get()).toBe('John');
|
|
9
|
+
});
|
|
10
|
+
it('should update when selected value changes', () => {
|
|
11
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
12
|
+
const nameSelector = select(source, user => user.name);
|
|
13
|
+
source.set({ name: 'Jane', age: 25 });
|
|
14
|
+
expect(nameSelector.get()).toBe('Jane');
|
|
15
|
+
});
|
|
16
|
+
it('should not notify subscribers when non-selected values change', () => {
|
|
17
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
18
|
+
const nameSelector = select(source, user => user.name);
|
|
19
|
+
const subscriber = vi.fn();
|
|
20
|
+
nameSelector.subscribe(subscriber);
|
|
21
|
+
// Reset the mock to ignore initial call
|
|
22
|
+
subscriber.mockReset();
|
|
23
|
+
// Update age only
|
|
24
|
+
source.set({ name: 'John', age: 26 });
|
|
25
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
it('should notify subscribers when selected value changes', () => {
|
|
28
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
29
|
+
const nameSelector = select(source, user => user.name);
|
|
30
|
+
const subscriber = vi.fn();
|
|
31
|
+
nameSelector.subscribe(subscriber);
|
|
32
|
+
// Reset the mock to ignore initial call
|
|
33
|
+
subscriber.mockReset();
|
|
34
|
+
source.set({ name: 'Jane', age: 25 });
|
|
35
|
+
expect(subscriber).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(subscriber).toHaveBeenCalledWith('Jane');
|
|
37
|
+
});
|
|
38
|
+
it('should prevent direct modifications to selector', () => {
|
|
39
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
40
|
+
const nameSelector = select(source, user => user.name);
|
|
41
|
+
expect(() => {
|
|
42
|
+
nameSelector.set('Jane');
|
|
43
|
+
}).toThrow('Cannot set values directly on a selector');
|
|
44
|
+
});
|
|
45
|
+
it('should work with complex selectors', () => {
|
|
46
|
+
const source = chunk({ user: { profile: { name: 'John' } } });
|
|
47
|
+
const nameSelector = select(source, state => state.user.profile.name);
|
|
48
|
+
expect(nameSelector.get()).toBe('John');
|
|
49
|
+
source.set({ user: { profile: { name: 'Jane' } } });
|
|
50
|
+
expect(nameSelector.get()).toBe('Jane');
|
|
51
|
+
});
|
|
52
|
+
it('should handle array selectors', () => {
|
|
53
|
+
const source = chunk({ items: [1, 2, 3] });
|
|
54
|
+
const firstItemSelector = select(source, state => state.items[0]);
|
|
55
|
+
expect(firstItemSelector.get()).toBe(1);
|
|
56
|
+
source.set({ items: [4, 2, 3] });
|
|
57
|
+
expect(firstItemSelector.get()).toBe(4);
|
|
58
|
+
});
|
|
59
|
+
it('should work with computed values', () => {
|
|
60
|
+
const source = chunk({ numbers: [1, 2, 3, 4, 5] });
|
|
61
|
+
const sumSelector = select(source, state => state.numbers.reduce((sum, num) => sum + num, 0));
|
|
62
|
+
expect(sumSelector.get()).toBe(15);
|
|
63
|
+
source.set({ numbers: [1, 2, 3] });
|
|
64
|
+
expect(sumSelector.get()).toBe(6);
|
|
65
|
+
});
|
|
66
|
+
it('should properly clean up subscriptions on destroy', () => {
|
|
67
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
68
|
+
const nameSelector = select(source, user => user.name);
|
|
69
|
+
const subscriber = vi.fn();
|
|
70
|
+
const unsubscribe = nameSelector.subscribe(subscriber);
|
|
71
|
+
// Reset mock to ignore initial call
|
|
72
|
+
subscriber.mockReset();
|
|
73
|
+
unsubscribe();
|
|
74
|
+
nameSelector.destroy();
|
|
75
|
+
source.set({ name: 'Jane', age: 25 });
|
|
76
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('should work with multiple independent selectors', () => {
|
|
79
|
+
const source = chunk({ name: 'John', age: 25 });
|
|
80
|
+
const nameSelector = select(source, user => user.name);
|
|
81
|
+
const ageSelector = select(source, user => user.age);
|
|
82
|
+
const nameSubscriber = vi.fn();
|
|
83
|
+
const ageSubscriber = vi.fn();
|
|
84
|
+
nameSelector.subscribe(nameSubscriber);
|
|
85
|
+
ageSelector.subscribe(ageSubscriber);
|
|
86
|
+
// Reset mocks to ignore initial calls
|
|
87
|
+
nameSubscriber.mockReset();
|
|
88
|
+
ageSubscriber.mockReset();
|
|
89
|
+
source.set({ name: 'John', age: 26 });
|
|
90
|
+
expect(nameSubscriber).not.toHaveBeenCalled();
|
|
91
|
+
expect(ageSubscriber).toHaveBeenCalledWith(26);
|
|
92
|
+
source.set({ name: 'Jane', age: 26 });
|
|
93
|
+
expect(nameSubscriber).toHaveBeenCalledWith('Jane');
|
|
94
|
+
expect(ageSubscriber).toHaveBeenCalledTimes(1); // Still from previous update
|
|
95
|
+
});
|
|
96
|
+
it("should not update if selected object has the same values (shallow equal)", () => {
|
|
97
|
+
const source = chunk({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
98
|
+
const detailsSelector = select(source, (user) => user.details, { useShallowEqual: true });
|
|
99
|
+
const callback = vi.fn();
|
|
100
|
+
detailsSelector.subscribe(callback);
|
|
101
|
+
callback.mockReset();
|
|
102
|
+
// Setting a new object with the same values
|
|
103
|
+
source.set({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
104
|
+
expect(callback).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
// Test without shallow equality
|
|
107
|
+
it("should update if selected object is new but has same values (without shallow equal)", () => {
|
|
108
|
+
const source = chunk({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
109
|
+
// Not using shallow equality here
|
|
110
|
+
const detailsSelector = select(source, (user) => user.details);
|
|
111
|
+
const callback = vi.fn();
|
|
112
|
+
detailsSelector.subscribe(callback);
|
|
113
|
+
// Reset mock to clear initial call
|
|
114
|
+
callback.mockReset();
|
|
115
|
+
// Setting a new object with the same values
|
|
116
|
+
source.set({ name: "John", details: { age: 25, city: "Lagos" } });
|
|
117
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
// Test nested derivation
|
|
120
|
+
it('should support nested derivation', () => {
|
|
121
|
+
const source = chunk({ user: { profile: { name: 'John' } } });
|
|
122
|
+
const profileSelector = select(source, (data) => data.user.profile);
|
|
123
|
+
const nameSelector = profileSelector.derive(profile => profile.name);
|
|
124
|
+
expect(nameSelector.get()).toBe('John');
|
|
125
|
+
source.set({ user: { profile: { name: 'Alice' } } });
|
|
126
|
+
expect(nameSelector.get()).toBe('Alice');
|
|
127
|
+
});
|
|
128
|
+
it('should pass options to nested selectors', () => {
|
|
129
|
+
const source = chunk({
|
|
130
|
+
user: {
|
|
131
|
+
profile: { details: { age: 30, city: 'New York' } }
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const profileSelector = select(source, (data) => data.user.profile, { useShallowEqual: true });
|
|
135
|
+
const detailsSelector = profileSelector.derive(profile => profile.details);
|
|
136
|
+
const callback = vi.fn();
|
|
137
|
+
detailsSelector.subscribe(callback);
|
|
138
|
+
// Reset mock to clear initial call
|
|
139
|
+
callback.mockReset();
|
|
140
|
+
// Update with new object but same values
|
|
141
|
+
source.set({
|
|
142
|
+
user: {
|
|
143
|
+
profile: { details: { age: 30, city: 'New York' } }
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
expect(callback).not.toHaveBeenCalled(); // Should NOT trigger due to shallow equality
|
|
147
|
+
});
|
|
148
|
+
// Test that set and reset throw errors
|
|
149
|
+
it('should throw error when trying to set or reset a selector', () => {
|
|
150
|
+
const source = chunk({ name: 'John' });
|
|
151
|
+
const nameSelector = select(source, (user) => user.name);
|
|
152
|
+
expect(() => nameSelector.set('Alice')).toThrow();
|
|
153
|
+
expect(() => nameSelector.reset()).toThrow();
|
|
154
|
+
});
|
|
155
|
+
// Test cleanup
|
|
156
|
+
it('should unsubscribe from source when destroyed', () => {
|
|
157
|
+
const source = chunk({ name: 'John' });
|
|
158
|
+
const nameSelector = select(source, (user) => user.name);
|
|
159
|
+
const callback = vi.fn();
|
|
160
|
+
nameSelector.subscribe(callback);
|
|
161
|
+
// Reset mock to clear initial call
|
|
162
|
+
callback.mockReset();
|
|
163
|
+
nameSelector.destroy();
|
|
164
|
+
source.set({ name: 'Alice' });
|
|
165
|
+
expect(callback).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
});
|
package/package.json
CHANGED
|
@@ -1,91 +1,97 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "stunk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue, Svelte, and Vanilla JS/TS applications.",
|
|
5
|
-
"scripts": {
|
|
6
|
-
"build": "tsc",
|
|
7
|
-
"test": "vitest",
|
|
8
|
-
"prepublishOnly": "vitest && tsc",
|
|
9
|
-
"prepare": "npm run build"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"@testing-library/
|
|
77
|
-
"@
|
|
78
|
-
"@
|
|
79
|
-
"@
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"react
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"react": "^19.0.0",
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "stunk",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Stunk is a lightweight, framework-agnostic state management library for JavaScript and TypeScript. It uses chunk-based state units for efficient updates, reactivity, and performance optimization in React, Vue, Svelte, and Vanilla JS/TS applications.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"test": "vitest",
|
|
8
|
+
"prepublishOnly": "vitest && tsc",
|
|
9
|
+
"prepare": "npm run build",
|
|
10
|
+
"lint": "eslint . --ext .js,.ts,.tsx,.vue"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/I-am-abdulazeez/stunk"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://stunk.vercel.app/",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/I-am-abdulazeez/stunk/issues"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./middleware": {
|
|
32
|
+
"import": "./dist/middleware/index.js",
|
|
33
|
+
"types": "./dist/types/middleware/index.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"import": "./dist/use-react/index.js",
|
|
37
|
+
"types": "./dist/types/use-react/index.d.ts"
|
|
38
|
+
},
|
|
39
|
+
"./vue": {
|
|
40
|
+
"import": "./dist/use-vue/index.js",
|
|
41
|
+
"types": "./dist/types/use-vue/index.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"state-management",
|
|
46
|
+
"atomic-state",
|
|
47
|
+
"chunk-based state",
|
|
48
|
+
"framework-agnostic",
|
|
49
|
+
"reactive state library",
|
|
50
|
+
"frontend state management",
|
|
51
|
+
"JavaScript state management",
|
|
52
|
+
"TypeScript state management",
|
|
53
|
+
"React state management",
|
|
54
|
+
"Vue state management",
|
|
55
|
+
"Svelte state management",
|
|
56
|
+
"recoil alternative",
|
|
57
|
+
"jotai alternative",
|
|
58
|
+
"zustand alternative",
|
|
59
|
+
"lightweight state management",
|
|
60
|
+
"state container",
|
|
61
|
+
"reusable state",
|
|
62
|
+
"efficient state updates",
|
|
63
|
+
"performance optimization",
|
|
64
|
+
"stunk",
|
|
65
|
+
"chunk"
|
|
66
|
+
],
|
|
67
|
+
"author": "AbdulAzeez",
|
|
68
|
+
"contributors": [
|
|
69
|
+
{
|
|
70
|
+
"name": "AbdulAzeez",
|
|
71
|
+
"url": "https://github.com/I-am-abdulazeez"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@testing-library/dom": "^10.4.0",
|
|
77
|
+
"@testing-library/react": "^16.2.0",
|
|
78
|
+
"@testing-library/vue": "^8.1.0",
|
|
79
|
+
"@types/react": "^19.0.10",
|
|
80
|
+
"@typescript-eslint/eslint-plugin": "^8.26.1",
|
|
81
|
+
"@typescript-eslint/parser": "^8.27.0",
|
|
82
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
83
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
84
|
+
"eslint": "^9.22.0",
|
|
85
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
86
|
+
"jsdom": "^26.0.0",
|
|
87
|
+
"react": "^19.0.0",
|
|
88
|
+
"react-dom": "^19.0.0",
|
|
89
|
+
"typescript": "^5.0.0",
|
|
90
|
+
"vitest": "^3.0.8",
|
|
91
|
+
"vue": "^3.5.13"
|
|
92
|
+
},
|
|
93
|
+
"peerDependencies": {
|
|
94
|
+
"react": "^19.0.0",
|
|
95
|
+
"vue": "^3.5.13"
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/core/computed.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { chunk } from "./core";
|
|
2
|
-
export function computed(dependencies, computeFn) {
|
|
3
|
-
const initialValues = dependencies.map(dep => dep.get());
|
|
4
|
-
let cachedValue = computeFn(...initialValues);
|
|
5
|
-
let isDirty = false;
|
|
6
|
-
let lastDependencyValues = [...initialValues];
|
|
7
|
-
const computedChunk = chunk(cachedValue);
|
|
8
|
-
const originalSet = computedChunk.set;
|
|
9
|
-
const recalculate = () => {
|
|
10
|
-
if (!isDirty)
|
|
11
|
-
return;
|
|
12
|
-
const currentValues = dependencies.map(dep => dep.get());
|
|
13
|
-
const hasChanges = currentValues.some((val, i) => val !== lastDependencyValues[i]);
|
|
14
|
-
if (hasChanges) {
|
|
15
|
-
lastDependencyValues = [...currentValues];
|
|
16
|
-
const newValue = computeFn(...currentValues);
|
|
17
|
-
if (newValue !== cachedValue) {
|
|
18
|
-
cachedValue = newValue;
|
|
19
|
-
originalSet(newValue);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
// Always clear the dirty flag after recalculation
|
|
23
|
-
isDirty = false;
|
|
24
|
-
};
|
|
25
|
-
computedChunk.get = () => {
|
|
26
|
-
if (isDirty) {
|
|
27
|
-
recalculate();
|
|
28
|
-
}
|
|
29
|
-
return cachedValue;
|
|
30
|
-
};
|
|
31
|
-
const unsub = dependencies.map(dep => dep.subscribe(() => {
|
|
32
|
-
if (!isDirty) {
|
|
33
|
-
isDirty = true;
|
|
34
|
-
recalculate();
|
|
35
|
-
}
|
|
36
|
-
}));
|
|
37
|
-
return {
|
|
38
|
-
...computedChunk,
|
|
39
|
-
isDirty: () => isDirty,
|
|
40
|
-
recompute: () => {
|
|
41
|
-
isDirty = true;
|
|
42
|
-
recalculate();
|
|
43
|
-
},
|
|
44
|
-
set: () => {
|
|
45
|
-
throw new Error('Cannot set values directly on computed. Modify the source chunk instead.');
|
|
46
|
-
},
|
|
47
|
-
destroy: () => {
|
|
48
|
-
unsub.forEach(cleanup => cleanup());
|
|
49
|
-
if (computedChunk.destroy) {
|
|
50
|
-
computedChunk.destroy();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
}
|
package/dist/core/selector.d.ts
DELETED
package/dist/core/selector.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { computed } from "./computed";
|
|
2
|
-
export function select(sourceChunk, selector) {
|
|
3
|
-
return {
|
|
4
|
-
...computed([sourceChunk], (value) => selector(value)),
|
|
5
|
-
set: () => {
|
|
6
|
-
throw new Error('Cannot set values directly on a selector. Modify the source chunk instead.');
|
|
7
|
-
},
|
|
8
|
-
};
|
|
9
|
-
}
|