Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions modules/signals/spec/signal-state.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ describe('signalState', () => {
expect(isSignal(state.ngrx)).toBe(true);
});

it('does not expose state slices as writable signals', () => {
const state = signalState(initialState);
expect(() => (state as any).foo.set('baz')).toThrow(
'set is not a function'
);
});

it('caches previously created signals', () => {
const state = signalState(initialState);
const user1 = state.user;
Expand Down
13 changes: 12 additions & 1 deletion modules/signals/spec/with-linked-state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { linkedSignal, signal } from '@angular/core';
import { linkedSignal, signal, WritableSignal } from '@angular/core';
import {
getState,
patchState,
Expand Down Expand Up @@ -254,4 +254,15 @@ describe('withLinkedState', () => {
'value'
);
});

it('does not expose linked state properties as writable signals', () => {
const initialStore = getInitialInnerStore();
const userStore = withLinkedState(() => ({
foo: () => 'bar',
}))(initialStore);

expect(() =>
(userStore.stateSignals.foo as WritableSignal<string>).set('baz')
).toThrow('set is not a function');
});
});
15 changes: 14 additions & 1 deletion modules/signals/spec/with-state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isSignal, signal } from '@angular/core';
import { isSignal, signal, WritableSignal } from '@angular/core';
import { getState, withComputed, withMethods, withState } from '../src';
import { getInitialInnerStore } from '../src/signal-store';

Expand Down Expand Up @@ -38,6 +38,19 @@ describe('withState', () => {
expect(isSignal(store.stateSignals.x.y)).toBe(true);
});

it('does not expose state slices as writable signals', () => {
const initialStore = getInitialInnerStore();

const store = withState({
foo: 'bar',
x: { y: 'z' },
})(initialStore);

expect(() =>
(store.stateSignals.foo as WritableSignal<string>).set('baz')
).toThrow('set is not a function');
});

it('patches state source and creates deep signals for state slices provided via factory', () => {
const initialStore = getInitialInnerStore();

Expand Down
6 changes: 3 additions & 3 deletions modules/signals/src/signal-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { computed, signal } from '@angular/core';
import { DeepSignal, toDeepSignal } from './deep-signal';
import { SignalsDictionary } from './signal-store-models';
import { WritableSignalsDictionary } from './signal-store-models';
import { STATE_SOURCE, WritableStateSource } from './state-source';

export type SignalState<State extends object> = DeepSignal<State> &
Expand All @@ -16,7 +16,7 @@ export function signalState<State extends object>(
...signalsDict,
[key]: signal((initialState as Record<string | symbol, unknown>)[key]),
}),
{} as SignalsDictionary
{} as WritableSignalsDictionary
);

const signalState = computed(() =>
Expand All @@ -32,7 +32,7 @@ export function signalState<State extends object>(

for (const key of stateKeys) {
Object.defineProperty(signalState, key, {
value: toDeepSignal(stateSource[key]),
value: toDeepSignal(stateSource[key].asReadonly()),
});
}

Expand Down
7 changes: 6 additions & 1 deletion modules/signals/src/signal-store-models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Signal } from '@angular/core';
import { Signal, WritableSignal } from '@angular/core';
import { DeepSignal } from './deep-signal';
import { WritableStateSource } from './state-source';
import { IsKnownRecord, Prettify } from './ts-helpers';
Expand All @@ -14,6 +14,11 @@ export type StateSignals<State> =

export type SignalsDictionary = Record<string | symbol, Signal<unknown>>;

export type WritableSignalsDictionary = Record<
string | symbol,
WritableSignal<unknown>
>;

export type MethodsDictionary = Record<string, Function>;

export type SignalStoreHooks = {
Expand Down
5 changes: 3 additions & 2 deletions modules/signals/src/with-linked-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SignalStoreFeature,
SignalStoreFeatureResult,
StateSignals,
WritableSignalsDictionary,
} from './signal-store-models';
import { isWritableSignal, STATE_SOURCE } from './state-source';
import { Prettify } from './ts-helpers';
Expand Down Expand Up @@ -89,15 +90,15 @@ export function withLinkedState<
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
assertUniqueStoreMembers(store, stateKeys);
}
const stateSource = store[STATE_SOURCE] as SignalsDictionary;
const stateSource = store[STATE_SOURCE] as WritableSignalsDictionary;
const stateSignals = {} as SignalsDictionary;

for (const key of stateKeys) {
const signalOrComputationFn = linkedState[key];
stateSource[key] = isWritableSignal(signalOrComputationFn)
? signalOrComputationFn
: linkedSignal(signalOrComputationFn);
stateSignals[key] = toDeepSignal(stateSource[key]);
stateSignals[key] = toDeepSignal(stateSource[key].asReadonly());
}

return {
Expand Down
10 changes: 4 additions & 6 deletions modules/signals/src/with-state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Signal, signal } from '@angular/core';
import { signal } from '@angular/core';
import { toDeepSignal } from './deep-signal';
import { assertUniqueStoreMembers } from './signal-store-assertions';
import {
Expand All @@ -7,6 +7,7 @@ import {
SignalsDictionary,
SignalStoreFeature,
SignalStoreFeatureResult,
WritableSignalsDictionary,
} from './signal-store-models';
import { STATE_SOURCE } from './state-source';

Expand Down Expand Up @@ -38,15 +39,12 @@ export function withState<State extends object>(
assertUniqueStoreMembers(store, stateKeys);
}

const stateSource = store[STATE_SOURCE] as Record<
string | symbol,
Signal<unknown>
>;
const stateSource = store[STATE_SOURCE] as WritableSignalsDictionary;
const stateSignals: SignalsDictionary = {};

for (const key of stateKeys) {
stateSource[key] = signal(state[key]);
stateSignals[key] = toDeepSignal(stateSource[key]);
stateSignals[key] = toDeepSignal(stateSource[key].asReadonly());
}

return {
Expand Down