Skip to content
Closed
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
2 changes: 2 additions & 0 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,8 @@ export type ResourceOptions<T, R> = PromiseResourceOptions<T, R> | StreamingReso
// @public
export interface ResourceRef<T> extends WritableResource<T> {
destroy(): void;
// (undocumented)
hasValue(): this is ResourceRef<Exclude<T, undefined>>;
}

// @public
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/resource/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export interface WritableResource<T> extends Resource<T> {
* @experimental
*/
export interface ResourceRef<T> extends WritableResource<T> {
hasValue(): this is ResourceRef<Exclude<T, undefined>>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we maybe capture this in a test ?


/**
* Manually destroy the resource, which cancels pending requests and returns it to `idle` state.
*/
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/resource/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ abstract class BaseWritableResource<T> implements WritableResource<T> {
() => this.status() === ResourceStatus.Loading || this.status() === ResourceStatus.Reloading,
);

hasValue(): this is WritableResource<Exclude<T, undefined>> {
hasValue(): this is ResourceRef<Exclude<T, undefined>> {
return this.value() !== undefined;
}

Expand Down Expand Up @@ -269,7 +269,7 @@ class ResourceImpl<T, R> extends BaseWritableResource<T> implements ResourceRef<
private async loadEffect(): Promise<void> {
// 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
Expand All @@ -280,8 +280,8 @@ class ResourceImpl<T, R> extends BaseWritableResource<T> 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`).
Expand Down Expand Up @@ -310,15 +310,15 @@ class ResourceImpl<T, R> extends BaseWritableResource<T> 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<R, undefined>,
abortSignal,
previous: {
status: previousStatus,
},
}),
);
});
});

if (abortSignal.aborted) {
return;
Expand Down
19 changes: 19 additions & 0 deletions packages/core/test/resource/resource_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down