diff --git a/modules/signals/entities/src/helpers.ts b/modules/signals/entities/src/helpers.ts index 0ed58ccb4a..d14dc8b69c 100644 --- a/modules/signals/entities/src/helpers.ts +++ b/modules/signals/entities/src/helpers.ts @@ -2,6 +2,7 @@ import { DidMutate, EntityChanges, EntityId, + EntityMap, EntityPredicate, EntityState, SelectEntityId, @@ -10,6 +11,11 @@ import { declare const ngDevMode: unknown; const defaultSelectId: SelectEntityId<{ id: EntityId }> = (entity) => entity.id; +type EntityStateMutable = { + entityMap: Record; + ids: EntityId[]; +}; + export function getEntityIdSelector(config?: { selectId?: SelectEntityId; }): SelectEntityId { @@ -79,8 +85,8 @@ export function addEntityMutably( return DidMutate.None; } - state.entityMap[id] = entity; - state.ids.push(id); + (state as EntityStateMutable).entityMap[id] = entity; + (state as EntityStateMutable).ids.push(id); return DidMutate.Both; } @@ -111,12 +117,12 @@ export function setEntityMutably( const id = selectId(entity); if (state.entityMap[id]) { - state.entityMap[id] = entity; + (state as EntityStateMutable).entityMap[id] = entity; return DidMutate.Entities; } - state.entityMap[id] = entity; - state.ids.push(id); + (state as EntityStateMutable).entityMap[id] = entity; + (state as EntityStateMutable).ids.push(id); return DidMutate.Both; } @@ -152,13 +158,15 @@ export function removeEntitiesMutably( for (const id of ids) { if (state.entityMap[id]) { - delete state.entityMap[id]; + delete (state as EntityStateMutable).entityMap[id]; didMutate = DidMutate.Both; } } if (didMutate === DidMutate.Both) { - state.ids = state.ids.filter((id) => id in state.entityMap); + (state as EntityStateMutable).ids = state.ids.filter( + (id) => id in state.entityMap + ); } return didMutate; @@ -182,13 +190,16 @@ export function updateEntitiesMutably( if (entity) { const changesRecord = typeof changes === 'function' ? changes(entity) : changes; - state.entityMap[id] = { ...entity, ...changesRecord }; + (state as EntityStateMutable).entityMap[id] = { + ...entity, + ...changesRecord, + }; didMutate = DidMutate.Entities; const newId = selectId(state.entityMap[id]); if (newId !== id) { - state.entityMap[newId] = state.entityMap[id]; - delete state.entityMap[id]; + (state as EntityStateMutable).entityMap[newId] = state.entityMap[id]; + delete (state as EntityStateMutable).entityMap[id]; newIds = newIds || {}; newIds[id] = newId; @@ -197,7 +208,7 @@ export function updateEntitiesMutably( } if (newIds) { - state.ids = state.ids.map((id) => newIds[id] ?? id); + (state as EntityStateMutable).ids = state.ids.map((id) => newIds[id] ?? id); didMutate = DidMutate.Both; } diff --git a/modules/signals/entities/src/models.ts b/modules/signals/entities/src/models.ts index 956311b50b..2fd58d7625 100644 --- a/modules/signals/entities/src/models.ts +++ b/modules/signals/entities/src/models.ts @@ -2,11 +2,11 @@ import { Signal } from '@angular/core'; export type EntityId = string | number; -export type EntityMap = Record; +export type EntityMap = Readonly>; export type EntityState = { - entityMap: EntityMap; - ids: EntityId[]; + readonly entityMap: EntityMap; + readonly ids: readonly EntityId[]; }; export type NamedEntityState = { @@ -14,7 +14,7 @@ export type NamedEntityState = { }; export type EntityProps = { - entities: Signal; + entities: Signal; }; export type NamedEntityProps = { diff --git a/modules/signals/spec/types/signal-store.types.spec.ts b/modules/signals/spec/types/signal-store.types.spec.ts index 6854f27064..1d6594e6b4 100644 --- a/modules/signals/spec/types/signal-store.types.spec.ts +++ b/modules/signals/spec/types/signal-store.types.spec.ts @@ -34,7 +34,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'Store', - 'Type<{ foo: Signal; bar: Signal; } & StateSource<{ foo: string; bar: number[]; }>>' + 'Type<{ readonly foo: Signal; readonly bar: Signal; } & StateSource<{ foo: string; bar: number[]; }>>' ); }); @@ -64,7 +64,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSource<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>' + '{ readonly user: DeepSignal<{ age: number; details: { first: string; flags: boolean[]; }; }>; } & StateSource<{ user: { age: number; details: { first: string; flags: boolean[]; }; }; }>' ); expectSnippet(snippet).toInfer( @@ -265,7 +265,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ foo: Signal; bar: DeepSignal<{ baz: { b: boolean; } | null; }>; x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSource<{ foo: number | { ...; }; bar: { ...; }; x: { ...; }; }>' + '{ readonly foo: Signal; readonly bar: DeepSignal<{ baz: { b: boolean; } | null; }>; readonly x: DeepSignal<{ y: { z: number | undefined; }; }>; } & StateSource<...>' ); expectSnippet(snippet).toInfer('foo', 'Signal'); @@ -305,7 +305,7 @@ describe('signalStore', () => { expectSnippet(snippet1).toInfer( 'Store', - 'Type<{ name: DeepSignal<{ x: { y: string; }; }>; arguments: Signal; call: Signal; } & StateSource<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>' + 'Type<{ readonly name: DeepSignal<{ x: { y: string; }; }>; readonly arguments: Signal; readonly call: Signal; } & StateSource<{ name: { x: { y: string; }; }; arguments: number[]; call: boolean; }>>' ); const snippet2 = ` @@ -322,7 +322,7 @@ describe('signalStore', () => { expectSnippet(snippet2).toInfer( 'Store', - 'Type<{ apply: Signal; bind: DeepSignal<{ foo: string; }>; prototype: Signal; } & StateSource<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>' + 'Type<{ readonly apply: Signal; readonly bind: DeepSignal<{ foo: string; }>; readonly prototype: Signal; } & StateSource<{ apply: string; bind: { foo: string; }; prototype: string[]; }>>' ); const snippet3 = ` @@ -338,7 +338,7 @@ describe('signalStore', () => { expectSnippet(snippet3).toInfer( 'Store', - 'Type<{ length: Signal; caller: Signal; } & StateSource<{ length: number; caller: undefined; }>>' + 'Type<{ readonly length: Signal; readonly caller: Signal; } & StateSource<{ length: number; caller: undefined; }>>' ); }); @@ -393,7 +393,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ bar: DeepSignal<{ baz?: number | undefined; }>; x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSource<{ bar: { baz?: number | undefined; }; x: { y?: { z: boolean; } | undefined; }; }>' + '{ readonly bar: DeepSignal<{ baz?: number | undefined; }>; readonly x: DeepSignal<{ y?: { z: boolean; } | undefined; }>; } & StateSource<{ bar: { baz?: number | undefined; }; x: { ...; }; }>' ); expectSnippet(snippet).toInfer( @@ -503,7 +503,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store1', - '{ count: Signal; } & StateSource<{ count: number; }>' + '{ readonly count: Signal; } & StateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state1', '{ count: number; }'); @@ -515,7 +515,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store2', - '{ count: Signal; } & StateSource<{ count: number; }>' + '{ readonly count: Signal; } & StateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state2', '{ count: number; }'); @@ -548,7 +548,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store1', - '{ count: Signal; } & StateSource<{ count: number; }>' + '{ readonly count: Signal; } & StateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state1', '{ count: number; }'); @@ -560,7 +560,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store2', - '{ count: Signal; } & StateSource<{ count: number; }>' + '{ readonly count: Signal; } & StateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state2', '{ count: number; }'); @@ -593,14 +593,14 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store1', - '{ count: Signal; } & WritableStateSource<{ count: number; }>' + '{ readonly count: Signal; } & WritableStateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state1', '{ count: number; }'); expectSnippet(snippet).toInfer( 'store2', - '{ count: Signal; } & WritableStateSource<{ count: number; }>' + '{ readonly count: Signal; } & WritableStateSource<{ count: number; }>' ); expectSnippet(snippet).toInfer('state2', '{ count: number; }'); @@ -762,7 +762,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ ngrx: Signal; x: DeepSignal<{ y: string; }>; signals: Signal; mgmt: (arg: boolean) => number; } & StateSource<{ ngrx: string; x: { y: string; }; }>' + '{ readonly ngrx: Signal; readonly x: DeepSignal<{ y: string; }>; readonly signals: Signal; readonly mgmt: (arg: boolean) => number; } & StateSource<...>' ); }); @@ -790,7 +790,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ foo: Signal; bar: Signal; baz: (x: number) => void; } & StateSource<{ foo: number; }>' + '{ readonly foo: Signal; readonly bar: Signal; readonly baz: (x: number) => void; } & StateSource<{ foo: number; }>' ); }); @@ -838,7 +838,7 @@ describe('signalStore', () => { expectSnippet(snippet).toInfer( 'store', - '{ count1: Signal; doubleCount2: Signal; increment1: () => void; } & StateSource<{ count1: number; }>' + '{ readonly count1: Signal; readonly doubleCount2: Signal; readonly increment1: () => void; } & StateSource<{ count1: number; }>' ); }); diff --git a/modules/signals/src/signal-store.ts b/modules/signals/src/signal-store.ts index ce70ab8e46..aef83b7759 100644 --- a/modules/signals/src/signal-store.ts +++ b/modules/signals/src/signal-store.ts @@ -13,10 +13,12 @@ type SignalStoreConfig = { providedIn?: 'root'; protectedState?: boolean }; type SignalStoreMembers = Prettify< - OmitPrivate< - StateSignals & - FeatureResult['props'] & - FeatureResult['methods'] + Readonly< + OmitPrivate< + StateSignals & + FeatureResult['props'] & + FeatureResult['methods'] + > > >; diff --git a/modules/signals/src/state-source.ts b/modules/signals/src/state-source.ts index 4db6ec3a68..7de0ea7f07 100644 --- a/modules/signals/src/state-source.ts +++ b/modules/signals/src/state-source.ts @@ -14,11 +14,11 @@ const STATE_WATCHERS = new WeakMap, Array>>(); export const STATE_SOURCE = Symbol('STATE_SOURCE'); export type WritableStateSource = { - [STATE_SOURCE]: WritableSignal; + readonly [STATE_SOURCE]: WritableSignal; }; export type StateSource = { - [STATE_SOURCE]: Signal; + readonly [STATE_SOURCE]: Signal; }; export type PartialStateUpdater = (