@dxos/effect-atom-solid 0.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/LICENSE +8 -0
- package/README.md +1 -0
- package/package.json +42 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAtom.test.tsx +49 -0
- package/src/hooks/useAtom.ts +39 -0
- package/src/hooks/useAtomInitialValues.test.tsx +31 -0
- package/src/hooks/useAtomInitialValues.ts +18 -0
- package/src/hooks/useAtomMount.ts +17 -0
- package/src/hooks/useAtomRef.ts +35 -0
- package/src/hooks/useAtomRefresh.test.tsx +49 -0
- package/src/hooks/useAtomRefresh.ts +20 -0
- package/src/hooks/useAtomResource.test.tsx +43 -0
- package/src/hooks/useAtomResource.ts +58 -0
- package/src/hooks/useAtomSet.test.tsx +71 -0
- package/src/hooks/useAtomSet.ts +64 -0
- package/src/hooks/useAtomSubscribe.test.tsx +52 -0
- package/src/hooks/useAtomSubscribe.ts +22 -0
- package/src/hooks/useAtomSuspense.test.tsx +46 -0
- package/src/hooks/useAtomSuspense.ts +74 -0
- package/src/hooks/useAtomValue.test.tsx +67 -0
- package/src/hooks/useAtomValue.ts +57 -0
- package/src/index.ts +13 -0
- package/src/registry.test.tsx +53 -0
- package/src/registry.ts +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2025 DXOS
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @dxos/effect-atom-solid
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/effect-atom-solid",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Solid.js bindings for Effect Atom",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "DXOS.org",
|
|
9
|
+
"sideEffects": true,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/types/src/index.d.ts",
|
|
15
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
16
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"types": "dist/types/src/index.d.ts",
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@effect-atom/atom": "^0.4.10",
|
|
29
|
+
"@solidjs/testing-library": "^0.8.10",
|
|
30
|
+
"effect": "3.19.11",
|
|
31
|
+
"solid-js": "^1.9.9",
|
|
32
|
+
"vite-plugin-solid": "^2.11.10"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@effect-atom/atom": "^0.4.10",
|
|
36
|
+
"effect": "3.19.11",
|
|
37
|
+
"solid-js": "^1.9.9"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export * from './useAtomValue';
|
|
6
|
+
export * from './useAtomSet';
|
|
7
|
+
export * from './useAtom';
|
|
8
|
+
export * from './useAtomMount';
|
|
9
|
+
export * from './useAtomRefresh';
|
|
10
|
+
export * from './useAtomSubscribe';
|
|
11
|
+
export * from './useAtomRef';
|
|
12
|
+
export * from './useAtomResource';
|
|
13
|
+
export * from './useAtomSuspense';
|
|
14
|
+
export * from './useAtomInitialValues';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { fireEvent, render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtom } from './useAtom';
|
|
12
|
+
|
|
13
|
+
describe('useAtom', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Reset the default registry between tests
|
|
16
|
+
defaultRegistry.reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('returns both value and setter', async () => {
|
|
20
|
+
const countAtom = Atom.make(0);
|
|
21
|
+
|
|
22
|
+
function TestComponent() {
|
|
23
|
+
const [count, setCount] = useAtom(countAtom);
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<span data-testid='count'>{count()}</span>
|
|
27
|
+
<button data-testid='increment' onClick={() => setCount((c) => c + 1)}>
|
|
28
|
+
+
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
35
|
+
expect(getByTestId('count').textContent).toBe('0');
|
|
36
|
+
|
|
37
|
+
fireEvent.click(getByTestId('increment'));
|
|
38
|
+
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(getByTestId('count').textContent).toBe('1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
fireEvent.click(getByTestId('increment'));
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(getByTestId('count').textContent).toBe('2');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import type * as Result from '@effect-atom/atom/Result';
|
|
7
|
+
import { type Accessor, createSignal, onCleanup } from 'solid-js';
|
|
8
|
+
|
|
9
|
+
import { useRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { type SetAtomFn, createSetAtom } from './useAtomSet';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to both read and write an atom
|
|
15
|
+
* Returns a tuple of [value accessor, setter function]
|
|
16
|
+
*/
|
|
17
|
+
export function useAtom<R, W, Mode extends 'value' | 'promise' | 'promiseExit' = never>(
|
|
18
|
+
atom: Atom.Writable<R, W>,
|
|
19
|
+
options?: {
|
|
20
|
+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : 'value') | undefined;
|
|
21
|
+
},
|
|
22
|
+
): readonly [Accessor<R>, SetAtomFn<R, W, Mode>] {
|
|
23
|
+
const registry = useRegistry();
|
|
24
|
+
|
|
25
|
+
const [value, setValue] = createSignal<R>(registry.get(atom));
|
|
26
|
+
|
|
27
|
+
// Subscribe to atom changes
|
|
28
|
+
const unsubscribe = registry.subscribe(
|
|
29
|
+
atom,
|
|
30
|
+
(nextValue) => {
|
|
31
|
+
setValue(() => nextValue);
|
|
32
|
+
},
|
|
33
|
+
{ immediate: true },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
onCleanup(unsubscribe);
|
|
37
|
+
|
|
38
|
+
return [value, createSetAtom(registry, atom, options)] as const;
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { render } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtomInitialValues } from './useAtomInitialValues';
|
|
12
|
+
import { useAtomValue } from './useAtomValue';
|
|
13
|
+
|
|
14
|
+
describe('useAtomInitialValues', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
defaultRegistry.reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('initializes atoms', () => {
|
|
20
|
+
const atom = Atom.make(0);
|
|
21
|
+
|
|
22
|
+
function TestComponent() {
|
|
23
|
+
useAtomInitialValues([[atom, 42]]);
|
|
24
|
+
const value = useAtomValue(atom);
|
|
25
|
+
return <span data-testid='val'>{value()}</span>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
29
|
+
expect(getByTestId('val').textContent).toBe('42');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
|
|
7
|
+
import { useRegistry } from '../registry';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to initialize atoms with values
|
|
11
|
+
*/
|
|
12
|
+
export function useAtomInitialValues(initialValues: Iterable<readonly [Atom.Writable<any, any>, any]>): void {
|
|
13
|
+
const registry = useRegistry();
|
|
14
|
+
|
|
15
|
+
for (const [atom, value] of initialValues) {
|
|
16
|
+
registry.set(atom, value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { onCleanup } from 'solid-js';
|
|
7
|
+
|
|
8
|
+
import { useRegistry } from '../registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to mount an atom without reading its value
|
|
12
|
+
*/
|
|
13
|
+
export function useAtomMount<A>(atom: Atom.Atom<A>): void {
|
|
14
|
+
const registry = useRegistry();
|
|
15
|
+
const unmount = registry.mount(atom);
|
|
16
|
+
onCleanup(unmount);
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as AtomRef from '@effect-atom/atom/AtomRef';
|
|
6
|
+
import { type Accessor, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to read an AtomRef value
|
|
10
|
+
*/
|
|
11
|
+
export function useAtomRef<A>(ref: AtomRef.ReadonlyRef<A>): Accessor<A> {
|
|
12
|
+
const [value, setValue] = createSignal<A>(ref.value);
|
|
13
|
+
|
|
14
|
+
const unsubscribe = ref.subscribe((next) => {
|
|
15
|
+
setValue(() => next);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
onCleanup(unsubscribe);
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hook to get a prop accessor from an AtomRef
|
|
25
|
+
*/
|
|
26
|
+
export function useAtomRefProp<A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): AtomRef.AtomRef<A[K]> {
|
|
27
|
+
return createMemo(() => ref.prop(prop))();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook to read a prop value from an AtomRef
|
|
32
|
+
*/
|
|
33
|
+
export function useAtomRefPropValue<A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K): Accessor<A[K]> {
|
|
34
|
+
return useAtomRef(useAtomRefProp(ref, prop));
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { fireEvent, render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtomRefresh } from './useAtomRefresh';
|
|
12
|
+
import { useAtomValue } from './useAtomValue';
|
|
13
|
+
|
|
14
|
+
describe('useAtomRefresh', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset the default registry between tests
|
|
17
|
+
defaultRegistry.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns a refresh function', async () => {
|
|
21
|
+
let callCount = 0;
|
|
22
|
+
const computedAtom = Atom.make(() => {
|
|
23
|
+
callCount++;
|
|
24
|
+
return callCount;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function TestComponent() {
|
|
28
|
+
const value = useAtomValue(computedAtom);
|
|
29
|
+
const refresh = useAtomRefresh(computedAtom);
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<span data-testid='value'>{value()}</span>
|
|
33
|
+
<button data-testid='refresh' onClick={refresh}>
|
|
34
|
+
Refresh
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
41
|
+
expect(getByTestId('value').textContent).toBe('1');
|
|
42
|
+
|
|
43
|
+
fireEvent.click(getByTestId('refresh'));
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(getByTestId('value').textContent).toBe('2');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { onCleanup } from 'solid-js';
|
|
7
|
+
|
|
8
|
+
import { useRegistry } from '../registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to get a refresh function for an atom
|
|
12
|
+
*/
|
|
13
|
+
export function useAtomRefresh<A>(atom: Atom.Atom<A>): () => void {
|
|
14
|
+
const registry = useRegistry();
|
|
15
|
+
|
|
16
|
+
const unmount = registry.mount(atom);
|
|
17
|
+
onCleanup(unmount);
|
|
18
|
+
|
|
19
|
+
return () => registry.refresh(atom);
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { defaultRegistry } from '../registry';
|
|
11
|
+
|
|
12
|
+
import { useAtomResource } from './useAtomResource';
|
|
13
|
+
|
|
14
|
+
describe('useAtomResource', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset the default registry between tests
|
|
17
|
+
defaultRegistry.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('handles loading, success states for Result atoms', async () => {
|
|
21
|
+
const dataAtom = Atom.make(Effect.succeed(42));
|
|
22
|
+
|
|
23
|
+
function TestComponent() {
|
|
24
|
+
const { value, loading, error } = useAtomResource(dataAtom);
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<span data-testid='loading'>{loading() ? 'true' : 'false'}</span>
|
|
28
|
+
<span data-testid='value'>{value() ?? 'none'}</span>
|
|
29
|
+
<span data-testid='error'>{error() ? 'has error' : 'no error'}</span>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
35
|
+
|
|
36
|
+
// Wait for the effect to resolve
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(getByTestId('value').textContent).toBe('42');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(getByTestId('error').textContent).toBe('no error');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import type * as Result from '@effect-atom/atom/Result';
|
|
7
|
+
import * as Cause from 'effect/Cause';
|
|
8
|
+
import { type Accessor, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
9
|
+
|
|
10
|
+
import { useRegistry } from '../registry';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resource-like hook for atoms that contain Result values
|
|
14
|
+
* Automatically handles loading and error states
|
|
15
|
+
*/
|
|
16
|
+
export function useAtomResource<A, E>(
|
|
17
|
+
atom: Atom.Atom<Result.Result<A, E>>,
|
|
18
|
+
): {
|
|
19
|
+
value: Accessor<A | undefined>;
|
|
20
|
+
error: Accessor<E | undefined>;
|
|
21
|
+
loading: Accessor<boolean>;
|
|
22
|
+
result: Accessor<Result.Result<A, E>>;
|
|
23
|
+
} {
|
|
24
|
+
const registry = useRegistry();
|
|
25
|
+
const [result, setResult] = createSignal<Result.Result<A, E>>(registry.get(atom));
|
|
26
|
+
|
|
27
|
+
const unsubscribe = registry.subscribe(
|
|
28
|
+
atom,
|
|
29
|
+
(nextValue) => {
|
|
30
|
+
setResult(() => nextValue);
|
|
31
|
+
},
|
|
32
|
+
{ immediate: true },
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
onCleanup(unsubscribe);
|
|
36
|
+
|
|
37
|
+
const value = createMemo(() => {
|
|
38
|
+
const r = result();
|
|
39
|
+
return r._tag === 'Success' ? r.value : undefined;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const error = createMemo(() => {
|
|
43
|
+
const r = result();
|
|
44
|
+
return r._tag === 'Failure' ? (Cause.squash(r.cause) as E) : undefined;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const loading = createMemo(() => {
|
|
48
|
+
const r = result();
|
|
49
|
+
return r._tag === 'Initial' || r.waiting;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
value,
|
|
54
|
+
error,
|
|
55
|
+
loading,
|
|
56
|
+
result,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { fireEvent, render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtomSet } from './useAtomSet';
|
|
12
|
+
import { useAtomValue } from './useAtomValue';
|
|
13
|
+
|
|
14
|
+
describe('useAtomSet', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset the default registry between tests
|
|
17
|
+
defaultRegistry.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns a setter function', async () => {
|
|
21
|
+
const countAtom = Atom.make(0);
|
|
22
|
+
|
|
23
|
+
function TestComponent() {
|
|
24
|
+
const setCount = useAtomSet(countAtom);
|
|
25
|
+
const count = useAtomValue(countAtom);
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<span data-testid='count'>{count()}</span>
|
|
29
|
+
<button data-testid='set' onClick={() => setCount(10)}>
|
|
30
|
+
Set to 10
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
37
|
+
expect(getByTestId('count').textContent).toBe('0');
|
|
38
|
+
|
|
39
|
+
fireEvent.click(getByTestId('set'));
|
|
40
|
+
|
|
41
|
+
await waitFor(() => {
|
|
42
|
+
expect(getByTestId('count').textContent).toBe('10');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('supports updater function', async () => {
|
|
47
|
+
const countAtom = Atom.make(5);
|
|
48
|
+
|
|
49
|
+
function TestComponent() {
|
|
50
|
+
const setCount = useAtomSet(countAtom);
|
|
51
|
+
const count = useAtomValue(countAtom);
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<span data-testid='count'>{count()}</span>
|
|
55
|
+
<button data-testid='double' onClick={() => setCount((c) => c * 2)}>
|
|
56
|
+
Double
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
63
|
+
expect(getByTestId('count').textContent).toBe('5');
|
|
64
|
+
|
|
65
|
+
fireEvent.click(getByTestId('double'));
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(getByTestId('count').textContent).toBe('10');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import * as Registry from '@effect-atom/atom/Registry';
|
|
7
|
+
import type * as Result from '@effect-atom/atom/Result';
|
|
8
|
+
import * as Cause from 'effect/Cause';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Exit from 'effect/Exit';
|
|
11
|
+
import { onCleanup } from 'solid-js';
|
|
12
|
+
|
|
13
|
+
import { useRegistry } from '../registry';
|
|
14
|
+
|
|
15
|
+
const flattenExit = <A, E>(exit: Exit.Exit<A, E>): A => {
|
|
16
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
17
|
+
throw Cause.squash(exit.cause);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SetAtomFn<R, W, Mode extends 'value' | 'promise' | 'promiseExit'> = 'promise' extends Mode
|
|
21
|
+
? (value: W) => Promise<Result.Result.Success<R>>
|
|
22
|
+
: 'promiseExit' extends Mode
|
|
23
|
+
? (value: W) => Promise<Exit.Exit<Result.Result.Success<R>, Result.Result.Failure<R>>>
|
|
24
|
+
: (value: W | ((value: R) => W)) => void;
|
|
25
|
+
|
|
26
|
+
export function createSetAtom<R, W, Mode extends 'value' | 'promise' | 'promiseExit' = never>(
|
|
27
|
+
registry: Registry.Registry,
|
|
28
|
+
atom: Atom.Writable<R, W>,
|
|
29
|
+
options?: {
|
|
30
|
+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : 'value') | undefined;
|
|
31
|
+
},
|
|
32
|
+
): SetAtomFn<R, W, Mode> {
|
|
33
|
+
if (options?.mode === 'promise' || options?.mode === 'promiseExit') {
|
|
34
|
+
return ((value: W) => {
|
|
35
|
+
registry.set(atom, value);
|
|
36
|
+
const promise = Effect.runPromiseExit(
|
|
37
|
+
Registry.getResult(registry, atom as Atom.Atom<Result.Result<any, any>>, { suspendOnWaiting: true }),
|
|
38
|
+
);
|
|
39
|
+
return options!.mode === 'promise' ? promise.then(flattenExit) : promise;
|
|
40
|
+
}) as SetAtomFn<R, W, Mode>;
|
|
41
|
+
}
|
|
42
|
+
return ((value: W | ((value: R) => W)) => {
|
|
43
|
+
registry.set(atom, typeof value === 'function' ? (value as any)(registry.get(atom)) : value);
|
|
44
|
+
}) as SetAtomFn<R, W, Mode>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook to get a setter function for an atom
|
|
49
|
+
* Also mounts the atom in the registry
|
|
50
|
+
*/
|
|
51
|
+
export function useAtomSet<R, W, Mode extends 'value' | 'promise' | 'promiseExit' = never>(
|
|
52
|
+
atom: Atom.Writable<R, W>,
|
|
53
|
+
options?: {
|
|
54
|
+
readonly mode?: ([R] extends [Result.Result<any, any>] ? Mode : 'value') | undefined;
|
|
55
|
+
},
|
|
56
|
+
): SetAtomFn<R, W, Mode> {
|
|
57
|
+
const registry = useRegistry();
|
|
58
|
+
|
|
59
|
+
// Mount the atom
|
|
60
|
+
const unmount = registry.mount(atom);
|
|
61
|
+
onCleanup(unmount);
|
|
62
|
+
|
|
63
|
+
return createSetAtom(registry, atom, options);
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { fireEvent, render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtomSet } from './useAtomSet';
|
|
12
|
+
import { useAtomSubscribe } from './useAtomSubscribe';
|
|
13
|
+
|
|
14
|
+
describe('useAtomSubscribe', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// Reset the default registry between tests
|
|
17
|
+
defaultRegistry.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('calls callback on value changes', async () => {
|
|
21
|
+
const countAtom = Atom.make(0);
|
|
22
|
+
const values: number[] = [];
|
|
23
|
+
|
|
24
|
+
function TestComponent() {
|
|
25
|
+
useAtomSubscribe(
|
|
26
|
+
countAtom,
|
|
27
|
+
(value) => {
|
|
28
|
+
values.push(value);
|
|
29
|
+
},
|
|
30
|
+
{ immediate: true },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const setCount = useAtomSet(countAtom);
|
|
34
|
+
return (
|
|
35
|
+
<button data-testid='increment' onClick={() => setCount((c) => c + 1)}>
|
|
36
|
+
+
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
42
|
+
|
|
43
|
+
// Should have received the initial value
|
|
44
|
+
expect(values).toContain(0);
|
|
45
|
+
|
|
46
|
+
fireEvent.click(getByTestId('increment'));
|
|
47
|
+
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(values).toContain(1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { onCleanup } from 'solid-js';
|
|
7
|
+
|
|
8
|
+
import { useRegistry } from '../registry';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to subscribe to atom changes with a callback
|
|
12
|
+
*/
|
|
13
|
+
export function useAtomSubscribe<A>(
|
|
14
|
+
atom: Atom.Atom<A>,
|
|
15
|
+
f: (value: A) => void,
|
|
16
|
+
options?: { readonly immediate?: boolean },
|
|
17
|
+
): void {
|
|
18
|
+
const registry = useRegistry();
|
|
19
|
+
|
|
20
|
+
const unsubscribe = registry.subscribe(atom, f, options);
|
|
21
|
+
onCleanup(unsubscribe);
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import * as Result from '@effect-atom/atom/Result';
|
|
7
|
+
import { render, waitFor } from '@solidjs/testing-library';
|
|
8
|
+
import { Suspense } from 'solid-js';
|
|
9
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { defaultRegistry } from '../registry';
|
|
12
|
+
|
|
13
|
+
import { useAtomSuspense } from './useAtomSuspense';
|
|
14
|
+
|
|
15
|
+
describe('useAtomSuspense', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
defaultRegistry.reset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('suspends while waiting for value', async () => {
|
|
21
|
+
const atom = Atom.make<Result.Result<string, never>>(Result.initial());
|
|
22
|
+
|
|
23
|
+
function Child() {
|
|
24
|
+
const value = useAtomSuspense(atom);
|
|
25
|
+
return <span data-testid='value'>{value()}</span>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TestComponent() {
|
|
29
|
+
return (
|
|
30
|
+
<Suspense fallback={<span data-testid='loading'>Loading</span>}>
|
|
31
|
+
<Child />
|
|
32
|
+
</Suspense>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
37
|
+
expect(getByTestId('loading')).toBeTruthy();
|
|
38
|
+
|
|
39
|
+
// Update atom
|
|
40
|
+
defaultRegistry.set(atom, Result.success('ready'));
|
|
41
|
+
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(getByTestId('value').textContent).toBe('ready');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import type * as Result from '@effect-atom/atom/Result';
|
|
7
|
+
import { createResource, onCleanup } from 'solid-js';
|
|
8
|
+
|
|
9
|
+
import { useRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Hook to read an atom value with Suspense support
|
|
13
|
+
*/
|
|
14
|
+
export function useAtomSuspense<A, E>(atom: Atom.Atom<Result.Result<A, E>>): () => A {
|
|
15
|
+
const registry = useRegistry();
|
|
16
|
+
|
|
17
|
+
const [data, { mutate, refetch }] = createResource(async () => {
|
|
18
|
+
// Check if we already have a value
|
|
19
|
+
const current = registry.get(atom);
|
|
20
|
+
if (current._tag === 'Success') {
|
|
21
|
+
return current.value;
|
|
22
|
+
}
|
|
23
|
+
if (current._tag === 'Failure') {
|
|
24
|
+
throw current.cause;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wait for the next success or failure
|
|
28
|
+
return new Promise<A>((resolve, reject) => {
|
|
29
|
+
const unsubscribe = registry.subscribe(atom, (next) => {
|
|
30
|
+
if (next._tag === 'Success') {
|
|
31
|
+
unsubscribe();
|
|
32
|
+
resolve(next.value);
|
|
33
|
+
} else if (next._tag === 'Failure') {
|
|
34
|
+
unsubscribe();
|
|
35
|
+
reject(next.cause);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// Note: We might want to handle cancellation if the resource is disposed?
|
|
39
|
+
// But creating a promise that leaks isn't great.
|
|
40
|
+
// Ideally we tie this to the component, but the promise itself is localized.
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Subscribe to updates to keep the resource fresh
|
|
45
|
+
const unsubscribe = registry.subscribe(atom, (next) => {
|
|
46
|
+
if (next._tag === 'Success') {
|
|
47
|
+
mutate(() => next.value);
|
|
48
|
+
} else if (next._tag === 'Failure') {
|
|
49
|
+
// If we encounter a failure, we trigger a refetch which will hit the fetcher
|
|
50
|
+
// and throw the error (reject the promise)
|
|
51
|
+
// Or we could try to mutate error state?
|
|
52
|
+
// createResource doesn't have a direct 'setError'.
|
|
53
|
+
// Refetching is the safe bet to re-enter the promise/error flow.
|
|
54
|
+
void refetch();
|
|
55
|
+
} else if (next._tag === 'Initial' || (next as any).waiting) {
|
|
56
|
+
// If we go back to loading, refetch to suspend
|
|
57
|
+
void refetch();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
onCleanup(unsubscribe);
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
const value = data();
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
// This case handles when the resource is loading initially
|
|
67
|
+
// createResource reads undefined (or initial value) when loading if strict mode isn't on for types,
|
|
68
|
+
// but wrapping it ensures we signal "read" to Suspense.
|
|
69
|
+
// However, data() itself should trigger Suspense if loading.
|
|
70
|
+
// We assume standard Suspense behavior.
|
|
71
|
+
}
|
|
72
|
+
return value as A;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import { fireEvent, render, waitFor } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { defaultRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
import { useAtomValue } from './useAtomValue';
|
|
12
|
+
|
|
13
|
+
describe('useAtomValue', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Reset the default registry between tests
|
|
16
|
+
defaultRegistry.reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('reads initial atom value', () => {
|
|
20
|
+
const countAtom = Atom.make(42);
|
|
21
|
+
|
|
22
|
+
function TestComponent() {
|
|
23
|
+
const count = useAtomValue(countAtom);
|
|
24
|
+
return <div data-testid='count'>{count()}</div>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
28
|
+
expect(getByTestId('count').textContent).toBe('42');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('updates when atom value changes', async () => {
|
|
32
|
+
const countAtom = Atom.make(0);
|
|
33
|
+
|
|
34
|
+
function TestComponent() {
|
|
35
|
+
const count = useAtomValue(countAtom);
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<span data-testid='count'>{count()}</span>
|
|
39
|
+
<button data-testid='increment' onClick={() => defaultRegistry.set(countAtom, count() + 1)}>
|
|
40
|
+
+
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
47
|
+
expect(getByTestId('count').textContent).toBe('0');
|
|
48
|
+
|
|
49
|
+
fireEvent.click(getByTestId('increment'));
|
|
50
|
+
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(getByTestId('count').textContent).toBe('1');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('supports mapping function', () => {
|
|
57
|
+
const countAtom = Atom.make(5);
|
|
58
|
+
|
|
59
|
+
function TestComponent() {
|
|
60
|
+
const doubled = useAtomValue(countAtom, (n) => n * 2);
|
|
61
|
+
return <div data-testid='doubled'>{doubled()}</div>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { getByTestId } = render(() => <TestComponent />);
|
|
65
|
+
expect(getByTestId('doubled').textContent).toBe('10');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import * as AtomModule from '@effect-atom/atom/Atom';
|
|
7
|
+
import { type Accessor, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
8
|
+
|
|
9
|
+
import { useRegistry } from '../registry';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A value that may be a static value or a reactive accessor.
|
|
13
|
+
*/
|
|
14
|
+
type MaybeAccessor<T> = T | Accessor<T>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a MaybeAccessor to its value.
|
|
18
|
+
*/
|
|
19
|
+
const access = <T>(value: MaybeAccessor<T>): T => (typeof value === 'function' ? (value as Accessor<T>)() : value);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to read the value of an atom.
|
|
23
|
+
* The returned accessor will update whenever the atom's value changes.
|
|
24
|
+
* The atom parameter can be reactive (MaybeAccessor) - if the atom changes,
|
|
25
|
+
* the hook will automatically unsubscribe from the old atom and subscribe to the new one.
|
|
26
|
+
*/
|
|
27
|
+
export function useAtomValue<A>(atom: MaybeAccessor<Atom.Atom<A>>): Accessor<A>;
|
|
28
|
+
export function useAtomValue<A, B>(atom: MaybeAccessor<Atom.Atom<A>>, f: (a: A) => B): Accessor<B>;
|
|
29
|
+
export function useAtomValue<A>(atom: MaybeAccessor<Atom.Atom<A>>, f?: (a: A) => A): Accessor<A> {
|
|
30
|
+
const registry = useRegistry();
|
|
31
|
+
|
|
32
|
+
// Resolve the atom reactively and apply mapping if provided.
|
|
33
|
+
const resolvedAtom = createMemo(() => {
|
|
34
|
+
const a = access(atom);
|
|
35
|
+
return f ? AtomModule.map(a, f) : a;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const [value, setValue] = createSignal<A>(registry.get(resolvedAtom()));
|
|
39
|
+
|
|
40
|
+
// Subscribe to atom changes reactively - re-subscribes when atom changes.
|
|
41
|
+
createEffect(() => {
|
|
42
|
+
const currentAtom = resolvedAtom();
|
|
43
|
+
setValue(() => registry.get(currentAtom));
|
|
44
|
+
|
|
45
|
+
const unsubscribe = registry.subscribe(
|
|
46
|
+
currentAtom,
|
|
47
|
+
(nextValue) => {
|
|
48
|
+
setValue(() => nextValue);
|
|
49
|
+
},
|
|
50
|
+
{ immediate: true },
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
onCleanup(unsubscribe);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return value;
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
export * as Registry from '@effect-atom/atom/Registry';
|
|
7
|
+
export * as Result from '@effect-atom/atom/Result';
|
|
8
|
+
export * as AtomRef from '@effect-atom/atom/AtomRef';
|
|
9
|
+
export * as AtomHttpApi from '@effect-atom/atom/AtomHttpApi';
|
|
10
|
+
export * as AtomRpc from '@effect-atom/atom/AtomRpc';
|
|
11
|
+
|
|
12
|
+
export * from './hooks';
|
|
13
|
+
export * from './registry';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Registry from '@effect-atom/atom/Registry';
|
|
6
|
+
import { render } from '@solidjs/testing-library';
|
|
7
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { RegistryProvider, defaultRegistry, useRegistry } from './registry';
|
|
10
|
+
|
|
11
|
+
describe('registry', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Reset the default registry between tests
|
|
14
|
+
defaultRegistry.reset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('useRegistry', () => {
|
|
18
|
+
test('returns the default registry', () => {
|
|
19
|
+
let capturedRegistry: Registry.Registry | null = null;
|
|
20
|
+
|
|
21
|
+
function TestComponent() {
|
|
22
|
+
capturedRegistry = useRegistry();
|
|
23
|
+
return <div>test</div>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render(() => <TestComponent />);
|
|
27
|
+
expect(capturedRegistry).toBe(defaultRegistry);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('RegistryProvider', () => {
|
|
32
|
+
test('provides a custom registry to children', () => {
|
|
33
|
+
const customRegistry = Registry.make();
|
|
34
|
+
let capturedRegistry: Registry.Registry | null = null;
|
|
35
|
+
|
|
36
|
+
function Child() {
|
|
37
|
+
capturedRegistry = useRegistry();
|
|
38
|
+
return <div>child</div>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render(() => (
|
|
42
|
+
<RegistryProvider registry={customRegistry}>
|
|
43
|
+
<Child />
|
|
44
|
+
</RegistryProvider>
|
|
45
|
+
));
|
|
46
|
+
|
|
47
|
+
expect(capturedRegistry).toBe(customRegistry);
|
|
48
|
+
|
|
49
|
+
// Clean up
|
|
50
|
+
customRegistry.dispose();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Atom from '@effect-atom/atom/Atom';
|
|
6
|
+
import * as Registry from '@effect-atom/atom/Registry';
|
|
7
|
+
import * as GlobalValue from 'effect/GlobalValue';
|
|
8
|
+
import { type Context, createContext, onCleanup, useContext } from 'solid-js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default registry instance
|
|
12
|
+
*/
|
|
13
|
+
export const defaultRegistry: Registry.Registry = GlobalValue.globalValue(
|
|
14
|
+
'@effect-atom/atom-solid/defaultRegistry',
|
|
15
|
+
() => Registry.make(),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Solid context for the atom registry
|
|
20
|
+
*/
|
|
21
|
+
export const RegistryContext: Context<Registry.Registry> = createContext<Registry.Registry>(defaultRegistry);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the current registry from context
|
|
25
|
+
*/
|
|
26
|
+
export const useRegistry = (): Registry.Registry => {
|
|
27
|
+
return useContext(RegistryContext);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Provider component for custom registry
|
|
32
|
+
*/
|
|
33
|
+
export interface RegistryProviderProps {
|
|
34
|
+
children: any;
|
|
35
|
+
registry?: Registry.Registry;
|
|
36
|
+
initialValues?: Iterable<readonly [Atom.Atom<any>, any]>;
|
|
37
|
+
scheduleTask?: (f: () => void) => void;
|
|
38
|
+
timeoutResolution?: number;
|
|
39
|
+
defaultIdleTTL?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function RegistryProvider(props: RegistryProviderProps) {
|
|
43
|
+
const registry =
|
|
44
|
+
props.registry ??
|
|
45
|
+
Registry.make({
|
|
46
|
+
scheduleTask: props.scheduleTask,
|
|
47
|
+
initialValues: props.initialValues,
|
|
48
|
+
timeoutResolution: props.timeoutResolution,
|
|
49
|
+
defaultIdleTTL: props.defaultIdleTTL ?? 400,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
onCleanup(() => {
|
|
53
|
+
// Delay disposal to allow for component re-mounting
|
|
54
|
+
const timeout = setTimeout(() => {
|
|
55
|
+
registry.dispose();
|
|
56
|
+
}, 500);
|
|
57
|
+
return () => clearTimeout(timeout);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return RegistryContext.Provider({
|
|
61
|
+
value: registry,
|
|
62
|
+
get children() {
|
|
63
|
+
return props.children;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|