diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index ec143fda7330..28197e6d9f02 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -1634,6 +1634,8 @@ export type ResourceOptions = PromiseResourceOptions | StreamingReso // @public export interface ResourceRef extends WritableResource { destroy(): void; + // (undocumented) + hasValue(): this is ResourceRef>; } // @public diff --git a/packages/core/src/resource/api.ts b/packages/core/src/resource/api.ts index 6464dc1218ff..ddb1f95acf20 100644 --- a/packages/core/src/resource/api.ts +++ b/packages/core/src/resource/api.ts @@ -133,6 +133,8 @@ export interface WritableResource extends Resource { * @experimental */ export interface ResourceRef extends WritableResource { + hasValue(): this is ResourceRef>; + /** * Manually destroy the resource, which cancels pending requests and returns it to `idle` state. */ diff --git a/packages/core/src/resource/resource.ts b/packages/core/src/resource/resource.ts index be975f56d20d..7392e511bf19 100644 --- a/packages/core/src/resource/resource.ts +++ b/packages/core/src/resource/resource.ts @@ -103,7 +103,7 @@ abstract class BaseWritableResource implements WritableResource { () => this.status() === ResourceStatus.Loading || this.status() === ResourceStatus.Reloading, ); - hasValue(): this is WritableResource> { + hasValue(): this is ResourceRef> { return this.value() !== undefined; } @@ -269,7 +269,7 @@ class ResourceImpl extends BaseWritableResource implements ResourceRef< private async loadEffect(): Promise { // Capture the previous status before any state transitions. Note that this is `untracked` since // we do not want the effect to depend on the state of the resource, only on the request. - const {status: previousStatus} = untracked(this.state); + const {status: currentStatus, previousStatus} = untracked(this.state); const {request, reload: reloadCounter} = this.extendedRequest(); // Subscribe side-effectfully to `reloadCounter`, although we don't actually care about its @@ -280,8 +280,8 @@ class ResourceImpl extends BaseWritableResource implements ResourceRef< // Nothing to load (and we should already be in a non-loading state). return; } else if ( - previousStatus !== ResourceStatus.Loading && - previousStatus !== ResourceStatus.Reloading + currentStatus !== ResourceStatus.Loading && + currentStatus !== ResourceStatus.Reloading ) { // We might've transitioned into a loading state, but has since been overwritten (likely via // `.set`). @@ -310,15 +310,15 @@ class ResourceImpl extends BaseWritableResource implements ResourceRef< // The actual loading is run through `untracked` - only the request side of `resource` is // reactive. This avoids any confusion with signals tracking or not tracking depending on // which side of the `await` they are. - const stream = await untracked(() => - this.loaderFn({ + const stream = await untracked(() => { + return this.loaderFn({ request: request as Exclude, abortSignal, previous: { status: previousStatus, }, - }), - ); + }); + }); if (abortSignal.aborted) { return; diff --git a/packages/core/test/resource/resource_spec.ts b/packages/core/test/resource/resource_spec.ts index 29dc60062ec0..aac793b9eff8 100644 --- a/packages/core/test/resource/resource_spec.ts +++ b/packages/core/test/resource/resource_spec.ts @@ -107,6 +107,25 @@ describe('resource', () => { expect(echoResource.error()).toBe(undefined); }); + it('should report idle status as the previous status on first run', async () => { + let prevStatus: ResourceStatus | undefined; + resource({ + loader: async ({previous}) => { + // Ensure the loader only runs once. + expect(prevStatus).toBeUndefined(); + + prevStatus = previous.status; + return true; + }, + injector: TestBed.inject(Injector), + }); + + TestBed.flushEffects(); + await flushMicrotasks(); + + expect(prevStatus).toBe(ResourceStatus.Idle); + }); + it('should expose errors thrown during resource loading', async () => { const backend = new MockEchoBackend(); const requestParam = {};