Skip to content

Trace ids #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 29, 2025
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ interface CachifiedOptions<Value> {
* Default: `undefined`
*/
waitUntil?: (promise: Promise<unknown>) => void;
/**
* Trace ID for debugging, is stored along cache metadata and can be accessed
* in `getFreshValue` and reporter
*/
traceId?: any;
}
```

Expand Down
93 changes: 91 additions & 2 deletions src/cachified.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CacheEntry,
GetFreshValue,
createCacheEntry,
getPendingValuesCache,
} from './index';
import { Deferred } from './createBatch';
import { delay, report } from './testHelpers';
Expand All @@ -35,6 +36,10 @@ function ignoreNode14<T>(callback: () => T) {
return callback();
}

const anyMetadata = expect.objectContaining({
createdTime: expect.any(Number),
} satisfies CacheMetadata);

let currentTime = 0;
beforeEach(() => {
currentTime = 0;
Expand Down Expand Up @@ -1108,6 +1113,23 @@ describe('cachified', () => {
expect(await getValue(() => 'FOUR')).toBe('THREE');
});

it('provides access to internal pending values cache', async () => {
const cache = new Map<string, CacheEntry>();
const pendingValuesCache = getPendingValuesCache(cache);
const d = new Deferred<string>();

cachified({ cache, key: 'test', ttl: 5, getFreshValue: () => d.promise });
await delay(0); // pending values are not set immediately

expect(pendingValuesCache.get('test')).toEqual(
expect.objectContaining({
metadata: anyMetadata,
value: expect.any(Promise),
resolve: expect.any(Function),
}),
);
});

it('uses stale cache while revalidating', async () => {
const cache = new Map<string, CacheEntry>();
const reporter = createReporter();
Expand Down Expand Up @@ -1423,7 +1445,7 @@ describe('cachified', () => {

expect(values).toEqual(['value-1', 'YOLO!', 'value-3']);
expect(getValues).toHaveBeenCalledTimes(1);
expect(getValues).toHaveBeenCalledWith([1, 3]);
expect(getValues).toHaveBeenCalledWith([1, 3], [anyMetadata, anyMetadata]);
});

it('rejects all values when batch get fails', async () => {
Expand Down Expand Up @@ -1469,7 +1491,10 @@ describe('cachified', () => {

expect(await valuesP).toEqual(['value-1', 'value-seven']);
expect(getValues).toHaveBeenCalledTimes(1);
expect(getValues).toHaveBeenCalledWith([1, 'seven']);
expect(getValues).toHaveBeenCalledWith(
[1, 'seven'],
[anyMetadata, anyMetadata],
);
});

it('can edit metadata for single batch values', async () => {
Expand Down Expand Up @@ -1670,6 +1695,70 @@ describe('cachified', () => {

expect(value).toBe('ONE');
});

it('supports trace ids', async () => {
expect.assertions(7);

const cache = new Map<string, CacheEntry>();
const traceId1 = Symbol();
const d = new Deferred<string>();

const value = cachified({
cache,
key: 'test-1',
ttl: 200,
traceId: traceId1,
async getFreshValue({ metadata: { traceId } }) {
// in getFreshValue
expect(traceId).toBe(traceId1);
return d.promise;
},
});
await delay(0);

// on pending values cache
expect(getPendingValuesCache(cache).get('test-1')?.metadata.traceId).toBe(
traceId1,
);

d.resolve('ONE');
expect(await value).toBe('ONE');

// on cache entry
expect(cache.get('test-1')?.metadata.traceId).toBe(traceId1);

const traceId2 = 'some-string-id';

// in batch getFreshValues
const batch = createBatch((freshIndexes, metadata) => {
expect(metadata[0].traceId).toBe(traceId2);
return freshIndexes.map((i) => `value-${i}`);
});

const createReporter = jest.fn(() => () => {});

await cachified(
{
cache,
key: 'test-2',
ttl: 200,
traceId: traceId2,
getFreshValue: batch.add(1),
},
createReporter,
);

expect(cache.get('test-2')?.metadata.traceId).toBe(traceId2);

expect(createReporter).toHaveBeenCalledWith(
expect.objectContaining({
traceId: traceId2,
metadata: expect.objectContaining({
traceId: traceId2,
}),
}),
);
});
});

function createReporter() {
Expand Down
21 changes: 11 additions & 10 deletions src/cachified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
CachifiedOptions,
CachifiedOptionsWithSchema,
Cache,
CacheEntry,
createContext,
HANDLE,
} from './common';
Expand All @@ -16,6 +15,16 @@ import { isExpired } from './isExpired';
// Keys are unique per cache but may be used by multiple caches
const pendingValuesByCache = new WeakMap<Cache, Map<string, any>>();

/**
* Get the internal pending values cache for a given cache
*/
export function getPendingValuesCache(cache: Cache) {
if (!pendingValuesByCache.has(cache)) {
pendingValuesByCache.set(cache, new Map());
}
return pendingValuesByCache.get(cache)!;
}

export async function cachified<Value, InternalValue>(
options: CachifiedOptionsWithSchema<Value, InternalValue>,
reporter?: CreateReporter<Value>,
Expand All @@ -30,15 +39,7 @@ export async function cachified<Value>(
): Promise<Value> {
const context = createContext(options, reporter);
const { key, cache, forceFresh, report, metadata } = context;

// Register this cache
if (!pendingValuesByCache.has(cache)) {
pendingValuesByCache.set(cache, new Map());
}
const pendingValues: Map<
string,
CacheEntry<Promise<Value>> & { resolve: (value: Value) => void }
> = pendingValuesByCache.get(cache)!;
const pendingValues = getPendingValuesCache(cache);

const hasPendingValue = () => {
return pendingValues.has(key);
Expand Down
17 changes: 15 additions & 2 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface CacheMetadata {
createdTime: number;
ttl?: number | null;
swr?: number | null;
traceId?: any;
/** @deprecated use swr instead */
readonly swv?: number | null;
}
Expand Down Expand Up @@ -201,6 +202,11 @@ export interface CachifiedOptions<Value> {
* Default: `undefined`
*/
waitUntil?: (promise: Promise<unknown>) => void;
/**
* Trace ID for debugging, is stored along cache metadata and can be accessed
* in `getFreshValue` and reporter
*/
traceId?: any;
}

/* When using a schema validator, a strongly typed getFreshValue is not required
Expand All @@ -216,12 +222,13 @@ export type CachifiedOptionsWithSchema<Value, InternalValue> = Omit<
export interface Context<Value>
extends Omit<
Required<CachifiedOptions<Value>>,
'fallbackToCache' | 'reporter' | 'checkValue' | 'swr'
'fallbackToCache' | 'reporter' | 'checkValue' | 'swr' | 'traceId'
> {
checkValue: CheckValue<Value>;
report: Reporter<Value>;
fallbackToCache: number;
metadata: CacheMetadata;
traceId?: any;
}

function validateWithSchema<Value>(
Expand Down Expand Up @@ -277,7 +284,11 @@ export function createContext<Value>(
staleRefreshTimeout: 0,
forceFresh: false,
...options,
metadata: createCacheMetaData({ ttl, swr: staleWhileRevalidate }),
metadata: createCacheMetaData({
ttl,
swr: staleWhileRevalidate,
traceId: options.traceId,
}),
waitUntil: options.waitUntil ?? (() => {}),
};

Expand Down Expand Up @@ -313,11 +324,13 @@ export function createCacheMetaData({
ttl = null,
swr = 0,
createdTime = Date.now(),
traceId,
}: Partial<Omit<CacheMetadata, 'swv'>> = {}) {
return {
ttl: ttl === Infinity ? null : ttl,
swr: swr === Infinity ? null : swr,
createdTime,
...(traceId ? { traceId } : {}),
};
}

Expand Down
24 changes: 19 additions & 5 deletions src/createBatch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { GetFreshValue, GetFreshValueContext } from './common';
import type {
CacheMetadata,
GetFreshValue,
GetFreshValueContext,
} from './common';
import { HANDLE } from './common';

type OnValueCallback<Value> = (
Expand All @@ -12,20 +16,25 @@ export type AddFn<Value, Param> = (
onValue?: OnValueCallback<Value>,
) => GetFreshValue<Value>;

export type GetFreshValues<Value, Param> = (
params: Param[],
metadata: CacheMetadata[],
) => Value[] | Promise<Value[]>;

export function createBatch<Value, Param>(
getFreshValues: (params: Param[]) => Value[] | Promise<Value[]>,
getFreshValues: GetFreshValues<Value, Param>,
autoSubmit: false,
): {
submit: () => Promise<void>;
add: AddFn<Value, Param>;
};
export function createBatch<Value, Param>(
getFreshValues: (params: Param[]) => Value[] | Promise<Value[]>,
getFreshValues: GetFreshValues<Value, Param>,
): {
add: AddFn<Value, Param>;
};
export function createBatch<Value, Param>(
getFreshValues: (params: Param[]) => Value[] | Promise<Value[]>,
getFreshValues: GetFreshValues<Value, Param>,
autoSubmit: boolean = true,
): {
submit?: () => Promise<void>;
Expand All @@ -35,6 +44,7 @@ export function createBatch<Value, Param>(
param: Param,
res: (value: Value) => void,
rej: (reason: unknown) => void,
metadata: CacheMetadata,
][] = [];

let count = 0;
Expand Down Expand Up @@ -62,7 +72,10 @@ export function createBatch<Value, Param>(

try {
const results = await Promise.resolve(
getFreshValues(requests.map(([param]) => param)),
getFreshValues(
requests.map(([param]) => param),
requests.map((args) => args[3]),
),
);
results.forEach((value, index) => requests[index][1](value));
submission.resolve();
Expand Down Expand Up @@ -97,6 +110,7 @@ export function createBatch<Value, Param>(
res(value);
},
rej,
context.metadata,
]);
if (!handled) {
handled = true;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type {
export { staleWhileRevalidate, totalTtl, createCacheEntry } from './common';
export * from './reporter';
export { createBatch } from './createBatch';
export { cachified } from './cachified';
export { cachified, getPendingValuesCache } from './cachified';
export { cachified as default } from './cachified';
export { shouldRefresh, isExpired } from './isExpired';
export { assertCacheEntry } from './assertCacheEntry';
Expand Down