cache observer

pull/17124/head
Elliott Johnson 3 weeks ago
parent a2bff0c0b5
commit 2e292b1a6d

@ -1,11 +1,12 @@
import { BaseCacheObserver } from '../../shared/cache-observer.js';
import { ObservableCache } from '../../shared/observable-cache.js';
import { set_hydratable_key } from '../context.js';
import { tick } from '../runtime.js';
import { render_effect } from './effects.js';
/** @typedef {{ count: number, item: any }} Entry */
/** @type {Map<string, Entry>} */
const client_cache = new Map();
/** @type {ObservableCache} */
const client_cache = new ObservableCache();
/**
* @template {(...args: any[]) => any} TFn
@ -68,8 +69,12 @@ function create_remover(key) {
});
}
// export class CacheObserver extends BaseCacheObserver {
// constructor() {
// super(client_cache);
// }
// }
/**
* @template T
* @extends BaseCacheObserver<T>
*/
export class CacheObserver extends BaseCacheObserver {
constructor(prefix = '') {
super(() => client_cache, prefix);
}
}

@ -1,5 +1,6 @@
import { set_hydratable_key } from '../hydratable';
import { get_render_context } from '../render-context';
import { BaseCacheObserver } from '../../shared/cache-observer.js';
import { set_hydratable_key } from '../hydratable.js';
import { get_render_context } from '../render-context.js';
/**
* @template {(...args: any[]) => any} TFn
@ -20,9 +21,12 @@ export function cache(key, fn) {
return new_entry;
}
// TODO, has to be async
// export class CacheObserver extends BaseCacheObserver {
// constructor() {
// super(get_render_store().cache);
// }
// }
/**
* @template T
* @extends BaseCacheObserver<T>
*/
export class CacheObserver extends BaseCacheObserver {
constructor(prefix = '') {
super(() => get_render_context().cache, prefix);
}
}

@ -1,6 +1,7 @@
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
/** @import { RenderContext } from '#server' */
import { ObservableCache } from '../shared/observable-cache';
import { deferred } from '../shared/utils';
/** @type {Promise<void> | null} */
@ -63,7 +64,7 @@ export async function with_render_context(fn) {
try {
sync_context = {
hydratables: new Map(),
cache: new Map()
cache: new ObservableCache()
};
if (in_webcontainer()) {
const { promise, resolve } = deferred();

@ -1,4 +1,5 @@
import type { Transport } from '#shared';
import type { ObservableCache } from '../shared/observable-cache';
import type { Element } from './dev';
import type { Renderer } from './renderer';
@ -23,7 +24,7 @@ export interface RenderContext {
transport: Transport<any> | undefined;
}
>;
cache: Map<string, unknown>;
cache: ObservableCache;
}
export interface SyncRenderOutput {

@ -1,56 +1,112 @@
/** @implements {ReadonlyMap<string, any>} */
/** @import { ObservableCache } from './observable-cache.js' */
/**
* @template T
* @implements {ReadonlyMap<string, T>} */
export class BaseCacheObserver {
/** @type {ReadonlyMap<string, any>} */
#cache;
/**
* This is a function so that you can create an ObservableCache instance globally and as long as you don't actually
* use it until you're inside the server render lifecycle you'll be okay
* @type {() => ObservableCache}
*/
#get_cache;
/** @type {string} */
#prefix;
/**
* @param {() => ObservableCache} get_cache
* @param {string} [prefix]
*/
constructor(get_cache, prefix = '') {
this.#get_cache = get_cache;
this.#prefix = prefix;
}
/**
* Register a callback to be called when a new key is inserted
* @param {(key: string, value: T) => void} callback
* @returns {() => void} Function to unregister the callback
*/
onInsert(callback) {
return this.#get_cache().on_insert((key, value) => {
if (!key.startsWith(this.#prefix)) return;
callback(key, value.item);
});
}
/**
* Register a callback to be called when an existing key is updated
* @param {(key: string, value: T, old_value: T) => void} callback
* @returns {() => void} Function to unregister the callback
*/
onUpdate(callback) {
return this.#get_cache().on_update((key, value, old_value) => {
if (!key.startsWith(this.#prefix)) return;
callback(key, value.item, old_value.item);
});
}
/** @param {Map<string, any>} cache */
constructor(cache) {
this.#cache = cache;
/**
* Register a callback to be called when a key is deleted
* @param {(key: string, old_value: T) => void} callback
* @returns {() => void} Function to unregister the callback
*/
onDelete(callback) {
return this.#get_cache().on_delete((key, old_value) => {
if (!key.startsWith(this.#prefix)) return;
callback(key, old_value.item);
});
}
/** @type {ReadonlyMap<string, any>['get']} */
/** @param {string} key */
get(key) {
const entry = this.#cache.get(key);
const entry = this.#get_cache().get(this.#key(key));
return entry?.item;
}
/** @type {ReadonlyMap<string, any>['has']} */
/** @param {string} key */
has(key) {
return this.#cache.has(key);
return this.#get_cache().has(this.#key(key));
}
/** @type {ReadonlyMap<string, any>['size']} */
get size() {
return this.#cache.size;
return [...this.keys()].length;
}
/** @type {ReadonlyMap<string, any>['forEach']} */
/** @param {(item: T, key: string, map: ReadonlyMap<string, T>) => void} cb */
forEach(cb) {
this.#cache.forEach((entry, key) => cb(entry.item, key, this));
this.entries().forEach(([key, entry]) => cb(entry, key, this));
}
/** @type {ReadonlyMap<string, any>['entries']} */
*entries() {
for (const [key, entry] of this.#cache.entries()) {
yield [key, entry.item];
for (const [key, entry] of this.#get_cache().entries()) {
if (!key.startsWith(this.#prefix)) continue;
yield /** @type {[string, T]} */ ([key, entry.item]);
}
return undefined;
}
/** @type {ReadonlyMap<string, any>['keys']} */
*keys() {
for (const key of this.#cache.keys()) {
for (const [key] of this.entries()) {
yield key;
}
return undefined;
}
/** @type {ReadonlyMap<string, any>['values']} */
*values() {
for (const entry of this.#cache.values()) {
yield entry.item;
for (const [, entry] of this.entries()) {
yield entry;
}
return undefined;
}
[Symbol.iterator]() {
return this.entries();
}
/** @param {string} key */
#key(key) {
return this.#prefix + key;
}
}

@ -0,0 +1,88 @@
/** @import { CacheEntry } from '#shared' */
/**
* @extends {Map<string, CacheEntry>}
*/
export class ObservableCache extends Map {
/** @type {Set<(key: string, value: CacheEntry) => void>} */
#insert_callbacks = new Set();
/** @type {Set<(key: string, value: CacheEntry, old_value: CacheEntry) => void>} */
#update_callbacks = new Set();
/** @type {Set<(key: string, old_value: CacheEntry) => void>} */
#delete_callbacks = new Set();
/**
* @param {(key: string, value: CacheEntry) => void} callback
* @returns {() => void} Function to unregister the callback
*/
on_insert(callback) {
this.#insert_callbacks.add(callback);
return () => this.#insert_callbacks.delete(callback);
}
/**
* @param {(key: string, value: CacheEntry, old_value: CacheEntry) => void} callback
* @returns {() => void} Function to unregister the callback
*/
on_update(callback) {
this.#update_callbacks.add(callback);
return () => this.#update_callbacks.delete(callback);
}
/**
* @param {(key: string, old_value: CacheEntry) => void} callback
* @returns {() => void} Function to unregister the callback
*/
on_delete(callback) {
this.#delete_callbacks.add(callback);
return () => this.#delete_callbacks.delete(callback);
}
/**
* @param {string} key
* @param {CacheEntry} value
* @returns {this}
*/
set(key, value) {
const had = this.has(key);
if (had) {
const old_value = /** @type {CacheEntry} */ (super.get(key));
super.set(key, value);
for (const callback of this.#update_callbacks) {
callback(key, value, old_value);
}
} else {
super.set(key, value);
for (const callback of this.#insert_callbacks) {
callback(key, value);
}
}
return this;
}
/**
* @param {string} key
* @returns {boolean}
*/
delete(key) {
const old_value = super.get(key);
const deleted = super.delete(key);
if (deleted) {
for (const callback of this.#delete_callbacks) {
callback(key, /** @type {CacheEntry} */ (old_value));
}
}
return deleted;
}
clear() {
for (const [key, value] of this) {
for (const callback of this.#delete_callbacks) {
callback(key, value);
}
}
super.clear();
}
}

@ -41,3 +41,5 @@ export type Resource<T> = {
);
export type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };
export type CacheEntry = { count: number; item: any };

@ -7,7 +7,7 @@ export { SvelteURLSearchParams } from './url-search-params.js';
export { MediaQuery } from './media-query.js';
export { createSubscriber } from './create-subscriber.js';
export { resource } from '../internal/client/reactivity/resource.js';
export { cache } from '../internal/client/reactivity/cache.js';
export { cache, CacheObserver } from '../internal/client/reactivity/cache.js';
export { fetcher } from '../internal/client/reactivity/fetcher.js';
/**

@ -1,6 +1,6 @@
/** @import { Resource as ResourceType } from '#shared' */
export { resource } from '../internal/server/reactivity/resource.js';
export { cache } from '../internal/server/reactivity/cache.js';
export { cache, CacheObserver } from '../internal/server/reactivity/cache.js';
export { fetcher } from '../internal/server/reactivity/fetcher.js';
export const SvelteDate = globalThis.Date;

@ -2422,7 +2422,6 @@ declare module 'svelte/reactivity' {
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
export function resource<T>(fn: () => Promise<T>): Resource_1<T>;
export function cache<TFn extends (...args: any[]) => any>(key: string, fn: TFn): ReturnType<TFn>;
export function fetcher<TReturn>(url: string | URL, init?: GetRequestInit | undefined): Resource_1<TReturn>;
type Resource_1<T> = {
then: Promise<T>['then'];
@ -2444,12 +2443,71 @@ declare module 'svelte/reactivity' {
);
type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };
type CacheEntry = { count: number; item: any };
export function cache<TFn extends (...args: any[]) => any>(key: string, fn: TFn): ReturnType<TFn>;
export class CacheObserver<T> extends BaseCacheObserver<T> {
constructor(prefix?: string);
}
class ReactiveValue<T> {
constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
}
class BaseCacheObserver<T> implements ReadonlyMap<string, T> {
constructor(get_cache: () => ObservableCache, prefix?: string | undefined);
/**
* Register a callback to be called when a new key is inserted
* @returns Function to unregister the callback
*/
onInsert(callback: (key: string, value: T) => void): () => void;
/**
* Register a callback to be called when an existing key is updated
* @returns Function to unregister the callback
*/
onUpdate(callback: (key: string, value: T, old_value: T) => void): () => void;
/**
* Register a callback to be called when a key is deleted
* @returns Function to unregister the callback
*/
onDelete(callback: (key: string, old_value: T) => void): () => void;
get(key: string): any;
has(key: string): boolean;
get size(): number;
forEach(cb: (item: T, key: string, map: ReadonlyMap<string, T>) => void): void;
entries(): Generator<[string, T], undefined, unknown>;
keys(): Generator<string, undefined, unknown>;
values(): Generator<T, undefined, unknown>;
[Symbol.iterator](): Generator<[string, T], undefined, unknown>;
#private;
}
class ObservableCache extends Map<string, CacheEntry> {
constructor();
constructor(entries?: readonly (readonly [string, CacheEntry])[] | null | undefined);
constructor();
constructor(iterable?: Iterable<readonly [string, CacheEntry]> | null | undefined);
/**
* @returns Function to unregister the callback
*/
on_insert(callback: (key: string, value: CacheEntry) => void): () => void;
/**
* @returns Function to unregister the callback
*/
on_update(callback: (key: string, value: CacheEntry, old_value: CacheEntry) => void): () => void;
/**
* @returns Function to unregister the callback
*/
on_delete(callback: (key: string, old_value: CacheEntry) => void): () => void;
set(key: string, value: CacheEntry): this;
#private;
}
export {};
}

Loading…
Cancel
Save