1- import { ResourceRef , Signal , computed , linkedSignal } from '@angular/core' ;
1+ import { ResourceRef , Signal , isSignal , linkedSignal } from '@angular/core' ;
22import {
33 SignalStoreFeature ,
44 SignalStoreFeatureResult ,
@@ -56,7 +56,7 @@ import {
5656 * );
5757 *
5858 * const store = TestBed.inject(Store);
59- * store.status(); // 'idle' | 'loading' | 'resolved' | 'error'
59+ * store.status(); // 'idle' | 'loading' | 'resolved' | 'error' | 'local'
6060 * store.value(); // Todo[]
6161 * store.ids(); // EntityId[]
6262 * store.entityMap(); // Record<EntityId, Todo>
@@ -93,7 +93,7 @@ export function withEntityResources<
9393> (
9494 resourceFactory : (
9595 store : Input [ 'props' ] & Input [ 'methods' ] & StateSignals < Input [ 'state' ] > ,
96- ) => ResourceRef < readonly Entity [ ] | Entity [ ] | undefined > ,
96+ ) => ResourceRef < TypedEntityResourceValue < Entity > > ,
9797) : SignalStoreFeature < Input , EntityResourceResult < Entity > > ;
9898
9999export function withEntityResources <
@@ -107,7 +107,7 @@ export function withEntityResources<
107107
108108export function withEntityResources <
109109 Input extends SignalStoreFeatureResult ,
110- ResourceValue extends readonly unknown [ ] | unknown [ ] | undefined ,
110+ ResourceValue extends EntityResourceValue ,
111111> (
112112 entityResourceFactory : (
113113 store : Input [ 'props' ] & Input [ 'methods' ] & StateSignals < Input [ 'state' ] > ,
@@ -127,52 +127,93 @@ export function withEntityResources<
127127 } ;
128128}
129129
130- function createUnnamedEntityResource <
131- R extends ResourceRef < readonly unknown [ ] | unknown [ ] | undefined > ,
132- > ( resource : R ) {
133- type E = InferEntityFromRef < R > & { id : EntityId } ;
134- const { idsLinked , entityMapLinked , entitiesSignal } =
135- createEntityDerivations < E > (
136- resource . value as Signal < readonly E [ ] | E [ ] | undefined > ,
137- ) ;
138-
130+ /**
131+ * We cannot use the value of `resource` directly, but
132+ * have to use the one created through { @link withResource}
133+ * because { @link withResource} creates a Proxy around the resource value
134+ * to avoid the error throwing behavior of the Resource API.
135+ */
136+ function createUnnamedEntityResource < E extends Entity > (
137+ resource : ResourceRef < TypedEntityResourceValue < E > > ,
138+ ) {
139139 return signalStoreFeature (
140140 withResource ( ( ) => resource ) ,
141- withLinkedState ( ( ) => ( {
142- entityMap : entityMapLinked ,
143- ids : idsLinked ,
144- } ) ) ,
145- withComputed ( ( ) => ( {
146- entities : entitiesSignal ,
141+ withLinkedState ( ( { value } ) => {
142+ const { ids, entityMap } = createEntityDerivations ( value ) ;
143+
144+ return {
145+ entityMap,
146+ ids,
147+ } ;
148+ } ) ,
149+ withComputed ( ( { ids, entityMap } ) => ( {
150+ entities : createComputedEntities ( ids , entityMap ) ,
147151 } ) ) ,
148152 ) ;
149153}
150154
155+ /**
156+ * See {@link createUnnamedEntityResource} for why we cannot use the value of `resource` directly.
157+ */
151158function createNamedEntityResources < Dictionary extends EntityDictionary > (
152159 dictionary : Dictionary ,
153160) {
154161 const keys = Object . keys ( dictionary ) ;
155162
156- const linkedState : Record < string , Signal < unknown > > = { } ;
157- const computedProps : Record < string , Signal < unknown > > = { } ;
163+ const stateFactories = keys . map ( ( name ) => {
164+ return ( store : Record < string , unknown > ) => {
165+ const resourceValue = store [
166+ `${ name } Value`
167+ ] as Signal < EntityResourceValue > ;
168+ if ( ! isSignal ( resourceValue ) ) {
169+ throw new Error ( `Resource's value ${ name } Value does not exist` ) ;
170+ }
171+
172+ const { ids, entityMap } = createEntityDerivations ( resourceValue ) ;
173+
174+ return {
175+ [ `${ name } EntityMap` ] : entityMap ,
176+ [ `${ name } Ids` ] : ids ,
177+ } ;
178+ } ;
179+ } ) ;
180+
181+ const computedFactories = keys . map ( ( name ) => {
182+ return ( store : Record < string , unknown > ) => {
183+ const ids = store [ `${ name } Ids` ] as Signal < EntityId [ ] > ;
184+ const entityMap = store [ `${ name } EntityMap` ] as Signal <
185+ Record < EntityId , Entity >
186+ > ;
158187
159- keys . forEach ( ( name ) => {
160- const ref = dictionary [ name ] ;
161- type E = InferEntityFromRef < typeof ref > & { id : EntityId } ;
162- const { idsLinked, entityMapLinked, entitiesSignal } =
163- createEntityDerivations < E > (
164- ref . value as Signal < readonly E [ ] | E [ ] | undefined > ,
165- ) ;
188+ if ( ! isSignal ( ids ) ) {
189+ throw new Error ( `Entity Resource's ids ${ name } Ids does not exist` ) ;
190+ }
191+ if ( ! isSignal ( entityMap ) ) {
192+ throw new Error (
193+ `Entity Resource's entityMap ${ name } EntityMap does not exist` ,
194+ ) ;
195+ }
166196
167- linkedState [ `${ String ( name ) } EntityMap` ] = entityMapLinked ;
168- linkedState [ `${ String ( name ) } Ids` ] = idsLinked ;
169- computedProps [ `${ String ( name ) } Entities` ] = entitiesSignal ;
197+ return {
198+ [ `${ name } Entities` ] : createComputedEntities ( ids , entityMap ) ,
199+ } ;
200+ } ;
170201 } ) ;
171202
172203 return signalStoreFeature (
173204 withResource ( ( ) => dictionary ) ,
174- withLinkedState ( ( ) => linkedState ) ,
175- withComputed ( ( ) => computedProps ) ,
205+ withLinkedState ( ( store ) =>
206+ stateFactories . reduce (
207+ ( acc , factory ) => ( { ...acc , ...factory ( store ) } ) ,
208+ { } ,
209+ ) ,
210+ ) ,
211+ withComputed ( ( store ) =>
212+ computedFactories . reduce (
213+ ( acc , factory ) => ( { ...acc , ...factory ( store ) } ) ,
214+ { } ,
215+ ) ,
216+ ) ,
176217 ) ;
177218}
178219
@@ -209,20 +250,19 @@ type ArrayElement<T> = T extends readonly (infer E)[] | (infer E)[] ? E : never;
209250type InferEntityFromSignal < T > =
210251 T extends Signal < infer V > ? ArrayElement < V > : never ;
211252
212- type InferEntityFromRef <
213- R extends ResourceRef < readonly unknown [ ] | unknown [ ] | undefined > ,
214- > = R [ 'value' ] extends Signal < infer V > ? ArrayElement < V > : never ;
215-
216253type MergeUnion < U > = ( U extends unknown ? ( k : U ) => void : never ) extends (
217254 k : infer I ,
218255) => void
219256 ? I
220257 : never ;
221258
222- export type EntityDictionary = Record <
223- string ,
224- ResourceRef < readonly unknown [ ] | unknown [ ] | undefined >
225- > ;
259+ type Entity = { id : EntityId } ;
260+
261+ type EntityResourceValue = Entity [ ] | ( Entity [ ] | undefined ) ;
262+
263+ type TypedEntityResourceValue < E extends Entity > = E [ ] | ( E [ ] | undefined ) ;
264+
265+ export type EntityDictionary = Record < string , ResourceRef < EntityResourceValue > > ;
226266
227267type MergeNamedEntityStates < T extends EntityDictionary > = MergeUnion <
228268 {
@@ -249,21 +289,20 @@ type MergeNamedEntityProps<T extends EntityDictionary> = MergeUnion<
249289> ;
250290
251291export type NamedEntityResourceResult < T extends EntityDictionary > = {
252- state : NamedResourceResult < T > [ 'state' ] & MergeNamedEntityStates < T > ;
253- props : NamedResourceResult < T > [ 'props' ] & MergeNamedEntityProps < T > ;
254- methods : NamedResourceResult < T > [ 'methods' ] ;
292+ state : NamedResourceResult < T , false > [ 'state' ] & MergeNamedEntityStates < T > ;
293+ props : NamedResourceResult < T , false > [ 'props' ] & MergeNamedEntityProps < T > ;
294+ methods : NamedResourceResult < T , false > [ 'methods' ] ;
255295} ;
256296
257297/**
258298 * @internal
259299 * @description
260300 *
261- * Creates the three entity-related signals (`ids`, `entityMap`, `entities `) from
301+ * Creates the two entity-related state properties (`ids`, `entityMap`) from
262302 * a single source signal of entities. This mirrors the public contract of
263303 * `withEntities()`:
264304 * - `ids`: derived list of entity ids
265305 * - `entityMap`: map of id -> entity
266- * - `entities`: projection of `ids` through `entityMap`
267306 *
268307 * Implementation details:
269308 * - Uses `withLinkedState` + `linkedSignal` for `ids` and `entityMap` so they are
@@ -280,15 +319,15 @@ export type NamedEntityResourceResult<T extends EntityDictionary> = {
280319 * derived from signals. Using linked signals keeps the data flow declarative
281320 * and avoids imperative syncing code.
282321 */
283- function createEntityDerivations < E extends { id : EntityId } > (
284- source : Signal < readonly E [ ] | E [ ] | undefined > ,
322+ function createEntityDerivations < E extends Entity > (
323+ source : Signal < TypedEntityResourceValue < E > > ,
285324) {
286- const idsLinked = linkedSignal ( {
325+ const ids = linkedSignal ( {
287326 source,
288327 computation : ( list ) => ( list ?? [ ] ) . map ( ( e ) => e . id ) ,
289328 } ) ;
290329
291- const entityMapLinked = linkedSignal ( {
330+ const entityMap = linkedSignal ( {
292331 source,
293332 computation : ( list ) => {
294333 const map = { } as Record < EntityId , E > ;
@@ -299,11 +338,14 @@ function createEntityDerivations<E extends { id: EntityId }>(
299338 } ,
300339 } ) ;
301340
302- const entitiesSignal = computed ( ( ) => {
303- const ids = idsLinked ( ) ;
304- const map = entityMapLinked ( ) ;
305- return ids . map ( ( id ) => map [ id ] ) as readonly E [ ] ;
306- } ) ;
341+ return { ids, entityMap } ;
342+ }
307343
308- return { idsLinked, entityMapLinked, entitiesSignal } ;
344+ function createComputedEntities < E extends Entity > (
345+ ids : Signal < EntityId [ ] > ,
346+ entityMap : Signal < Record < EntityId , E > > ,
347+ ) {
348+ return ( ) => {
349+ return ids ( ) . map ( ( id ) => entityMap ( ) [ id ] ) ;
350+ } ;
309351}
0 commit comments