signalium 1.0.2 → 1.1.1
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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +18 -0
- package/dist/cjs/config.d.ts +2 -2
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/hooks.d.ts.map +1 -1
- package/dist/cjs/hooks.js +14 -2
- package/dist/cjs/hooks.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/internals/async.d.ts +1 -1
- package/dist/cjs/internals/async.d.ts.map +1 -1
- package/dist/cjs/internals/async.js +20 -15
- package/dist/cjs/internals/async.js.map +1 -1
- package/dist/cjs/internals/connect.d.ts.map +1 -1
- package/dist/cjs/internals/connect.js +6 -0
- package/dist/cjs/internals/connect.js.map +1 -1
- package/dist/cjs/internals/contexts.d.ts +9 -2
- package/dist/cjs/internals/contexts.d.ts.map +1 -1
- package/dist/cjs/internals/contexts.js +37 -3
- package/dist/cjs/internals/contexts.js.map +1 -1
- package/dist/cjs/internals/derived.d.ts +15 -5
- package/dist/cjs/internals/derived.d.ts.map +1 -1
- package/dist/cjs/internals/derived.js +11 -11
- package/dist/cjs/internals/derived.js.map +1 -1
- package/dist/cjs/internals/get.js +2 -2
- package/dist/cjs/internals/get.js.map +1 -1
- package/dist/cjs/internals/scheduling.d.ts +2 -0
- package/dist/cjs/internals/scheduling.d.ts.map +1 -1
- package/dist/cjs/internals/scheduling.js +16 -1
- package/dist/cjs/internals/scheduling.js.map +1 -1
- package/dist/cjs/internals/state.d.ts +2 -2
- package/dist/cjs/internals/state.d.ts.map +1 -1
- package/dist/cjs/internals/utils/equals.d.ts +2 -0
- package/dist/cjs/internals/utils/equals.d.ts.map +1 -1
- package/dist/cjs/internals/utils/equals.js +5 -3
- package/dist/cjs/internals/utils/equals.js.map +1 -1
- package/dist/cjs/react/context.d.ts.map +1 -1
- package/dist/cjs/react/context.js +5 -0
- package/dist/cjs/react/context.js.map +1 -1
- package/dist/cjs/react/provider.d.ts +2 -3
- package/dist/cjs/react/provider.d.ts.map +1 -1
- package/dist/cjs/react/provider.js +2 -6
- package/dist/cjs/react/provider.js.map +1 -1
- package/dist/cjs/react/rendering.d.ts +2 -0
- package/dist/cjs/react/rendering.d.ts.map +1 -0
- package/dist/cjs/react/rendering.js +25 -0
- package/dist/cjs/react/rendering.js.map +1 -0
- package/dist/cjs/react/signal-value.d.ts +1 -1
- package/dist/cjs/react/signal-value.d.ts.map +1 -1
- package/dist/cjs/react/signal-value.js +4 -53
- package/dist/cjs/react/signal-value.js.map +1 -1
- package/dist/cjs/react/state.d.ts +2 -2
- package/dist/cjs/react/state.d.ts.map +1 -1
- package/dist/cjs/react/state.js.map +1 -1
- package/dist/cjs/types.d.ts +13 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/esm/config.d.ts +2 -2
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/hooks.d.ts.map +1 -1
- package/dist/esm/hooks.js +14 -2
- package/dist/esm/hooks.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internals/async.d.ts +1 -1
- package/dist/esm/internals/async.d.ts.map +1 -1
- package/dist/esm/internals/async.js +21 -16
- package/dist/esm/internals/async.js.map +1 -1
- package/dist/esm/internals/connect.d.ts.map +1 -1
- package/dist/esm/internals/connect.js +6 -0
- package/dist/esm/internals/connect.js.map +1 -1
- package/dist/esm/internals/contexts.d.ts +9 -2
- package/dist/esm/internals/contexts.d.ts.map +1 -1
- package/dist/esm/internals/contexts.js +35 -3
- package/dist/esm/internals/contexts.js.map +1 -1
- package/dist/esm/internals/derived.d.ts +15 -5
- package/dist/esm/internals/derived.d.ts.map +1 -1
- package/dist/esm/internals/derived.js +11 -11
- package/dist/esm/internals/derived.js.map +1 -1
- package/dist/esm/internals/get.js +2 -2
- package/dist/esm/internals/get.js.map +1 -1
- package/dist/esm/internals/scheduling.d.ts +2 -0
- package/dist/esm/internals/scheduling.d.ts.map +1 -1
- package/dist/esm/internals/scheduling.js +14 -0
- package/dist/esm/internals/scheduling.js.map +1 -1
- package/dist/esm/internals/state.d.ts +2 -2
- package/dist/esm/internals/state.d.ts.map +1 -1
- package/dist/esm/internals/utils/equals.d.ts +2 -0
- package/dist/esm/internals/utils/equals.d.ts.map +1 -1
- package/dist/esm/internals/utils/equals.js +2 -2
- package/dist/esm/internals/utils/equals.js.map +1 -1
- package/dist/esm/react/context.d.ts.map +1 -1
- package/dist/esm/react/context.js +5 -0
- package/dist/esm/react/context.js.map +1 -1
- package/dist/esm/react/provider.d.ts +2 -3
- package/dist/esm/react/provider.d.ts.map +1 -1
- package/dist/esm/react/provider.js +3 -7
- package/dist/esm/react/provider.js.map +1 -1
- package/dist/esm/react/rendering.d.ts +2 -0
- package/dist/esm/react/rendering.d.ts.map +1 -0
- package/dist/esm/react/rendering.js +19 -0
- package/dist/esm/react/rendering.js.map +1 -0
- package/dist/esm/react/signal-value.d.ts +1 -1
- package/dist/esm/react/signal-value.d.ts.map +1 -1
- package/dist/esm/react/signal-value.js +2 -18
- package/dist/esm/react/signal-value.js.map +1 -1
- package/dist/esm/react/state.d.ts +2 -2
- package/dist/esm/react/state.d.ts.map +1 -1
- package/dist/esm/react/state.js.map +1 -1
- package/dist/esm/types.d.ts +13 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/context.test.ts +103 -1
- package/src/__tests__/gc.test.ts +256 -0
- package/src/__tests__/utils/instrumented-hooks.ts +8 -11
- package/src/config.ts +5 -3
- package/src/hooks.ts +17 -3
- package/src/index.ts +8 -1
- package/src/internals/async.ts +13 -8
- package/src/internals/connect.ts +8 -0
- package/src/internals/contexts.ts +45 -5
- package/src/internals/derived.ts +25 -16
- package/src/internals/get.ts +2 -2
- package/src/internals/scheduling.ts +20 -0
- package/src/internals/state.ts +3 -3
- package/src/internals/utils/equals.ts +2 -2
- package/src/react/__tests__/contexts.test.tsx +84 -2
- package/src/react/context.ts +6 -0
- package/src/react/provider.tsx +4 -12
- package/src/react/rendering.ts +25 -0
- package/src/react/signal-value.ts +3 -26
- package/src/react/state.ts +3 -3
- package/src/types.ts +15 -1
@@ -6,6 +6,11 @@ import { runListeners as runStateListeners } from './state.js';
|
|
6
6
|
import { Tracer } from '../trace.js';
|
7
7
|
import { unwatchSignal } from './connect.js';
|
8
8
|
import { StateSignal } from './state.js';
|
9
|
+
import { ROOT_SCOPE, SignalScope } from './contexts.js';
|
10
|
+
|
11
|
+
// Determine once at startup which scheduling function to use for GC
|
12
|
+
const scheduleIdleCallback =
|
13
|
+
typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb: () => void) => _scheduleFlush(cb);
|
9
14
|
|
10
15
|
let PROMISE_WAS_RESOLVED = false;
|
11
16
|
|
@@ -14,6 +19,7 @@ let PENDING_ASYNC_PULLS: DerivedSignal<any, any>[] = [];
|
|
14
19
|
let PENDING_UNWATCH = new Map<DerivedSignal<any, any>, number>();
|
15
20
|
let PENDING_LISTENERS: (DerivedSignal<any, any> | StateSignal<any>)[] = [];
|
16
21
|
let PENDING_TRACERS: Tracer[] = [];
|
22
|
+
let PENDING_GC = new Set<SignalScope>();
|
17
23
|
|
18
24
|
const microtask = () => Promise.resolve();
|
19
25
|
|
@@ -62,6 +68,20 @@ export const scheduleTracer = (tracer: Tracer) => {
|
|
62
68
|
scheduleFlush(flushWatchers);
|
63
69
|
};
|
64
70
|
|
71
|
+
export const scheduleGcSweep = (scope: SignalScope) => {
|
72
|
+
PENDING_GC.add(scope);
|
73
|
+
|
74
|
+
if (PENDING_GC.size > 1) return;
|
75
|
+
|
76
|
+
scheduleIdleCallback(() => {
|
77
|
+
for (const scope of PENDING_GC) {
|
78
|
+
scope.sweepGc();
|
79
|
+
}
|
80
|
+
|
81
|
+
PENDING_GC.clear();
|
82
|
+
});
|
83
|
+
};
|
84
|
+
|
65
85
|
const flushWatchers = async () => {
|
66
86
|
const flush = currentFlush!;
|
67
87
|
|
package/src/internals/state.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { TRACER as TRACER, TracerEventType } from '../trace.js';
|
2
|
-
import {
|
3
|
-
import { DerivedSignal
|
2
|
+
import { SignalEquals, SignalListener, SignalOptions, StateSignal as IStateSignal } from '../types.js';
|
3
|
+
import { DerivedSignal } from './derived.js';
|
4
4
|
import { dirtySignal } from './dirty.js';
|
5
5
|
import { CURRENT_CONSUMER } from './get.js';
|
6
6
|
import { useStateSignal } from '../config.js';
|
@@ -8,7 +8,7 @@ import { scheduleListeners } from './scheduling.js';
|
|
8
8
|
|
9
9
|
let STATE_ID = 0;
|
10
10
|
|
11
|
-
export class StateSignal<T> implements
|
11
|
+
export class StateSignal<T> implements IStateSignal<T> {
|
12
12
|
private _value: T;
|
13
13
|
private _equals: SignalEquals<T>;
|
14
14
|
private _subs = new Map<WeakRef<DerivedSignal<unknown, unknown[]>>, number>();
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { SignalEquals } from '../../types.js';
|
2
2
|
|
3
|
-
const DEFAULT_EQUALS: SignalEquals<unknown> = (a, b) => a === b;
|
4
|
-
const FALSE_EQUALS: SignalEquals<unknown> = () => false;
|
3
|
+
export const DEFAULT_EQUALS: SignalEquals<unknown> = (a, b) => a === b;
|
4
|
+
export const FALSE_EQUALS: SignalEquals<unknown> = () => false;
|
5
5
|
|
6
6
|
export const equalsFrom = <T>(equals: SignalEquals<T> | false | undefined): SignalEquals<T> => {
|
7
7
|
if (equals === false) {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
2
2
|
import { render } from 'vitest-browser-react';
|
3
|
-
import { state, reactive, createContext, useContext } from '
|
4
|
-
import { ContextProvider, setupReact } from '../index.js';
|
3
|
+
import { state, reactive, createContext, useContext, setRootContexts } from '../../index.js';
|
4
|
+
import { ContextProvider, setupReact, useScope } from '../index.js';
|
5
5
|
import React, { useState } from 'react';
|
6
6
|
|
7
7
|
setupReact();
|
@@ -49,6 +49,69 @@ describe('React > contexts', () => {
|
|
49
49
|
await expect.element(getByText('Hey, World')).toBeInTheDocument();
|
50
50
|
});
|
51
51
|
|
52
|
+
test('provider inherits from root scope', async () => {
|
53
|
+
const defaultValue1 = state('default1');
|
54
|
+
const defaultValue2 = state('default2');
|
55
|
+
const ctx1 = createContext(defaultValue1);
|
56
|
+
const ctx2 = createContext(defaultValue2);
|
57
|
+
const rootOverride1 = state('root1');
|
58
|
+
const rootOverride2 = state('root2');
|
59
|
+
|
60
|
+
// Set root contexts
|
61
|
+
setRootContexts([
|
62
|
+
[ctx1, rootOverride1],
|
63
|
+
[ctx2, rootOverride2],
|
64
|
+
]);
|
65
|
+
|
66
|
+
// Component that uses both contexts
|
67
|
+
function Component({ testId }: { testId: string }): React.ReactNode {
|
68
|
+
const value1 = useContext(ctx1);
|
69
|
+
const value2 = useContext(ctx2);
|
70
|
+
const derived = reactive(() => `${value1.get()}-${value2.get()}`);
|
71
|
+
return <div data-testid={testId}>{derived()}</div>;
|
72
|
+
}
|
73
|
+
const localOverride1 = state('local1');
|
74
|
+
const localOverride2 = state('local2');
|
75
|
+
|
76
|
+
// Should inherit from root scope when no local overrides
|
77
|
+
const { getByTestId } = render(
|
78
|
+
<>
|
79
|
+
<ContextProvider contexts={[]}>
|
80
|
+
<Component testId="result" />
|
81
|
+
</ContextProvider>
|
82
|
+
<ContextProvider contexts={[[ctx1, localOverride1]]}>
|
83
|
+
<Component testId="result2" />
|
84
|
+
</ContextProvider>
|
85
|
+
<ContextProvider
|
86
|
+
contexts={[
|
87
|
+
[ctx1, localOverride1],
|
88
|
+
[ctx2, localOverride2],
|
89
|
+
]}
|
90
|
+
>
|
91
|
+
<Component testId="result3" />
|
92
|
+
</ContextProvider>
|
93
|
+
</>,
|
94
|
+
);
|
95
|
+
|
96
|
+
await expect.element(getByTestId('result')).toHaveTextContent('root1-root2');
|
97
|
+
|
98
|
+
// Should inherit from root scope for unoverridden contexts
|
99
|
+
await expect.element(getByTestId('result2')).toHaveTextContent('local1-root2');
|
100
|
+
|
101
|
+
// Should use local overrides when provided
|
102
|
+
await expect.element(getByTestId('result3')).toHaveTextContent('local1-local2');
|
103
|
+
|
104
|
+
// Changes to root contexts should be reflected in inherited contexts
|
105
|
+
rootOverride1.set('updated-root1');
|
106
|
+
rootOverride2.set('updated-root2');
|
107
|
+
|
108
|
+
await expect.element(getByTestId('result')).toHaveTextContent('updated-root1-updated-root2');
|
109
|
+
|
110
|
+
// Local overrides should remain unaffected by root context changes
|
111
|
+
await expect.element(getByTestId('result2')).toHaveTextContent('local1-updated-root2');
|
112
|
+
await expect.element(getByTestId('result3')).toHaveTextContent('local1-local2');
|
113
|
+
});
|
114
|
+
|
52
115
|
test('useContext works inside computed value passed via context provider', async () => {
|
53
116
|
const value = state('Hello');
|
54
117
|
const override = state('Hey');
|
@@ -96,4 +159,23 @@ describe('React > contexts', () => {
|
|
96
159
|
|
97
160
|
await expect.element(getByText('Hi, World')).toBeInTheDocument();
|
98
161
|
});
|
162
|
+
|
163
|
+
test('useScope returns undefined outside of rendering context', async () => {
|
164
|
+
// Direct call outside of rendering should return undefined
|
165
|
+
expect(useScope()).toBeUndefined();
|
166
|
+
|
167
|
+
// Inside a component during rendering, it should return the scope
|
168
|
+
function TestComponent() {
|
169
|
+
const scope = useScope();
|
170
|
+
return <div data-testid="scope">{scope ? 'has-scope' : 'no-scope'}</div>;
|
171
|
+
}
|
172
|
+
|
173
|
+
const { getByTestId } = render(
|
174
|
+
<ContextProvider contexts={[]}>
|
175
|
+
<TestComponent />
|
176
|
+
</ContextProvider>,
|
177
|
+
);
|
178
|
+
|
179
|
+
await expect.element(getByTestId('scope')).toHaveTextContent('has-scope');
|
180
|
+
});
|
99
181
|
});
|
package/src/react/context.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
import { createContext, useContext } from 'react';
|
2
2
|
import { SignalScope } from '../internals/contexts.js';
|
3
|
+
import { isRendering } from './rendering.js';
|
3
4
|
|
4
5
|
export const ScopeContext = createContext<SignalScope | undefined>(undefined);
|
5
6
|
|
6
7
|
export function useScope() {
|
8
|
+
if (!isRendering()) {
|
9
|
+
return undefined;
|
10
|
+
}
|
11
|
+
|
12
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
7
13
|
return useContext(ScopeContext);
|
8
14
|
}
|
package/src/react/provider.tsx
CHANGED
@@ -1,25 +1,17 @@
|
|
1
1
|
import { useContext } from 'react';
|
2
2
|
import { ScopeContext } from './context.js';
|
3
|
-
import { ContextImpl, ContextPair, SignalScope } from '../internals/contexts.js';
|
3
|
+
import { ContextImpl, ContextPair, ROOT_SCOPE, SignalScope } from '../internals/contexts.js';
|
4
4
|
|
5
5
|
export function ContextProvider<C extends unknown[]>({
|
6
6
|
children,
|
7
|
-
contexts,
|
7
|
+
contexts = [],
|
8
8
|
inherit = true,
|
9
|
-
root = false,
|
10
9
|
}: {
|
11
10
|
children: React.ReactNode;
|
12
|
-
contexts
|
11
|
+
contexts?: [...ContextPair<C>] | [];
|
13
12
|
inherit?: boolean;
|
14
|
-
root?: boolean;
|
15
13
|
}) {
|
16
|
-
|
17
|
-
// useEffect(() => )
|
18
|
-
|
19
|
-
// return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
|
20
|
-
// }
|
21
|
-
|
22
|
-
const parentScope = useContext(ScopeContext);
|
14
|
+
const parentScope = useContext(ScopeContext) ?? ROOT_SCOPE;
|
23
15
|
const scope = new SignalScope(contexts as [ContextImpl<unknown>, unknown][], inherit ? parentScope : undefined);
|
24
16
|
|
25
17
|
return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
// This is a private React internal that we need to access to check if we are rendering.
|
4
|
+
// There is no other consistent way to check if we are rendering in both development
|
5
|
+
// and production, and it doesn't appear that the React team wants to add one. This
|
6
|
+
// should be checked on every major React version upgrade.
|
7
|
+
const REACT_INTERNALS =
|
8
|
+
(React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
|
9
|
+
(React as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
|
10
|
+
(React as any).__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
11
|
+
|
12
|
+
const IS_REACT_18 = !!REACT_INTERNALS.ReactCurrentDispatcher;
|
13
|
+
const ReactCurrentDispatcher = REACT_INTERNALS.ReactCurrentDispatcher || REACT_INTERNALS;
|
14
|
+
|
15
|
+
export function isRendering() {
|
16
|
+
const dispatcher = IS_REACT_18 ? ReactCurrentDispatcher.current : ReactCurrentDispatcher.H;
|
17
|
+
|
18
|
+
return (
|
19
|
+
!!dispatcher &&
|
20
|
+
// dispatcher can be in a state where it's defined, but all hooks are invalid to call.
|
21
|
+
// Only way we can tell is that if they are invalid, they will all be equal to each other
|
22
|
+
// (e.g. because it's the function that throws an error)
|
23
|
+
dispatcher.useState !== dispatcher.useEffect
|
24
|
+
);
|
25
|
+
}
|
@@ -1,34 +1,11 @@
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
2
|
-
import
|
2
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
3
3
|
import type { DerivedSignal } from '../internals/derived.js';
|
4
4
|
import type { StateSignal } from '../internals/state.js';
|
5
5
|
import type { ReactiveValue } from '../types.js';
|
6
6
|
import { isReactivePromise } from '../internals/utils/type-utils.js';
|
7
7
|
import { isReactiveSubscription } from '../internals/async.js';
|
8
|
-
|
9
|
-
// This is a private React internal that we need to access to check if we are rendering.
|
10
|
-
// There is no other consistent way to check if we are rendering in both development
|
11
|
-
// and production, and it doesn't appear that the React team wants to add one. This
|
12
|
-
// should be checked on every major React version upgrade.
|
13
|
-
const REACT_INTERNALS =
|
14
|
-
(React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED ||
|
15
|
-
(React as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
|
16
|
-
(React as any).__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
17
|
-
|
18
|
-
const IS_REACT_18 = !!REACT_INTERNALS.ReactCurrentDispatcher;
|
19
|
-
const ReactCurrentDispatcher = REACT_INTERNALS.ReactCurrentDispatcher || REACT_INTERNALS;
|
20
|
-
|
21
|
-
function isRendering() {
|
22
|
-
const dispatcher = IS_REACT_18 ? ReactCurrentDispatcher.current : ReactCurrentDispatcher.H;
|
23
|
-
|
24
|
-
return (
|
25
|
-
!!dispatcher &&
|
26
|
-
// dispatcher can be in a state where it's defined, but all hooks are invalid to call.
|
27
|
-
// Only way we can tell is that if they are invalid, they will all be equal to each other
|
28
|
-
// (e.g. because it's the function that throws an error)
|
29
|
-
dispatcher.useState !== dispatcher.useEffect
|
30
|
-
);
|
31
|
-
}
|
8
|
+
import { isRendering } from './rendering.js';
|
32
9
|
|
33
10
|
export function useStateSignal<T>(signal: StateSignal<T>): T {
|
34
11
|
if (!isRendering()) {
|
@@ -42,7 +19,7 @@ export function useStateSignal<T>(signal: StateSignal<T>): T {
|
|
42
19
|
);
|
43
20
|
}
|
44
21
|
|
45
|
-
export function useDerivedSignal<T>(signal: DerivedSignal<T,
|
22
|
+
export function useDerivedSignal<T, Args extends unknown[]>(signal: DerivedSignal<T, Args>): ReactiveValue<T> {
|
46
23
|
if (!isRendering()) {
|
47
24
|
return signal.get();
|
48
25
|
}
|
package/src/react/state.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
import { useRef } from 'react';
|
2
2
|
import { state } from '../hooks.js';
|
3
|
-
import { SignalOptions,
|
3
|
+
import { SignalOptions, StateSignal } from '../types.js';
|
4
4
|
|
5
|
-
export function useStateSignal<T>(value: T, opts?: SignalOptions<T, unknown[]>):
|
6
|
-
const ref = useRef<
|
5
|
+
export function useStateSignal<T>(value: T, opts?: SignalOptions<T, unknown[]>): StateSignal<T> {
|
6
|
+
const ref = useRef<StateSignal<T> | undefined>(undefined);
|
7
7
|
|
8
8
|
if (!ref.current) {
|
9
9
|
ref.current = state(value, opts);
|
package/src/types.ts
CHANGED
@@ -5,10 +5,17 @@ export interface Signal<T = unknown> {
|
|
5
5
|
addListener(listener: SignalListener): () => void;
|
6
6
|
}
|
7
7
|
|
8
|
-
export interface
|
8
|
+
export interface StateSignal<T> extends Signal<T> {
|
9
9
|
set(value: T): void;
|
10
|
+
peek(): T;
|
11
|
+
update(updater: (value: T) => T): void;
|
10
12
|
}
|
11
13
|
|
14
|
+
/**
|
15
|
+
* @deprecated Use `StateSignal` instead.
|
16
|
+
*/
|
17
|
+
export type WriteableSignal<T> = StateSignal<T>;
|
18
|
+
|
12
19
|
export type SignalEquals<T> = (prev: T, next: T) => boolean;
|
13
20
|
|
14
21
|
export type SignalListener = () => void;
|
@@ -34,6 +41,13 @@ export interface SignalOptions<T, Args extends unknown[]> {
|
|
34
41
|
desc?: string;
|
35
42
|
scope?: SignalScope;
|
36
43
|
paramKey?: (...args: Args) => string;
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Called when signal's watchCount reaches 0.
|
47
|
+
* Return `true` to allow GC, `false` to prevent it.
|
48
|
+
* If not provided, defaults to always allowing GC.
|
49
|
+
*/
|
50
|
+
shouldGC?: (signal: object, value: T, args: Args) => boolean;
|
37
51
|
}
|
38
52
|
|
39
53
|
export interface SignalOptionsWithInit<T, Args extends unknown[]> extends SignalOptions<T, Args> {
|