chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Redis
This is a simple package that is used to return a valid Redis client and provides an error callback. It will log and swallow errors if they're not handled.
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@internal/cache",
"private": true,
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"dependencies": {
"@internal/redis": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@unkey/cache": "^1.5.0",
"@unkey/error": "^0.2.0",
"lru-cache": "^11.2.4",
"superjson": "^2.2.1"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:watch": "vitest"
}
}
+15
View File
@@ -0,0 +1,15 @@
export {
createCache,
DefaultStatefulContext,
Namespace,
type Cache as UnkeyCache,
type CacheError,
} from "@unkey/cache";
export { type Result, Ok, Err } from "@unkey/error";
export { RedisCacheStore } from "./stores/redis.js";
export { createMemoryStore, type MemoryStore } from "./stores/memory.js";
export {
LRUMemoryStore,
createLRUMemoryStore,
type LRUMemoryStoreConfig,
} from "./stores/lruMemory.js";
+333
View File
@@ -0,0 +1,333 @@
import type { Entry } from "@unkey/cache/stores";
import { beforeEach, describe, expect, it } from "vitest";
import { LRUMemoryStore, createLRUMemoryStore } from "./lruMemory.js";
function createEntry<T>(value: T, freshUntil: number, staleUntil: number): Entry<T> {
return { value, freshUntil, staleUntil };
}
describe("LRUMemoryStore", () => {
let store: LRUMemoryStore<string, string>;
beforeEach(() => {
store = new LRUMemoryStore({ max: 5, name: "test-store" });
});
describe("basic operations", () => {
it("should set and get a value", async () => {
const entry = createEntry("test-value", Date.now() + 60000, Date.now() + 120000);
const setResult = await store.set("ns", "key1", entry);
expect(setResult.err).toBeUndefined();
const getResult = await store.get("ns", "key1");
expect(getResult.err).toBeUndefined();
expect(getResult.val).toEqual(entry);
});
it("should return undefined for missing keys", async () => {
const result = await store.get("ns", "nonexistent");
expect(result.err).toBeUndefined();
expect(result.val).toBeUndefined();
});
it("should remove a single key", async () => {
const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000);
await store.set("ns", "key1", entry);
const removeResult = await store.remove("ns", "key1");
expect(removeResult.err).toBeUndefined();
const getResult = await store.get("ns", "key1");
expect(getResult.val).toBeUndefined();
});
it("should remove multiple keys", async () => {
const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000);
await store.set("ns", "key1", entry);
await store.set("ns", "key2", entry);
await store.set("ns", "key3", entry);
const removeResult = await store.remove("ns", ["key1", "key2"]);
expect(removeResult.err).toBeUndefined();
expect((await store.get("ns", "key1")).val).toBeUndefined();
expect((await store.get("ns", "key2")).val).toBeUndefined();
expect((await store.get("ns", "key3")).val).not.toBeUndefined();
});
});
describe("namespace isolation", () => {
it("should isolate keys by namespace", async () => {
const entry1 = createEntry("value1", Date.now() + 60000, Date.now() + 120000);
const entry2 = createEntry("value2", Date.now() + 60000, Date.now() + 120000);
await store.set("ns1", "key", entry1);
await store.set("ns2", "key", entry2);
const result1 = await store.get("ns1", "key");
const result2 = await store.get("ns2", "key");
expect(result1.val?.value).toBe("value1");
expect(result2.val?.value).toBe("value2");
});
});
describe("TTL expiration", () => {
it("should return undefined for expired entries (past staleUntil)", async () => {
const entry = createEntry("value", Date.now() - 2000, Date.now() - 1000); // Already expired
await store.set("ns", "expired-key", entry);
const result = await store.get("ns", "expired-key");
expect(result.val).toBeUndefined();
});
it("should return entry that is stale but not expired", async () => {
const now = Date.now();
// Fresh until 1 second ago, stale until 1 hour from now
const entry = createEntry("value", now - 1000, now + 3600000);
await store.set("ns", "stale-key", entry);
const result = await store.get("ns", "stale-key");
expect(result.val).not.toBeUndefined();
expect(result.val?.value).toBe("value");
});
it("should delete expired entry on get", async () => {
const entry = createEntry("value", Date.now() - 2000, Date.now() - 1000);
await store.set("ns", "key", entry);
// First get should return undefined and delete
await store.get("ns", "key");
// Size should reflect deletion
expect(store.size).toBe(0);
});
});
describe("LRU eviction", () => {
it("should evict least recently used items when at capacity", async () => {
const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Fill the cache (max: 5)
await store.set("ns", "key1", entry("value1"));
await store.set("ns", "key2", entry("value2"));
await store.set("ns", "key3", entry("value3"));
await store.set("ns", "key4", entry("value4"));
await store.set("ns", "key5", entry("value5"));
expect(store.size).toBe(5);
// Add one more - should evict key1 (least recently used)
await store.set("ns", "key6", entry("value6"));
expect(store.size).toBe(5);
expect((await store.get("ns", "key1")).val).toBeUndefined(); // Evicted
expect((await store.get("ns", "key6")).val?.value).toBe("value6"); // Present
});
it("should update LRU order on get", async () => {
const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Fill the cache
await store.set("ns", "key1", entry("value1"));
await store.set("ns", "key2", entry("value2"));
await store.set("ns", "key3", entry("value3"));
await store.set("ns", "key4", entry("value4"));
await store.set("ns", "key5", entry("value5"));
// Access key1 to make it recently used
await store.get("ns", "key1");
// Add new item - should evict key2 (now least recently used)
await store.set("ns", "key6", entry("value6"));
expect((await store.get("ns", "key1")).val?.value).toBe("value1"); // Still present
expect((await store.get("ns", "key2")).val).toBeUndefined(); // Evicted
});
it("should update LRU order on set (update existing)", async () => {
const entry = (val: string) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Fill the cache
await store.set("ns", "key1", entry("value1"));
await store.set("ns", "key2", entry("value2"));
await store.set("ns", "key3", entry("value3"));
await store.set("ns", "key4", entry("value4"));
await store.set("ns", "key5", entry("value5"));
// Update key1 to make it recently used
await store.set("ns", "key1", entry("updated-value1"));
// Add new item - should evict key2 (now least recently used)
await store.set("ns", "key6", entry("value6"));
expect((await store.get("ns", "key1")).val?.value).toBe("updated-value1");
expect((await store.get("ns", "key2")).val).toBeUndefined(); // Evicted
});
});
describe("hard limit enforcement", () => {
it("should never exceed max size regardless of write rate", async () => {
const smallStore = new LRUMemoryStore<string, number>({ max: 10 });
const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Write 1000 items rapidly
for (let i = 0; i < 1000; i++) {
await smallStore.set("ns", `key${i}`, entry(i));
// Verify size never exceeds max
expect(smallStore.size).toBeLessThanOrEqual(10);
}
expect(smallStore.size).toBe(10);
});
it("should maintain most recent items when at capacity", async () => {
const smallStore = new LRUMemoryStore<string, number>({ max: 3 });
const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Write items sequentially
await smallStore.set("ns", "key1", entry(1));
await smallStore.set("ns", "key2", entry(2));
await smallStore.set("ns", "key3", entry(3));
await smallStore.set("ns", "key4", entry(4));
await smallStore.set("ns", "key5", entry(5));
// Only the 3 most recent should remain
expect((await smallStore.get("ns", "key1")).val).toBeUndefined();
expect((await smallStore.get("ns", "key2")).val).toBeUndefined();
expect((await smallStore.get("ns", "key3")).val?.value).toBe(3);
expect((await smallStore.get("ns", "key4")).val?.value).toBe(4);
expect((await smallStore.get("ns", "key5")).val?.value).toBe(5);
});
});
describe("utility methods", () => {
it("should report correct size", async () => {
const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000);
expect(store.size).toBe(0);
await store.set("ns", "key1", entry);
expect(store.size).toBe(1);
await store.set("ns", "key2", entry);
expect(store.size).toBe(2);
await store.remove("ns", "key1");
expect(store.size).toBe(1);
});
it("should clear all items", async () => {
const entry = createEntry("value", Date.now() + 60000, Date.now() + 120000);
await store.set("ns1", "key1", entry);
await store.set("ns2", "key2", entry);
await store.set("ns3", "key3", entry);
expect(store.size).toBe(3);
store.clear();
expect(store.size).toBe(0);
expect((await store.get("ns1", "key1")).val).toBeUndefined();
});
it("should use custom name", () => {
const customStore = new LRUMemoryStore({ max: 10, name: "custom-name" });
expect(customStore.name).toBe("custom-name");
});
it("should use default name when not provided", () => {
const defaultStore = new LRUMemoryStore({ max: 10 });
expect(defaultStore.name).toBe("lru-memory");
});
});
describe("createLRUMemoryStore helper", () => {
it("should create a store with specified max size", async () => {
const helperStore = createLRUMemoryStore(3);
const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
await helperStore.set("ns", "key1", entry(1));
await helperStore.set("ns", "key2", entry(2));
await helperStore.set("ns", "key3", entry(3));
await helperStore.set("ns", "key4", entry(4));
expect(helperStore.size).toBe(3);
expect((await helperStore.get("ns", "key1")).val).toBeUndefined();
});
it("should accept custom name", () => {
const namedStore = createLRUMemoryStore(10, "my-cache");
expect(namedStore.name).toBe("my-cache");
});
});
describe("complex value types", () => {
it("should handle object values", async () => {
const objectStore = new LRUMemoryStore<string, { id: number; data: string[] }>({ max: 5 });
const complexValue = { id: 123, data: ["a", "b", "c"] };
const entry = createEntry(complexValue, Date.now() + 60000, Date.now() + 120000);
await objectStore.set("ns", "obj-key", entry);
const result = await objectStore.get("ns", "obj-key");
expect(result.val?.value).toEqual(complexValue);
});
it("should handle null and undefined values", async () => {
const nullStore = new LRUMemoryStore<string, null | undefined>({ max: 5 });
const nullEntry = createEntry(null, Date.now() + 60000, Date.now() + 120000);
const undefinedEntry = createEntry(undefined, Date.now() + 60000, Date.now() + 120000);
await nullStore.set("ns", "null-key", nullEntry);
await nullStore.set("ns", "undefined-key", undefinedEntry);
expect((await nullStore.get("ns", "null-key")).val?.value).toBeNull();
expect((await nullStore.get("ns", "undefined-key")).val?.value).toBeUndefined();
});
});
describe("concurrent operations", () => {
it("should handle concurrent writes safely", async () => {
const concurrentStore = new LRUMemoryStore<string, number>({ max: 100 });
const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Simulate concurrent writes
const writes = Array.from({ length: 50 }, (_, i) =>
concurrentStore.set("ns", `key${i}`, entry(i))
);
await Promise.all(writes);
expect(concurrentStore.size).toBe(50);
});
it("should handle concurrent reads and writes", async () => {
const concurrentStore = new LRUMemoryStore<string, number>({ max: 100 });
const entry = (val: number) => createEntry(val, Date.now() + 60000, Date.now() + 120000);
// Pre-populate
for (let i = 0; i < 50; i++) {
await concurrentStore.set("ns", `key${i}`, entry(i));
}
// Mix of reads and writes
const operations = [
...Array.from({ length: 25 }, (_, i) => concurrentStore.get("ns", `key${i}`)),
...Array.from({ length: 25 }, (_, i) =>
concurrentStore.set("ns", `new-key${i}`, entry(i + 100))
),
];
await Promise.all(operations);
// Should not exceed max
expect(concurrentStore.size).toBeLessThanOrEqual(100);
});
});
});
+165
View File
@@ -0,0 +1,165 @@
import { LRUCache } from "lru-cache";
import { CacheError } from "@unkey/cache";
import type { Store, Entry } from "@unkey/cache/stores";
import { Ok, Err, type Result } from "@unkey/error";
export type LRUMemoryStoreConfig = {
/**
* Maximum number of items to store in the cache.
* This is a hard limit - the cache will never exceed this size.
*/
max: number;
/**
* Name for metrics/tracing.
* @default "lru-memory"
*/
name?: string;
};
/**
* A memory store implementation using lru-cache.
*
* This provides O(1) get/set/delete operations and automatic LRU eviction
* without blocking the event loop (unlike @unkey/cache's MemoryStore which
* uses O(n) synchronous iteration for eviction).
*
* TTL is checked lazily on get() - expired items are not proactively removed
* but will be evicted by LRU when the cache is full.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class LRUMemoryStore<TNamespace extends string, TValue = any> implements Store<
TNamespace,
TValue
> {
readonly name: string;
private readonly cache: LRUCache<string, Entry<TValue>>;
constructor(config: LRUMemoryStoreConfig) {
this.name = config.name ?? "lru-memory";
this.cache = new LRUCache<string, Entry<TValue>>({
max: config.max,
// Don't use ttlAutopurge - it creates a setTimeout per item which
// doesn't scale well at high throughput (thousands of items/second).
// Instead, we check TTL lazily on get().
ttlAutopurge: false,
// Allow returning stale values - the cache layer handles SWR semantics
allowStale: true,
// Use the staleUntil timestamp for TTL calculation
ttl: 1, // Placeholder, we set per-item TTL in set()
});
}
private buildCacheKey(namespace: TNamespace, key: string): string {
return `${namespace}::${key}`;
}
async get(
namespace: TNamespace,
key: string
): Promise<Result<Entry<TValue> | undefined, CacheError>> {
try {
const cacheKey = this.buildCacheKey(namespace, key);
const entry = this.cache.get(cacheKey);
if (!entry) {
return Ok(undefined);
}
// Check if entry is expired (past staleUntil)
// The cache layer will handle fresh vs stale semantics
if (entry.staleUntil <= Date.now()) {
// Remove expired entry
this.cache.delete(cacheKey);
return Ok(undefined);
}
return Ok(entry);
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: err instanceof Error ? err.message : String(err),
})
);
}
}
async set(
namespace: TNamespace,
key: string,
entry: Entry<TValue>
): Promise<Result<void, CacheError>> {
try {
const cacheKey = this.buildCacheKey(namespace, key);
// Calculate TTL from staleUntil timestamp
const ttl = Math.max(0, entry.staleUntil - Date.now());
this.cache.set(cacheKey, entry, { ttl });
return Ok(undefined as void);
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: err instanceof Error ? err.message : String(err),
})
);
}
}
async remove(namespace: TNamespace, keys: string | string[]): Promise<Result<void, CacheError>> {
try {
const keyArray = Array.isArray(keys) ? keys : [keys];
for (const key of keyArray) {
const cacheKey = this.buildCacheKey(namespace, key);
this.cache.delete(cacheKey);
}
return Ok(undefined as void);
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key: Array.isArray(keys) ? keys.join(",") : keys,
message: err instanceof Error ? err.message : String(err),
})
);
}
}
/**
* Returns the current number of items in the cache.
*/
get size(): number {
return this.cache.size;
}
/**
* Clears all items from the cache.
*/
clear(): void {
this.cache.clear();
}
}
/**
* Creates an LRU memory store with the specified maximum size.
*
* This is a drop-in replacement for createMemoryStore() that uses lru-cache
* instead of @unkey/cache's MemoryStore, providing:
* - O(1) operations (vs O(n) eviction in MemoryStore)
* - No event loop blocking
* - Strict memory bounds (hard max vs soft cap)
*
* @param maxItems Maximum number of items to store
* @param name Optional name for metrics/tracing (default: "lru-memory")
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createLRUMemoryStore(maxItems: number, name?: string): LRUMemoryStore<string, any> {
return new LRUMemoryStore({ max: maxItems, name });
}
+13
View File
@@ -0,0 +1,13 @@
import { MemoryStore } from "@unkey/cache/stores";
export type { MemoryStore };
export function createMemoryStore(maxItems: number, frequency: number = 0.01) {
return new MemoryStore({
persistentMap: new Map(),
unstableEvictOnSet: {
frequency,
maxItems,
},
});
}
+107
View File
@@ -0,0 +1,107 @@
import { CacheError } from "@unkey/cache";
import type { Entry, Store } from "@unkey/cache/stores";
import { Err, Ok, type Result } from "@unkey/error";
import type { Redis, RedisOptions } from "@internal/redis";
import { createRedisClient } from "@internal/redis";
export type RedisCacheStoreConfig = {
connection: RedisOptions;
name?: string;
useModernCacheKeyBuilder?: boolean;
};
export class RedisCacheStore<TNamespace extends string, TValue = any> implements Store<
TNamespace,
TValue
> {
public readonly name = "redis";
private readonly redis: Redis;
constructor(private readonly config: RedisCacheStoreConfig) {
this.redis = createRedisClient({
...config.connection,
name: config.name ?? "trigger:cacheStore",
});
}
private buildCacheKey(namespace: TNamespace, key: string): string {
if (this.config.useModernCacheKeyBuilder) {
return [namespace, key].join(":");
}
return [namespace, key].join("::");
}
public async get(
namespace: TNamespace,
key: string
): Promise<Result<Entry<TValue> | undefined, CacheError>> {
let raw: string | null;
try {
raw = await this.redis.get(this.buildCacheKey(namespace, key));
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
if (!raw) {
return Promise.resolve(Ok(undefined));
}
try {
const superjson = await import("superjson");
const entry = superjson.parse(raw) as Entry<TValue>;
return Ok(entry);
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
public async set(
namespace: TNamespace,
key: string,
entry: Entry<TValue>
): Promise<Result<void, CacheError>> {
const cacheKey = this.buildCacheKey(namespace, key);
try {
const superjson = await import("superjson");
await this.redis.set(cacheKey, superjson.stringify(entry), "PXAT", entry.staleUntil);
return Ok();
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
public async remove(namespace: TNamespace, key: string): Promise<Result<void, CacheError>> {
try {
const cacheKey = this.buildCacheKey(namespace, key);
await this.redis.del(cacheKey);
return Promise.resolve(Ok());
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2019",
"lib": ["ES2019", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
"module": "Node16",
"moduleResolution": "Node16",
"moduleDetection": "force",
"verbatimModuleSyntax": false,
"types": ["vitest/globals"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"preserveWatchOutput": true,
"skipLibCheck": true,
"noEmit": true,
"strict": true,
"paths": {
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"]
}
},
"exclude": ["node_modules"]
}
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.test.ts"],
globals: true,
isolate: true,
testTimeout: 10_000,
},
});