This commit is contained in:
2026-09-05 04:33:55 +05:30
commit 02a4264bc3
121 changed files with 7913 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from "bun:test";
import { LRUCache, MemoryCache } from "../src/cache";
describe("LRUCache", () => {
let cache: LRUCache<string, Error>;
const maxSize = 2;
beforeEach(() => {
cache = new LRUCache(maxSize);
});
it("inserts and retrieves items", () => {
const key = "foo";
const lastModified = Date.now();
const entry = { data: "bar", isValidating: false, isLoading: false, lastModified };
cache.set(key, entry);
expect(cache.get(key)).toEqual(entry);
});
it("evicts oldest item when max size is reached", () => {
cache.set("1", { data: "1", isValidating: false, isLoading: false, lastModified: Date.now() });
cache.set("2", { data: "2", isValidating: false, isLoading: false, lastModified: Date.now() });
cache.set("3", { data: "3", isValidating: false, isLoading: false, lastModified: Date.now() });
expect(cache.has("1")).toBe(false);
expect(cache.has("2")).toBe(true);
expect(cache.has("3")).toBe(true);
});
it("updates LRU order on get", () => {
cache.set("1", { data: "1", isValidating: false, isLoading: false, lastModified: Date.now() });
cache.set("2", { data: "2", isValidating: false, isLoading: false, lastModified: Date.now() });
// Access "1" to make it most recent
cache.get("1");
cache.set("3", { data: "3", isValidating: false, isLoading: false, lastModified: Date.now() });
expect(cache.has("2")).toBe(false); // "2" was oldest
expect(cache.has("1")).toBe(true);
expect(cache.has("3")).toBe(true);
});
it("handles TTL expiration", () => {
vi.useFakeTimers();
const now = Date.now();
cache.set("exp", { data: "val", isValidating: false, isLoading: false, lastModified: now, ttl: 1000 });
expect(cache.get("exp")).toBeDefined();
vi.advanceTimersByTime(1001);
expect(cache.get("exp")).toBeUndefined();
vi.useRealTimers();
});
});
describe("MemoryCache", () => {
it("cleans up on destroy", () => {
vi.useFakeTimers();
const cache = new MemoryCache(100, 1000);
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval');
cache.destroy();
expect(clearIntervalSpy).toHaveBeenCalled();
vi.useRealTimers();
});
});
+116
View File
@@ -0,0 +1,116 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import { createSWR } from "../src/swr";
import type { Key, FetcherOptions } from "../src/types";
describe("SWRCore", () => {
let swr: any;
let fetcher: any;
const key = "test-key";
beforeEach(() => {
fetcher = vi.fn((k: Key, options: FetcherOptions) => Promise.resolve(`data-${k}`));
swr = createSWR({ fetcher, dedupingInterval: 100 });
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("fetches data on subscription if not in cache", async () => {
const callback = vi.fn();
swr.subscribe(key, callback);
// Initial fetch is async
vi.runAllTimers();
expect(fetcher).toHaveBeenCalledWith(key, expect.objectContaining({ signal: expect.any(AbortSignal) }));
expect(callback).toHaveBeenCalledWith(`data-${key}`, undefined, false, false);
});
it("deduplicates concurrent fetches", async () => {
swr.revalidate(key);
swr.revalidate(key);
swr.revalidate(key);
expect(fetcher).toHaveBeenCalledTimes(1);
});
it("updates all subscribers on mutation", async () => {
const cb1 = vi.fn();
const cb2 = vi.fn();
swr.subscribe(key, cb1);
swr.subscribe(key, cb2);
await swr.mutate(key, "manual-data");
expect(cb1).toHaveBeenCalledWith("manual-data", undefined, false, false);
expect(cb2).toHaveBeenCalledWith("manual-data", undefined, false, false);
});
it("handles fetcher errors", async () => {
const error = new Error("fetch failed");
fetcher.mockRejectedValue(error);
const callback = vi.fn();
swr.subscribe(key, callback);
vi.runAllTimers();
expect(callback).toHaveBeenCalledWith(undefined, error, false, false);
});
it("respects revalidateOnMount: false", async () => {
swr.updateConfig({ revalidateOnMount: false });
// Put something in cache
await swr.mutate(key, "cached");
fetcher.mockClear();
const callback = vi.fn();
swr.subscribe(key, callback);
vi.runAllTimers();
// Should not have fetched again despite subscription
expect(fetcher).not.toHaveBeenCalled();
});
it("refreshInterval periodically revalidates with per-key fetcher", async () => {
const intervalFetcher = vi.fn((k: Key, options: FetcherOptions) => Promise.resolve(`data-${k}`));
swr.updateConfig({ refreshInterval: 1000 });
const callback = vi.fn();
swr.subscribe(key, callback, intervalFetcher);
// Initial fetch (only run pending timers, not the interval)
vi.runOnlyPendingTimers();
expect(intervalFetcher).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(`data-${key}`, undefined, false, false);
// After interval passes, should revalidate again
vi.advanceTimersByTime(1000);
expect(intervalFetcher).toHaveBeenCalledTimes(2);
// After another interval, should revalidate again
vi.advanceTimersByTime(1000);
expect(intervalFetcher).toHaveBeenCalledTimes(3);
});
it("revalidate uses stored fetcher when called without fetcher option", async () => {
// Create SWR without global fetcher
const noGlobalFetcherSwr = createSWR({ dedupingInterval: 100 });
const perKeyFetcher = vi.fn((k: Key, options: FetcherOptions) => Promise.resolve(`data-${k}`));
const callback = vi.fn();
noGlobalFetcherSwr.subscribe(key, callback, perKeyFetcher);
// Initial fetch
vi.runOnlyPendingTimers();
expect(perKeyFetcher).toHaveBeenCalledTimes(1);
// Revalidate without passing fetcher - should use stored fetcher
await noGlobalFetcherSwr.revalidate(key);
expect(perKeyFetcher).toHaveBeenCalledTimes(2);
});
});
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "bunup";
export default defineConfig({
entry: "./src/index.ts",
dts: true,
});
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@supwr/core",
"version": "0.0.1",
"type": "module",
"files": [
"dist"
],
"module": "./dist/index.js",
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./package.json": "./package.json"
},
"scripts": {
"build": "bunup"
},
"devDependencies": {
"@types/bun": "^1.4.0",
"bunup": "^0.16.32"
}
}
+140
View File
@@ -0,0 +1,140 @@
import type { Cache, CacheEntry, Key } from './types'
/**
* LRU Cache implementation with TTL support
*/
export class LRUCache<Data = unknown, Error = unknown> implements Cache<Data, Error> {
private cache = new Map<string, CacheEntry<Data, Error>>()
private maxSize: number
constructor(maxSize: number = 1000) {
this.maxSize = maxSize
}
private serializeKey(key: Key): string {
if (key === null || key === undefined) {
return 'null'
}
if (typeof key === 'string') {
return key
}
if (Array.isArray(key)) {
return `["${key.join('","')}"]`
}
return JSON.stringify(key)
}
private isExpired(entry: CacheEntry<Data, Error>): boolean {
if (!entry.ttl) return false
return Date.now() - entry.lastModified > entry.ttl
}
get(key: Key): CacheEntry<Data, Error> | undefined {
const serializedKey = this.serializeKey(key)
const entry = this.cache.get(serializedKey)
if (!entry) return undefined
if (this.isExpired(entry)) {
this.cache.delete(serializedKey)
return undefined
}
// Move to end (LRU behavior)
this.cache.delete(serializedKey)
this.cache.set(serializedKey, entry)
return entry
}
set(key: Key, value: CacheEntry<Data, Error>): void {
const serializedKey = this.serializeKey(key)
// Remove oldest if at capacity
if (this.cache.size >= this.maxSize && !this.cache.has(serializedKey)) {
const firstKey = this.cache.keys().next().value
if (firstKey) {
this.cache.delete(firstKey)
}
}
this.cache.set(serializedKey, {
...value,
lastModified: Date.now()
})
}
delete(key: Key): void {
const serializedKey = this.serializeKey(key)
this.cache.delete(serializedKey)
}
clear(): void {
this.cache.clear()
}
keys(): Key[] {
return Array.from(this.cache.keys()).map(key => {
try {
return JSON.parse(key)
} catch {
return key
}
})
}
has(key: Key): boolean {
const serializedKey = this.serializeKey(key)
const entry = this.cache.get(serializedKey)
if (!entry) return false
if (this.isExpired(entry)) {
this.cache.delete(serializedKey)
return false
}
return true
}
// Cleanup expired entries
cleanup(): void {
for (const [key, entry] of this.cache.entries()) {
if (this.isExpired(entry)) {
this.cache.delete(key)
}
}
}
// Get cache size
size(): number {
return this.cache.size
}
}
/**
* Memory cache with automatic cleanup
*/
export class MemoryCache<Data = unknown, Error = unknown> extends LRUCache<Data, Error> {
private cleanupInterval: ReturnType<typeof globalThis.setInterval> | null = null
constructor(maxSize: number = 1000, cleanupIntervalMs: number = 60000) {
super(maxSize)
// Set up periodic cleanup
this.cleanupInterval = globalThis.setInterval(() => {
this.cleanup()
}, cleanupIntervalMs)
}
destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
this.clear()
}
}
+163
View File
@@ -0,0 +1,163 @@
import type { Callback, EventManager, Key } from './types'
import { serializeKey } from './key'
/**
* Event manager for handling SWR state changes
*/
export class SWREventManager implements EventManager {
private listeners = new Map<string, Set<Callback<unknown, unknown>>>()
on(event: string, callback: Callback<unknown, unknown>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
const eventListeners = this.listeners.get(event)!
eventListeners.add(callback)
// Return unsubscribe function
return () => {
eventListeners.delete(callback)
if (eventListeners.size === 0) {
this.listeners.delete(event)
}
}
}
emit(event: string, ...args: unknown[]): void {
const eventListeners = this.listeners.get(event)
if (!eventListeners) return
// Create a copy to avoid issues with listeners being added/removed during iteration
const listeners = Array.from(eventListeners)
for (const callback of listeners) {
try {
callback(...args)
} catch (error) {
console.error('Error in event listener:', error)
}
}
}
off(event: string, callback: Callback<unknown, unknown>): void {
const eventListeners = this.listeners.get(event)
if (!eventListeners) return
eventListeners.delete(callback)
if (eventListeners.size === 0) {
this.listeners.delete(event)
}
}
// Clear all listeners for an event or all events
clear(event?: string): void {
if (event) {
this.listeners.delete(event)
} else {
this.listeners.clear()
}
}
// Get the number of listeners for an event
listenerCount(event: string): number {
return this.listeners.get(event)?.size || 0
}
// Get all event names
eventNames(): string[] {
return Array.from(this.listeners.keys())
}
}
/**
* Key-specific event manager that handles events per cache key
*/
export class KeyEventManager {
private eventManager = new SWREventManager()
private keyListeners = new Map<string, Set<Callback<unknown, unknown>>>()
subscribe<Data = unknown, Error = unknown>(key: Key, callback: Callback<Data, Error>): () => void {
const serializedKey = serializeKey(key)
// Add to key-specific listeners
if (!this.keyListeners.has(serializedKey)) {
this.keyListeners.set(serializedKey, new Set())
}
this.keyListeners.get(serializedKey)!.add(callback as Callback<unknown, unknown>)
// Also subscribe to global events for this key
const unsubscribeGlobal = this.eventManager.on(`key:${serializedKey}`, callback as Callback<unknown, unknown>)
// Return unsubscribe function
return () => {
const keyListeners = this.keyListeners.get(serializedKey)
if (keyListeners) {
keyListeners.delete(callback as Callback<unknown, unknown>)
if (keyListeners.size === 0) {
this.keyListeners.delete(serializedKey)
}
}
unsubscribeGlobal()
}
}
emit(key: Key, ...args: unknown[]): void {
const serializedKey = serializeKey(key)
// Emit to key-specific listeners
const keyListeners = this.keyListeners.get(serializedKey)
if (keyListeners) {
const listeners = Array.from(keyListeners)
for (const callback of listeners) {
try {
callback(...args)
} catch (error) {
console.error('Error in key event listener:', error)
}
}
}
// Also emit to global event system
this.eventManager.emit(`key:${serializedKey}`, ...args)
}
// Global events
on(event: string, callback: Callback<unknown, unknown>): () => void {
return this.eventManager.on(event, callback)
}
emitGlobal(event: string, ...args: unknown[]): void {
this.eventManager.emit(event, ...args)
}
clear(key?: Key): void {
if (key) {
const serializedKey = serializeKey(key)
this.keyListeners.delete(serializedKey)
this.eventManager.clear(`key:${serializedKey}`)
} else {
this.keyListeners.clear()
this.eventManager.clear()
}
}
// Get statistics
getStats(): { keyListeners: number; globalListeners: number; totalKeys: number } {
let totalKeyListeners = 0
for (const listeners of this.keyListeners.values()) {
totalKeyListeners += listeners.size
}
let totalGlobalListeners = 0
for (const event of this.eventManager.eventNames()) {
totalGlobalListeners += this.eventManager.listenerCount(event)
}
return {
keyListeners: totalKeyListeners,
globalListeners: totalGlobalListeners,
totalKeys: this.keyListeners.size
}
}
}
+197
View File
@@ -0,0 +1,197 @@
import type { Fetcher, FetcherOptions, Key, CacheEntry } from './types'
import { serializeKey } from './key'
export class DeduplicationManager {
private pendingRequests = new Map<string, Promise<unknown>>()
constructor(private dedupingInterval: number = 2000) {}
async dedupe<Data>(
key: Key,
fetcher: Fetcher<Data>,
options: FetcherOptions
): Promise<Data> {
const serializedKey = serializeKey(key)
const pendingRequest = this.pendingRequests.get(serializedKey)
if (pendingRequest) {
return pendingRequest as Promise<Data>
}
const requestPromise = this.createRequest(key, fetcher, options)
this.pendingRequests.set(serializedKey, requestPromise)
try {
const result = await requestPromise
return result
} finally {
this.pendingRequests.delete(serializedKey)
}
}
private async createRequest<Data>(
key: Key,
fetcher: Fetcher<Data>,
options: FetcherOptions
): Promise<Data> {
try {
return await fetcher(key, options)
} catch (error) {
throw error
}
}
clear(): void {
this.pendingRequests.clear()
}
getPendingCount(): number {
return this.pendingRequests.size
}
isPending(key: Key): boolean {
const serializedKey = serializeKey(key)
return this.pendingRequests.has(serializedKey)
}
}
export class FetchManager {
private deduplicationManager: DeduplicationManager
private abortControllers = new Map<string, AbortController>()
constructor(dedupingInterval: number = 2000) {
this.deduplicationManager = new DeduplicationManager(dedupingInterval)
}
async fetch<Data>(
key: Key,
fetcher: Fetcher<Data>,
options: {
dedupe?: boolean
timeout?: number
signal?: AbortSignal
} = {}
): Promise<Data> {
const serializedKey = serializeKey(key)
const abortController = new AbortController()
this.abortControllers.set(serializedKey, abortController)
if (options.signal) {
if (options.signal.aborted) {
abortController.abort()
throw new DOMException('Request aborted', 'AbortError')
}
options.signal.addEventListener('abort', () => {
abortController.abort()
}, { once: true })
}
let timeoutId: ReturnType<typeof globalThis.setInterval> | undefined
if (options.timeout) {
timeoutId = globalThis.setTimeout(() => {
abortController.abort()
}, options.timeout)
}
try {
const fetcherOptions: FetcherOptions = {
signal: abortController.signal
}
let result: Data
if (options.dedupe !== false) {
result = await this.deduplicationManager.dedupe(key, fetcher, fetcherOptions)
} else {
result = await fetcher(key, fetcherOptions)
}
return result
} catch (error) {
if (abortController.signal.aborted) {
throw new DOMException('Request aborted', 'AbortError')
}
throw error
} finally {
this.abortControllers.delete(serializedKey)
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}
abort(key: Key): void {
const serializedKey = serializeKey(key)
const controller = this.abortControllers.get(serializedKey)
if (controller) {
controller.abort()
this.abortControllers.delete(serializedKey)
}
}
abortAll(): void {
for (const controller of this.abortControllers.values()) {
controller.abort()
}
this.abortControllers.clear()
this.deduplicationManager.clear()
}
isFetching(key: Key): boolean {
const serializedKey = serializeKey(key)
return this.abortControllers.has(serializedKey) || this.deduplicationManager.isPending(key)
}
getActiveCount(): number {
return this.abortControllers.size
}
getPendingCount(): number {
return this.deduplicationManager.getPendingCount()
}
}
export function createFetcher<Data = unknown>(
input: string | URL,
init?: RequestInit
): Fetcher<Data> {
return async (key: Key, options: FetcherOptions) => {
const url = typeof key === 'string' ? key : String(key)
const response = await fetch(url, {
...init,
signal: options.signal
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json() as Promise<Data>
}
}
export function createFetcherWithErrorHandler<Data = unknown>(
fetcher: (url: string, options: RequestInit) => Promise<Response>,
errorHandler?: (error: unknown, key: Key) => Error
): Fetcher<Data> {
return async (key: Key, options: FetcherOptions) => {
try {
const url = typeof key === 'string' ? key : String(key)
const response = await fetcher(url, { signal: options.signal })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json() as Promise<Data>
} catch (error) {
if (errorHandler) {
throw errorHandler(error, key)
}
throw error
}
}
}
+65
View File
@@ -0,0 +1,65 @@
// Core types
export type {
Key,
CacheEntry,
CacheOptions,
FetcherOptions,
Fetcher,
BareFetcher,
SWRFetcher,
MutatorOptions,
MutatorCallback,
MutatorValue,
RevalidateOptions,
Callback,
Unsubscribe,
SWRConfig,
Cache,
EventManager,
SWRCore
} from './types'
// Core implementation
export { SWRCoreImpl, createSWR } from './swr'
// Cache
export { LRUCache, MemoryCache } from './cache'
// Events
export { SWREventManager, KeyEventManager } from './events'
// Key utilities
export {
serializeKey,
resolveKey,
hashKey,
areKeysEqual,
isValidKey,
normalizeKey
} from './key'
// Configuration
export { defaultConfig, mergeConfig, validateConfig, ConfigManager } from './options'
// Fetcher
export {
DeduplicationManager,
FetchManager,
createFetcher,
createFetcherWithErrorHandler
} from './fetcher'
// Revalidation
export { RevalidationManager, setupGlobalRevalidationHandlers } from './revalidate'
// Mutation
export {
MutationManager,
createMutator,
createMutateFunction,
createOptimisticMutation,
createAsyncMutation
} from './mutate'
// Default export
export { createSWR as default } from './swr'
+87
View File
@@ -0,0 +1,87 @@
import type { Key } from './types'
/**
* Key resolution and serialization utilities
*/
export function serializeKey(key: Key): string {
if (key === null || key === undefined) {
return 'null'
}
if (typeof key === 'string') {
return key
}
if (Array.isArray(key)) {
// Handle nested arrays and objects
return `[${key.map(item => serializeKey(item)).join(',')}]`
}
if (typeof key === 'object') {
try {
return JSON.stringify(key, (k, v) => {
// Handle functions by converting to string representation
if (typeof v === 'function') {
return `[Function: ${v.name || 'anonymous'}]`
}
// Handle undefined
if (v === undefined) {
return '[Undefined]'
}
// Handle symbols
if (typeof v === 'symbol') {
return `[Symbol: ${v.toString()}]`
}
return v
})
} catch (error) {
// Fallback for circular references
return '[Object]'
}
}
return String(key)
}
export function resolveKey(key: Key | (() => Key)): Key {
if (typeof key === 'function') {
try {
return key()
} catch (error) {
console.error('Error resolving key function:', error)
return null
}
}
return key
}
export function hashKey(key: Key): string {
const serialized = serializeKey(key)
// Simple hash function for better distribution
let hash = 0
for (let i = 0; i < serialized.length; i++) {
const char = serialized.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // Convert to 32-bit integer
}
return Math.abs(hash).toString(36)
}
export function areKeysEqual(key1: Key, key2: Key): boolean {
return serializeKey(key1) === serializeKey(key2)
}
export function isValidKey(key: Key): boolean {
return key !== null && key !== undefined
}
export function normalizeKey(key: Key): string {
if (!isValidKey(key)) {
return 'null'
}
return serializeKey(key)
}
+267
View File
@@ -0,0 +1,267 @@
import type {
Key,
MutatorOptions,
MutatorValue,
MutatorCallback,
CacheEntry,
SWRConfig,
Fetcher
} from './types'
import { serializeKey } from './key'
import { FetchManager } from './fetcher'
/**
* Mutation manager for handling data mutations with optimistic updates
*/
export class MutationManager<Data = unknown, Error = unknown> {
private optimisticData = new Map<string, Data>()
private rollbackData = new Map<string, Data | undefined>()
constructor(
private fetchManager: FetchManager,
private config: SWRConfig<Data, Error>,
private onUpdate: (key: Key, entry: Partial<CacheEntry<Data, Error>>) => void,
private getCurrentData: (key: Key) => Data | undefined,
private getFetcher?: (key: Key) => Fetcher<Data> | undefined
) {}
updateConfig(config: SWRConfig<Data, Error>): void {
this.config = config
}
/**
* Mutate data for a key
*/
async mutate(
key: Key,
data?: MutatorValue<Data>,
options: MutatorOptions<Data> = {}
): Promise<Data | undefined> {
const serializedKey = serializeKey(key)
const currentData = this.getCurrentData(key)
// Store rollback data if needed
if (options.rollbackOnError !== false && currentData !== undefined) {
this.rollbackData.set(serializedKey, currentData)
}
try {
// Handle optimistic data
if (options.optimisticData !== undefined) {
this.optimisticData.set(serializedKey, options.optimisticData)
this.onUpdate(key, {
data: options.optimisticData,
isLoading: false,
isValidating: false
})
} else if (data !== undefined) {
// Handle immediate data update
const newData = await this.resolveData(data, currentData)
this.optimisticData.set(serializedKey, newData)
this.onUpdate(key, {
data: newData,
isLoading: false,
isValidating: false
})
}
// Revalidate if requested
if (options.revalidate !== false) {
await this.revalidate(key)
}
// Return the final data
return this.getCurrentData(key)
} catch (error) {
// Rollback on error if requested
if (options.rollbackOnError !== false) {
const rollbackValue = this.rollbackData.get(serializedKey)
if (rollbackValue !== undefined) {
this.onUpdate(key, {
data: rollbackValue,
error: error as Error,
isLoading: false,
isValidating: false
})
}
}
throw error
} finally {
// Clean up optimistic data
this.optimisticData.delete(serializedKey)
this.rollbackData.delete(serializedKey)
}
}
/**
* Resolve data from various input types
*/
private async resolveData(
data: MutatorValue<Data>,
currentData: Data | undefined
): Promise<Data> {
if (typeof data === 'function') {
const result = (data as MutatorCallback<Data>)(currentData)
return Promise.resolve(result)
}
if (data === undefined) {
throw new Error('Mutation data cannot be undefined')
}
return data
}
/**
* Revalidate a key after mutation
*/
private async revalidate(key: Key): Promise<void> {
const fetcher = this.getFetcher?.(key) || this.config.fetcher
if (!fetcher) return
try {
this.onUpdate(key, { isValidating: true })
const freshData = await this.fetchManager.fetch(key, fetcher, {
dedupe: false, // Always fetch fresh data for revalidation
timeout: this.config.loadingTimeout
})
// Update with fresh data
this.onUpdate(key, {
data: freshData,
error: undefined,
isValidating: false,
isLoading: false
})
this.config.onSuccess?.(freshData, key)
} catch (error) {
const errorObj = error as Error
this.onUpdate(key, {
error: errorObj,
isValidating: false,
isLoading: false
})
this.config.onError?.(errorObj, key)
}
}
/**
* Check if a key has optimistic data
*/
hasOptimisticData(key: Key): boolean {
const serializedKey = serializeKey(key)
return this.optimisticData.has(serializedKey)
}
/**
* Get optimistic data for a key
*/
getOptimisticData(key: Key): Data | undefined {
const serializedKey = serializeKey(key)
return this.optimisticData.get(serializedKey)
}
/**
* Clear all optimistic data
*/
clearOptimisticData(): void {
this.optimisticData.clear()
this.rollbackData.clear()
}
/**
* Create a mutation function for a specific key
*/
createMutator(key: Key) {
return (
data?: MutatorValue<Data>,
options?: MutatorOptions<Data>
): Promise<Data | undefined> => {
return this.mutate(key, data, options)
}
}
/**
* Batch mutate multiple keys
*/
async mutateBatch(
mutations: Array<{
key: Key
data?: MutatorValue<Data>
options?: MutatorOptions<Data>
}>
): Promise<(Data | undefined)[]> {
const results = await Promise.allSettled(
mutations.map(({ key, data, options }) => this.mutate(key, data, options))
)
return results.map((result, index) => {
const mutation = mutations[index]
if (result.status === 'fulfilled') {
return result.value
} else {
console.error(`Mutation failed for key ${mutation?.key}:`, result.reason || 'Unknown error')
return undefined
}
})
}
}
/**
* Utility function to create a mutator
*/
export function createMutator<Data = unknown, Error = unknown>(
fetchManager: FetchManager,
config: SWRConfig<Data, Error>,
onUpdate: (key: Key, entry: Partial<CacheEntry<Data, Error>>) => void,
getCurrentData: (key: Key) => Data | undefined
): MutationManager<Data, Error> {
return new MutationManager(fetchManager, config, onUpdate, getCurrentData)
}
/**
* Higher-order function that creates a mutate function
*/
export function createMutateFunction<Data = unknown, Error = unknown>(
mutationManager: MutationManager<Data, Error>
) {
return (
key: Key,
data?: MutatorValue<Data>,
options?: MutatorOptions<Data>
): Promise<Data | undefined> => {
return mutationManager.mutate(key, data, options)
}
}
/**
* Utility for creating optimistic mutations
*/
export function createOptimisticMutation<Data = unknown>(
currentData: Data,
updateFn: (current: Data) => Data
): {
optimisticData: Data
rollbackData: Data
} {
return {
optimisticData: updateFn(currentData),
rollbackData: currentData
}
}
/**
* Utility for creating async mutations
*/
export function createAsyncMutation<Data = unknown>(
asyncFn: (currentData?: Data) => Promise<Data>
): MutatorCallback<Data> {
return async (currentData?: Data) => {
return await asyncFn(currentData)
}
}
+196
View File
@@ -0,0 +1,196 @@
import type { SWRConfig, Fetcher } from './types'
/**
* Default configuration for SWR
*/
export const defaultConfig: SWRConfig = {
// Cache options
ttl: 0, // No TTL by default
revalidateOnMount: true,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0,
dedupingInterval: 2000,
errorRetryCount: 3,
errorRetryInterval: 5000,
loadingTimeout: 3000,
// Callbacks
onSuccess: () => {},
onError: () => {},
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (retryCount >= Math.max(config.errorRetryCount || 0, 0)) return
const timeout = ~~((Math.random() + 0.5) * (1 << (retryCount < 8 ? retryCount : 8))) * (config.errorRetryInterval || 0)
setTimeout(revalidate, timeout)
},
onLoadingSlow: () => {},
// Utilities
isPaused: () => false,
compare: (a, b) => a === b,
fallback: {}
}
/**
* Merge user configuration with defaults
*/
export function mergeConfig<Data = any, Error = any>(
userConfig?: Partial<SWRConfig<Data, Error>>
): SWRConfig<Data, Error> {
if (!userConfig) return defaultConfig as SWRConfig<Data, Error>
return {
...defaultConfig,
...userConfig,
// Ensure arrays and objects are properly merged
fallback: { ...defaultConfig.fallback, ...userConfig.fallback } as Record<string, Data>
} as SWRConfig<Data, Error>
}
/**
* Validate configuration
*/
export function validateConfig<Data = any, Error = any>(
config: SWRConfig<Data, Error>
): { valid: boolean; errors: string[] } {
const errors: string[] = []
// Validate fetcher
if (config.fetcher && typeof config.fetcher !== 'function') {
errors.push('fetcher must be a function')
}
// Validate numeric options
const numericFields = [
'ttl',
'refreshInterval',
'dedupingInterval',
'errorRetryCount',
'errorRetryInterval',
'loadingTimeout'
] as const
for (const field of numericFields) {
const value = config[field]
if (value !== undefined && (typeof value !== 'number' || value < 0)) {
errors.push(`${field} must be a non-negative number`)
}
}
// Validate boolean options
const booleanFields = [
'revalidateOnFocus',
'revalidateOnReconnect'
] as const
for (const field of booleanFields) {
const value = config[field]
if (value !== undefined && typeof value !== 'boolean') {
errors.push(`${field} must be a boolean`)
}
}
// Validate callbacks
const callbackFields = [
'onSuccess',
'onError',
'onErrorRetry',
'onLoadingSlow',
'isPaused',
'compare'
] as const
for (const field of callbackFields) {
const value = config[field]
if (value !== undefined && typeof value !== 'function') {
errors.push(`${field} must be a function`)
}
}
// Validate fallback
if (config.fallback && typeof config.fallback !== 'object') {
errors.push('fallback must be an object')
}
return {
valid: errors.length === 0,
errors
}
}
/**
* Create a configuration manager
*/
export class ConfigManager<Data = any, Error = any> {
private config: SWRConfig<Data, Error>
constructor(userConfig?: Partial<SWRConfig<Data, Error>>) {
const merged = mergeConfig(userConfig)
const validation = validateConfig(merged)
if (!validation.valid) {
console.warn('Invalid SWR configuration:', validation.errors)
}
this.config = merged
}
get(): SWRConfig<Data, Error> {
return this.config
}
update(updates: Partial<SWRConfig<Data, Error>>): void {
this.config = { ...this.config, ...updates }
const validation = validateConfig(this.config)
if (!validation.valid) {
console.warn('Invalid SWR configuration update:', validation.errors)
}
}
setFetcher(fetcher: Fetcher<Data>): void {
this.config.fetcher = fetcher
}
getFetcher(): Fetcher<Data> | undefined {
return this.config.fetcher
}
isPaused(): boolean {
return !!this.config.isPaused?.()
}
shouldRevalidateOnFocus(): boolean {
return !!(this.config.revalidateOnFocus && !this.isPaused())
}
shouldRevalidateOnReconnect(): boolean {
return !!(this.config.revalidateOnReconnect && !this.isPaused())
}
getRefreshInterval(): number {
return this.config.refreshInterval || 0
}
getDedupingInterval(): number {
return this.config.dedupingInterval || 2000
}
getErrorRetryCount(): number {
return this.config.errorRetryCount || 3
}
getErrorRetryInterval(): number {
return this.config.errorRetryInterval || 5000
}
getLoadingTimeout(): number {
return this.config.loadingTimeout || 3000
}
getFallbackData(key: string): Data | undefined {
return this.config.fallback?.[key]
}
}
+313
View File
@@ -0,0 +1,313 @@
import type { Key, RevalidateOptions, SWRConfig, CacheEntry, Fetcher } from './types'
import { serializeKey } from './key'
import { FetchManager } from './fetcher'
/**
* Revalidation manager handles different revalidation strategies
*/
export class RevalidationManager<Data = any, Error = any> {
private intervals = new Map<string, ReturnType<typeof globalThis.setInterval>>()
private timeouts = new Map<string, ReturnType<typeof globalThis.setInterval>>()
private retryAttempts = new Map<string, number>()
private fetchers = new Map<string, Fetcher<Data>>()
constructor(
private fetchManager: FetchManager,
private config: SWRConfig<Data, Error>,
private onUpdate: (key: Key, entry: Partial<CacheEntry<Data, Error>>) => void
) {}
/**
* Get the fetcher for a key
*/
setFetcher(key: Key, fetcher: Fetcher<Data>): void {
const serializedKey = serializeKey(key)
this.fetchers.set(serializedKey, fetcher)
}
/**
* Get the stored fetcher for a key
*/
getFetcher(key: Key): Fetcher<Data> | undefined {
const serializedKey = serializeKey(key)
return this.fetchers.get(serializedKey)
}
/**
* Remove the fetcher for a key
*/
removeFetcher(key: Key): void {
const serializedKey = serializeKey(key)
this.fetchers.delete(serializedKey)
}
/**
* Start interval revalidation for a key
*/
startIntervalRevalidation(key: Key): void {
const serializedKey = serializeKey(key)
const interval = this.config.refreshInterval
if (!interval || interval <= 0) return
// Clear existing interval
this.stopIntervalRevalidation(key)
const intervalId = setInterval(() => {
if (!this.config.isPaused?.()) {
const fetcher = this.fetchers.get(serializedKey) || this.config.fetcher
this.revalidate(key, { dedupe: true, fetcher })
}
}, interval)
this.intervals.set(serializedKey, intervalId)
}
/**
* Stop interval revalidation for a key
*/
stopIntervalRevalidation(key: Key): void {
const serializedKey = serializeKey(key)
const intervalId = this.intervals.get(serializedKey)
if (intervalId) {
clearInterval(intervalId)
this.intervals.delete(serializedKey)
}
}
/**
* Revalidate on focus
*/
revalidateOnFocus(key: Key): void {
if (!this.config.revalidateOnFocus || this.config.isPaused?.()) return
this.revalidate(key, { dedupe: true })
}
/**
* Revalidate on reconnect
*/
revalidateOnReconnect(key: Key): void {
if (!this.config.revalidateOnReconnect || this.config.isPaused?.()) return
this.revalidate(key, { dedupe: true })
}
/**
* Manual revalidation
*/
async revalidate(key: Key, options: RevalidateOptions<Data> = {}): Promise<boolean> {
const serializedKey = serializeKey(key)
const fetcher = options.fetcher || this.fetchers.get(serializedKey) || this.config.fetcher
if (!fetcher) {
console.warn('No fetcher provided for revalidation')
return false
}
console.log('RevalidationManager.revalidate called for', key, 'dedupe:', options.dedupe, 'fetcher:', !!fetcher)
try {
// Set loading state
this.onUpdate(key, { isValidating: true })
const data = await this.fetchManager.fetch(key, fetcher, {
dedupe: options.dedupe !== false,
timeout: this.config.loadingTimeout
})
// Update cache with new data
this.onUpdate(key, {
data,
error: undefined,
isValidating: false,
isLoading: false,
lastModified: Date.now()
})
// Reset retry attempts on success
this.retryAttempts.delete(serializedKey)
// Call success callback
this.config.onSuccess?.(data, key)
return true
} catch (error) {
const errorObj = error as Error
// Update cache with error
this.onUpdate(key, {
error: errorObj,
isValidating: false,
isLoading: false,
lastModified: Date.now()
})
// Call error callback
this.config.onError?.(errorObj, key)
// Handle retry logic
await this.handleRetry(key, errorObj)
return false
}
}
/**
* Handle error retry logic
*/
private async handleRetry(key: Key, error: Error): Promise<void> {
const serializedKey = serializeKey(key)
const retryCount = this.retryAttempts.get(serializedKey) || 0
const maxRetries = this.config.errorRetryCount || 0
if (retryCount >= maxRetries) {
this.retryAttempts.delete(serializedKey)
return
}
// Increment retry count
this.retryAttempts.set(serializedKey, retryCount + 1)
// Calculate retry delay with exponential backoff
const baseInterval = this.config.errorRetryInterval || 5000
const delay = Math.min(baseInterval * Math.pow(2, retryCount), 30000)
// Schedule retry
const timeoutId = setTimeout(() => {
if (!this.config.isPaused?.()) {
this.revalidate(key, { dedupe: true })
}
}, delay)
this.timeouts.set(`${serializedKey}:retry`, timeoutId)
}
/**
* Cancel retry for a key
*/
cancelRetry(key: Key): void {
const serializedKey = serializeKey(key)
const timeoutId = this.timeouts.get(`${serializedKey}:retry`)
if (timeoutId) {
clearTimeout(timeoutId)
this.timeouts.delete(`${serializedKey}:retry`)
}
this.retryAttempts.delete(serializedKey)
}
/**
* Clear all revalidation for a key
*/
clear(key: Key): void {
this.stopIntervalRevalidation(key)
this.cancelRetry(key)
this.removeFetcher(key)
const serializedKey = serializeKey(key)
// Clear any other timeouts for this key
for (const [timeoutKey, timeoutId] of this.timeouts.entries()) {
if (timeoutKey.startsWith(serializedKey)) {
clearTimeout(timeoutId)
this.timeouts.delete(timeoutKey)
}
}
}
/**
* Clear all revalidation
*/
clearAll(): void {
// Clear all intervals
for (const intervalId of this.intervals.values()) {
clearInterval(intervalId)
}
this.intervals.clear()
// Clear all timeouts
for (const timeoutId of this.timeouts.values()) {
clearTimeout(timeoutId)
}
this.timeouts.clear()
// Clear retry attempts
this.retryAttempts.clear()
}
/**
* Update configuration
*/
updateConfig(config: Partial<SWRConfig<Data, Error>>): void {
this.config = { ...this.config, ...config }
// Restart interval revalidation with new config if needed
// This would need to be called for all active keys
}
/**
* Get revalidation statistics
*/
getStats(): {
intervalRevalidations: number
pendingRetries: number
retryAttempts: Map<string, number>
} {
return {
intervalRevalidations: this.intervals.size,
pendingRetries: this.timeouts.size,
retryAttempts: new Map(this.retryAttempts)
}
}
}
/**
* Setup global event listeners for focus and reconnect
*/
export function setupGlobalRevalidationHandlers(
revalidationManager: RevalidationManager,
keys: Key[]
): () => void {
const handleFocus = () => {
for (const key of keys) {
revalidationManager.revalidateOnFocus(key)
}
}
const handleReconnect = () => {
for (const key of keys) {
revalidationManager.revalidateOnReconnect(key)
}
}
const handleVisibilityChange = () => {
if (typeof globalThis !== 'undefined' && 'document' in globalThis && !(globalThis as any).document.hidden) {
handleFocus()
}
}
// Add event listeners
if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
const globalObj = globalThis as any
globalObj.addEventListener('focus', handleFocus)
globalObj.addEventListener('online', handleReconnect)
if ('document' in globalObj) {
globalObj.document.addEventListener('visibilitychange', handleVisibilityChange)
}
}
// Return cleanup function
return () => {
if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
const globalObj = globalThis as any
globalObj.removeEventListener('focus', handleFocus)
globalObj.removeEventListener('online', handleReconnect)
if ('document' in globalObj) {
globalObj.document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
}
}
+383
View File
@@ -0,0 +1,383 @@
import type {
SWRCore,
Key,
CacheEntry,
Callback,
Unsubscribe,
SWRConfig,
MutatorValue,
MutatorOptions,
RevalidateOptions,
Fetcher
} from './types'
import { MemoryCache } from './cache'
import { KeyEventManager } from './events'
import { ConfigManager } from './options'
import { FetchManager } from './fetcher'
import { RevalidationManager } from './revalidate'
import { MutationManager } from './mutate'
import { resolveKey, serializeKey } from './key'
/**
* Main SWR Core implementation
*/
export class SWRCoreImpl<Data = unknown, Error = unknown> implements SWRCore<Data, Error> {
private cache: MemoryCache<Data, Error>
private eventManager: KeyEventManager
private configManager: ConfigManager<Data, Error>
private fetchManager: FetchManager
private revalidationManager: RevalidationManager<Data, Error>
private mutationManager: MutationManager<Data, Error>
// Track active keys for cleanup
private activeKeys = new Set<string>()
constructor(userConfig?: Partial<SWRConfig<Data, Error>>) {
// Initialize core components
this.configManager = new ConfigManager(userConfig)
this.cache = new MemoryCache()
this.eventManager = new KeyEventManager()
this.fetchManager = new FetchManager(this.configManager.getDedupingInterval())
// Initialize managers with dependencies
this.revalidationManager = new RevalidationManager(
this.fetchManager,
this.configManager.get(),
this.handleUpdate.bind(this)
)
this.mutationManager = new MutationManager(
this.fetchManager,
this.configManager.get(),
this.handleUpdate.bind(this),
(key) => this.get(key)?.data,
(key) => this.revalidationManager.getFetcher(key)
)
// Setup global revalidation handlers
this.setupGlobalHandlers()
}
/**
* Get cache entry for a key
*/
get(key: Key): CacheEntry<Data, Error> | undefined {
const resolvedKey = resolveKey(key)
if (!resolvedKey) return undefined
return this.cache.get(resolvedKey)
}
/**
* Subscribe to state changes for a key
*/
subscribe(key: Key, callback: Callback<Data, Error>, fetcher?: Fetcher<Data>): Unsubscribe {
const resolvedKey = resolveKey(key)
if (!resolvedKey) {
return () => {} // No-op for invalid keys
}
const serializedKey = serializeKey(resolvedKey)
// Add to active keys
this.activeKeys.add(serializedKey)
// Store fetcher for interval revalidation
if (fetcher) {
this.revalidationManager.setFetcher(resolvedKey, fetcher)
}
// Check if we have an entry in cache
const entry = this.cache.get(resolvedKey)
// Subscribe to events
const unsubscribe = this.eventManager.subscribe(resolvedKey, callback as Callback<unknown, unknown>)
const config = this.configManager.get()
// Start initial fetch if no data exists
if (!entry) {
if (config.revalidateOnMount !== false) {
// Set initial loading state synchronously
this.handleUpdate(resolvedKey, {
isLoading: true,
isValidating: true
})
}
// Trigger initial revalidation asynchronously, but and check for deduplication
setTimeout(() => {
// Only revalidate if not already validating/loading (deduplication)
const currentEntry = this.get(resolvedKey)
if (currentEntry?.isLoading && !this.fetchManager.isFetching(resolvedKey)) {
this.revalidate(resolvedKey, {
fetcher,
dedupe: true
})
} else if (!currentEntry) {
this.revalidate(resolvedKey, {
fetcher,
dedupe: true
})
}
}, 0)
} else {
// If data exists, revalidate if revalidateOnMount is not false
if (config.revalidateOnMount !== false) {
// Use Promise.resolve().then() for consistency with SWR behavior,
// but skip if a fetch is already in flight to avoid duplicate requests
// (e.g. when a suspense-mode consumer has already kicked off a fetch).
Promise.resolve().then(() => {
if (!this.fetchManager.isFetching(resolvedKey)) {
this.revalidate(resolvedKey, { fetcher, dedupe: true })
}
})
}
}
// Return cleanup function
return () => {
unsubscribe()
this.activeKeys.delete(serializedKey)
this.revalidationManager.clear(resolvedKey)
this.revalidationManager.removeFetcher(resolvedKey)
}
}
/**
* Subscribe to state changes for a key WITHOUT triggering any fetch.
* Used by consumers (e.g. React hooks in suspense mode) that manage
* fetching themselves but still need notifications when data updates.
*/
addEventListener(key: Key, callback: Callback<Data, Error>): Unsubscribe {
const resolvedKey = resolveKey(key)
if (!resolvedKey) {
return () => {}
}
return this.eventManager.subscribe(resolvedKey, callback as Callback<unknown, unknown>)
}
/**
* Set the loading+validating state for a key without fetching.
*/
setLoading(key: Key): void {
const resolvedKey = resolveKey(key)
if (!resolvedKey) return
this.handleUpdate(resolvedKey, {
isLoading: true,
isValidating: true
})
}
/**
* Mutate data for a key
*/
async mutate(
key: Key,
data?: MutatorValue<Data>,
options?: MutatorOptions<Data>
): Promise<Data | undefined> {
const resolvedKey = resolveKey(key)
if (!resolvedKey) return undefined
return this.mutationManager.mutate(resolvedKey, data, options)
}
/**
* Revalidate data for a key
*/
async revalidate(key: Key, options?: RevalidateOptions): Promise<boolean> {
const resolvedKey = resolveKey(key)
if (!resolvedKey) return false
return this.revalidationManager.revalidate(resolvedKey, options as RevalidateOptions<Data>)
}
/**
* Delete cache entry for a key
*/
delete(key: Key): void {
const resolvedKey = resolveKey(key)
if (!resolvedKey) return
const serializedKey = serializeKey(resolvedKey)
this.cache.delete(resolvedKey)
this.revalidationManager.clear(resolvedKey)
this.eventManager.clear(resolvedKey)
this.activeKeys.delete(serializedKey)
// Emit delete event
this.eventManager.emit(resolvedKey, undefined, undefined, false, false)
}
/**
* Clear all cache and state
*/
clear(): void {
this.cache.clear()
this.revalidationManager.clearAll()
this.eventManager.clear()
this.fetchManager.abortAll()
this.mutationManager.clearOptimisticData()
this.activeKeys.clear()
}
/**
* Update configuration
*/
updateConfig(config: Partial<SWRConfig<Data, Error>>): void {
this.configManager.update(config)
// Update managers with new config
this.revalidationManager.updateConfig(this.configManager.get())
this.mutationManager.updateConfig(this.configManager.get())
// Restart interval revalidation for active keys
for (const serializedKey of this.activeKeys) {
try {
const key = JSON.parse(serializedKey) as Key
this.revalidationManager.startIntervalRevalidation(key)
} catch {
// Skip invalid keys
}
}
}
/**
* Get current configuration
*/
getConfig(): SWRConfig<Data, Error> {
return this.configManager.get()
}
/**
* Get cache statistics
*/
getCacheStats(): {
size: number
keys: Key[]
activeKeys: number
} {
return {
size: this.cache.size(),
keys: this.cache.keys(),
activeKeys: this.activeKeys.size
}
}
/**
* Get performance statistics
*/
getPerformanceStats(): {
activeRequests: number
pendingRequests: number
revalidations: unknown
} {
return {
activeRequests: this.fetchManager.getActiveCount(),
pendingRequests: this.fetchManager.getPendingCount(),
revalidations: this.revalidationManager.getStats()
}
}
/**
* Handle cache updates and emit events
*/
private handleUpdate(key: Key, update: Partial<CacheEntry<Data, Error>>): void {
const currentEntry = this.cache.get(key)
const updatedEntry: CacheEntry<Data, Error> = {
data: currentEntry?.data,
error: currentEntry?.error,
isValidating: currentEntry?.isValidating || false,
isLoading: currentEntry?.isLoading || false,
lastModified: currentEntry?.lastModified || Date.now(),
...update
}
// Update cache
this.cache.set(key, updatedEntry)
// Start/stop interval revalidation based on data availability
if (updatedEntry.data !== undefined) {
this.revalidationManager.startIntervalRevalidation(key)
} else {
this.revalidationManager.stopIntervalRevalidation(key)
}
// Emit state change
this.eventManager.emit(
key,
updatedEntry.data,
updatedEntry.error,
updatedEntry.isValidating,
updatedEntry.isLoading
)
}
/**
* Fetch initial data for a key
*/
private async fetchInitialData(key: Key, fetcher?: Fetcher<Data>): Promise<void> {
const config = this.configManager.get()
const activeFetcher = fetcher || config.fetcher
if (!activeFetcher) return
try {
// Set loading state
this.handleUpdate(key, { isLoading: true, isValidating: true })
const data = await this.fetchManager.fetch(key, activeFetcher, {
dedupe: true,
timeout: config.loadingTimeout
})
// Update with data
this.handleUpdate(key, {
data,
error: undefined,
isLoading: false,
isValidating: false
})
config.onSuccess?.(data, key)
} catch (error) {
const errorObj = error as Error
// Update with error
this.handleUpdate(key, {
error: errorObj,
isLoading: false,
isValidating: false
})
config.onError?.(errorObj, key)
}
}
/**
* Setup global event handlers
*/
private setupGlobalHandlers(): void {
// This will be handled by the RevalidationManager
// which has its own global event setup
}
/**
* Cleanup resources
*/
destroy(): void {
this.clear()
this.cache.destroy?.()
}
}
/**
* Create a new SWR instance
*/
export function createSWR<Data = unknown, Error = unknown>(
config?: Partial<SWRConfig<Data, Error>>
): SWRCore<Data, Error> {
return new SWRCoreImpl<Data, Error>(config)
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Core types for the SWR implementation
*/
export type Key = string | string[] | Record<string, unknown> | null | undefined
export interface CacheEntry<Data = unknown, Error = unknown> {
data?: Data
error?: Error
isValidating: boolean
isLoading: boolean
lastModified: number
ttl?: number
}
export interface CacheOptions {
ttl?: number
revalidateOnMount?: boolean
revalidateOnFocus?: boolean
revalidateOnReconnect?: boolean
refreshInterval?: number
dedupingInterval?: number
errorRetryCount?: number
errorRetryInterval?: number
loadingTimeout?: number
}
export interface FetcherOptions {
signal: AbortSignal
}
export type Fetcher<Data = unknown> = (key: Key, options: FetcherOptions) => Promise<Data>
// Support for simpler fetcher signatures like SWR
type FetcherResponse<Data = unknown> = Data | Promise<Data>;
export type BareFetcher<Data = unknown> = (...args: any[]) => FetcherResponse<Data>;
export type SWRFetcher<Data = unknown> = BareFetcher<Data> | Fetcher<Data>
export interface MutatorOptions<Data = unknown> {
revalidate?: boolean
optimisticData?: Data
populateCache?: boolean
rollbackOnError?: boolean
}
export type MutatorCallback<Data = unknown> = (currentData?: Data) => Data | Promise<Data>
export type MutatorValue<Data = unknown> = Data | MutatorCallback<Data> | undefined
export interface RevalidateOptions<Data = unknown> {
dedupe?: boolean
retryCount?: number
fetcher?: Fetcher<Data>
}
export type Callback<Data = unknown, Error = unknown> = (
data?: Data,
error?: Error,
isValidating?: boolean,
isLoading?: boolean
) => void
export type Unsubscribe = () => void
export interface SWRConfig<Data = unknown, Error = unknown> extends CacheOptions {
fetcher?: Fetcher<Data>
onSuccess?: (data: Data, key: Key) => void
onError?: (error: Error, key: Key) => void
onErrorRetry?: (error: Error, key: Key, config: SWRConfig<Data, Error>, revalidate: () => void, options: { retryCount: number }) => void
onLoadingSlow?: (key: Key, config: SWRConfig<Data, Error>) => void
isPaused?: () => boolean
compare?: (a: Data | undefined, b: Data | undefined) => boolean
fallback?: Record<string, Data>
}
export interface Cache<Data = unknown, Error = unknown> {
get(key: Key): CacheEntry<Data, Error> | undefined
set(key: Key, value: CacheEntry<Data, Error>): void
delete(key: Key): void
clear(): void
keys(): Key[]
has(key: Key): boolean
}
export interface EventManager {
on(event: string, callback: Callback<unknown, unknown>): Unsubscribe
emit(event: string, ...args: unknown[]): void
off(event: string, callback: Callback<unknown, unknown>): void
}
export interface SWRCore<Data = unknown, Error = unknown> {
get(key: Key): CacheEntry<Data, Error> | undefined
subscribe(key: Key, callback: Callback<Data, Error>, fetcher?: Fetcher<Data>): Unsubscribe
/**
* Subscribe to state changes for a key WITHOUT triggering any fetch.
* Useful for consumers (like React hooks) that manage fetching themselves
* but still want to be notified when cached data updates.
*/
addEventListener(key: Key, callback: Callback<Data, Error>): Unsubscribe
/**
* Set the loading+validating state for a key without fetching.
* Useful for consumers that want to surface loading state immediately
* before initiating their own fetch.
*/
setLoading(key: Key): void
mutate(key: Key, data?: MutatorValue<Data>, options?: MutatorOptions<Data>): Promise<Data | undefined>
revalidate(key: Key, options?: RevalidateOptions<Data>): Promise<boolean>
delete(key: Key): void
clear(): void
updateConfig(config: Partial<SWRConfig<Data, Error>>): void
}
+39
View File
@@ -0,0 +1,39 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": [
"DOM",
"ES2022"
],
"target": "ES2018",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "preserve",
"allowJs": true,
"types": [
"bun"
],
// Bundler mode
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}