Skip to content

Commit

Permalink
feat: Add hasExpired and expiryMultiplier options to GCPolicy
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Feb 24, 2025
1 parent f796b6c commit 5f9c9b0
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 10 deletions.
2 changes: 2 additions & 0 deletions packages/core/src/controller/__tests__/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('Controller', () => {
meta: {
[fetchKey]: {
date: Date.now(),
fetchedAt: Date.now(),
expiresAt: Date.now() + 10000,
},
},
Expand Down Expand Up @@ -88,6 +89,7 @@ describe('Controller', () => {
meta: {
[fetchKey]: {
date: 0,
fetchedAt: 0,
expiresAt: 0,
},
},
Expand Down
61 changes: 52 additions & 9 deletions packages/core/src/state/GCPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { EntityPath } from '@data-client/normalizr';

import { GC } from '../actionTypes.js';
import Controller from '../controller/Controller.js';
import { State } from '../index.js';

export class GCPolicy implements GCInterface {
protected endpointCount = new Map<string, number>();
Expand All @@ -11,13 +12,23 @@ export class GCPolicy implements GCInterface {

declare protected intervalId: ReturnType<typeof setInterval>;
declare protected controller: Controller;
declare protected options: GCOptions;
declare protected options: Required<GCOptions>;

constructor(
constructor({
// every 5 min
{ intervalMS = 60 * 1000 * 5 }: GCOptions = {},
) {
this.options = { intervalMS };
intervalMS = 60 * 1000 * 5,
expiryMultiplier = 1,
hasExpired,
}: GCOptions = {}) {
this.options = {
intervalMS,
expiryMultiplier,
hasExpired:
hasExpired ??
(({ fetchedAt, expiresAt, now }) =>
(expiresAt - fetchedAt) * this.options.expiryMultiplier + fetchedAt <
now),
};
}

init(controller: Controller) {
Expand Down Expand Up @@ -79,6 +90,12 @@ export class GCPolicy implements GCInterface {
};
}

protected hasExpired(
meta: State<unknown>['entityMeta'][string][string] & { now: number },
): boolean {
return this.options.hasExpired(meta);
}

protected runSweep() {
const state = this.controller.getState();
const entities: EntityPath[] = [];
Expand All @@ -87,8 +104,10 @@ export class GCPolicy implements GCInterface {

const nextEndpointsQ = new Set<string>();
for (const key of this.endpointsQ) {
const expiresAt = state.meta[key]?.expiresAt ?? 0;
if (expiresAt < now && !this.endpointCount.has(key)) {
if (
!this.endpointCount.has(key) &&
this.options.hasExpired(buildHasExpiredMeta(state.meta[key], now))
) {
endpoints.push(key);
} else {
nextEndpointsQ.add(key);
Expand All @@ -98,8 +117,12 @@ export class GCPolicy implements GCInterface {

const nextEntitiesQ: EntityPath[] = [];
for (const path of this.entitiesQ) {
const expiresAt = state.entityMeta[path.key]?.[path.pk]?.expiresAt ?? 0;
if (expiresAt < now && !this.entityCount.get(path.key)?.has(path.pk)) {
if (
!this.entityCount.get(path.key)?.has(path.pk) &&
this.options.hasExpired(
buildHasExpiredMeta(state.entityMeta[path.key]?.[path.pk], now),
)
) {
entities.push(path);
} else {
nextEntitiesQ.push(path);
Expand Down Expand Up @@ -138,8 +161,28 @@ export class ImmortalGCPolicy implements GCInterface {
}
}

function buildHasExpiredMeta(
{
fetchedAt = 0,
date = 0,
expiresAt = 0,
}: Partial<State<unknown>['entityMeta'][string][string]> = {},
now: number,
) {
return {
fetchedAt,
date,
expiresAt,
now,
};
}

export interface GCOptions {
intervalMS?: number;
expiryMultiplier?: number;
hasExpired?: (
meta: State<unknown>['entityMeta'][string][string] & { now: number },
) => boolean;
}
export interface CreateCountRef {
({ key, paths }: { key?: string; paths?: EntityPath[] }): () => () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exports[`reducer should set error in meta for "set" 1`] = `
"error": [Error: hi],
"errorPolicy": undefined,
"expiresAt": 5000500000,
"fetchedAt": 5000000000,
},
},
"optimistic": [],
Expand Down Expand Up @@ -48,6 +49,7 @@ exports[`reducer singles should update state correctly 1`] = `
"http://test.com/article/20": {
"date": 5000000000,
"expiresAt": 5000500000,
"fetchedAt": 5000000000,
"prevExpiresAt": undefined,
},
},
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/state/reducer/setResponseReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function setResponseReducer(
...state.meta,
[action.key]: {
date: action.meta.date,
fetchedAt: action.meta.fetchedAt,
expiresAt: action.meta.expiresAt,
prevExpiresAt: state.meta[action.key]?.expiresAt,
},
Expand Down Expand Up @@ -126,8 +127,9 @@ function reduceError(
...state.meta,
[action.key]: {
date: action.meta.date,
error,
fetchedAt: action.meta.fetchedAt,
expiresAt: action.meta.expiresAt,
error,
errorPolicy: action.endpoint.errorPolicy?.(error),
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface State<T> {
readonly meta: {
readonly [key: string]: {
readonly date: number;
readonly fetchedAt: number;
readonly expiresAt: number;
readonly prevExpiresAt?: number;
readonly error?: ErrorTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ exports[`<DataProvider /> should change state 1`] = `
"GET http://test.com/article-cooler/5": {
"date": 50,
"expiresAt": 55,
"fetchedAt": 50,
"prevExpiresAt": undefined,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ exports[`<DataProvider /> should change state 1`] = `
"GET http://test.com/article-cooler/5": {
"date": 50,
"expiresAt": 60050,
"fetchedAt": 50,
"prevExpiresAt": undefined,
},
},
Expand Down

0 comments on commit 5f9c9b0

Please sign in to comment.