Skip to content

Commit 2e292b1

Browse files
cache observer
1 parent a2bff0c commit 2e292b1

File tree

10 files changed

+257
-42
lines changed

10 files changed

+257
-42
lines changed

packages/svelte/src/internal/client/reactivity/cache.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { BaseCacheObserver } from '../../shared/cache-observer.js';
2+
import { ObservableCache } from '../../shared/observable-cache.js';
23
import { set_hydratable_key } from '../context.js';
34
import { tick } from '../runtime.js';
45
import { render_effect } from './effects.js';
56

67
/** @typedef {{ count: number, item: any }} Entry */
7-
/** @type {Map<string, Entry>} */
8-
const client_cache = new Map();
8+
/** @type {ObservableCache} */
9+
const client_cache = new ObservableCache();
910

1011
/**
1112
* @template {(...args: any[]) => any} TFn
@@ -68,8 +69,12 @@ function create_remover(key) {
6869
});
6970
}
7071

71-
// export class CacheObserver extends BaseCacheObserver {
72-
// constructor() {
73-
// super(client_cache);
74-
// }
75-
// }
72+
/**
73+
* @template T
74+
* @extends BaseCacheObserver<T>
75+
*/
76+
export class CacheObserver extends BaseCacheObserver {
77+
constructor(prefix = '') {
78+
super(() => client_cache, prefix);
79+
}
80+
}

packages/svelte/src/internal/server/reactivity/cache.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { set_hydratable_key } from '../hydratable';
2-
import { get_render_context } from '../render-context';
1+
import { BaseCacheObserver } from '../../shared/cache-observer.js';
2+
import { set_hydratable_key } from '../hydratable.js';
3+
import { get_render_context } from '../render-context.js';
34

45
/**
56
* @template {(...args: any[]) => any} TFn
@@ -20,9 +21,12 @@ export function cache(key, fn) {
2021
return new_entry;
2122
}
2223

23-
// TODO, has to be async
24-
// export class CacheObserver extends BaseCacheObserver {
25-
// constructor() {
26-
// super(get_render_store().cache);
27-
// }
28-
// }
24+
/**
25+
* @template T
26+
* @extends BaseCacheObserver<T>
27+
*/
28+
export class CacheObserver extends BaseCacheObserver {
29+
constructor(prefix = '') {
30+
super(() => get_render_context().cache, prefix);
31+
}
32+
}

packages/svelte/src/internal/server/render-context.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @import { AsyncLocalStorage } from 'node:async_hooks' */
22
/** @import { RenderContext } from '#server' */
33

4+
import { ObservableCache } from '../shared/observable-cache';
45
import { deferred } from '../shared/utils';
56

67
/** @type {Promise<void> | null} */
@@ -63,7 +64,7 @@ export async function with_render_context(fn) {
6364
try {
6465
sync_context = {
6566
hydratables: new Map(),
66-
cache: new Map()
67+
cache: new ObservableCache()
6768
};
6869
if (in_webcontainer()) {
6970
const { promise, resolve } = deferred();

packages/svelte/src/internal/server/types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Transport } from '#shared';
2+
import type { ObservableCache } from '../shared/observable-cache';
23
import type { Element } from './dev';
34
import type { Renderer } from './renderer';
45

@@ -23,7 +24,7 @@ export interface RenderContext {
2324
transport: Transport<any> | undefined;
2425
}
2526
>;
26-
cache: Map<string, unknown>;
27+
cache: ObservableCache;
2728
}
2829

2930
export interface SyncRenderOutput {
Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,112 @@
1-
/** @implements {ReadonlyMap<string, any>} */
1+
/** @import { ObservableCache } from './observable-cache.js' */
2+
3+
/**
4+
* @template T
5+
* @implements {ReadonlyMap<string, T>} */
26
export class BaseCacheObserver {
3-
/** @type {ReadonlyMap<string, any>} */
4-
#cache;
7+
/**
8+
* This is a function so that you can create an ObservableCache instance globally and as long as you don't actually
9+
* use it until you're inside the server render lifecycle you'll be okay
10+
* @type {() => ObservableCache}
11+
*/
12+
#get_cache;
13+
14+
/** @type {string} */
15+
#prefix;
16+
17+
/**
18+
* @param {() => ObservableCache} get_cache
19+
* @param {string} [prefix]
20+
*/
21+
constructor(get_cache, prefix = '') {
22+
this.#get_cache = get_cache;
23+
this.#prefix = prefix;
24+
}
25+
26+
/**
27+
* Register a callback to be called when a new key is inserted
28+
* @param {(key: string, value: T) => void} callback
29+
* @returns {() => void} Function to unregister the callback
30+
*/
31+
onInsert(callback) {
32+
return this.#get_cache().on_insert((key, value) => {
33+
if (!key.startsWith(this.#prefix)) return;
34+
callback(key, value.item);
35+
});
36+
}
37+
38+
/**
39+
* Register a callback to be called when an existing key is updated
40+
* @param {(key: string, value: T, old_value: T) => void} callback
41+
* @returns {() => void} Function to unregister the callback
42+
*/
43+
onUpdate(callback) {
44+
return this.#get_cache().on_update((key, value, old_value) => {
45+
if (!key.startsWith(this.#prefix)) return;
46+
callback(key, value.item, old_value.item);
47+
});
48+
}
549

6-
/** @param {Map<string, any>} cache */
7-
constructor(cache) {
8-
this.#cache = cache;
50+
/**
51+
* Register a callback to be called when a key is deleted
52+
* @param {(key: string, old_value: T) => void} callback
53+
* @returns {() => void} Function to unregister the callback
54+
*/
55+
onDelete(callback) {
56+
return this.#get_cache().on_delete((key, old_value) => {
57+
if (!key.startsWith(this.#prefix)) return;
58+
callback(key, old_value.item);
59+
});
960
}
1061

11-
/** @type {ReadonlyMap<string, any>['get']} */
62+
/** @param {string} key */
1263
get(key) {
13-
const entry = this.#cache.get(key);
64+
const entry = this.#get_cache().get(this.#key(key));
1465
return entry?.item;
1566
}
1667

17-
/** @type {ReadonlyMap<string, any>['has']} */
68+
/** @param {string} key */
1869
has(key) {
19-
return this.#cache.has(key);
70+
return this.#get_cache().has(this.#key(key));
2071
}
2172

22-
/** @type {ReadonlyMap<string, any>['size']} */
2373
get size() {
24-
return this.#cache.size;
74+
return [...this.keys()].length;
2575
}
2676

27-
/** @type {ReadonlyMap<string, any>['forEach']} */
77+
/** @param {(item: T, key: string, map: ReadonlyMap<string, T>) => void} cb */
2878
forEach(cb) {
29-
this.#cache.forEach((entry, key) => cb(entry.item, key, this));
79+
this.entries().forEach(([key, entry]) => cb(entry, key, this));
3080
}
3181

32-
/** @type {ReadonlyMap<string, any>['entries']} */
3382
*entries() {
34-
for (const [key, entry] of this.#cache.entries()) {
35-
yield [key, entry.item];
83+
for (const [key, entry] of this.#get_cache().entries()) {
84+
if (!key.startsWith(this.#prefix)) continue;
85+
yield /** @type {[string, T]} */ ([key, entry.item]);
3686
}
87+
return undefined;
3788
}
3889

39-
/** @type {ReadonlyMap<string, any>['keys']} */
4090
*keys() {
41-
for (const key of this.#cache.keys()) {
91+
for (const [key] of this.entries()) {
4292
yield key;
4393
}
94+
return undefined;
4495
}
4596

46-
/** @type {ReadonlyMap<string, any>['values']} */
4797
*values() {
48-
for (const entry of this.#cache.values()) {
49-
yield entry.item;
98+
for (const [, entry] of this.entries()) {
99+
yield entry;
50100
}
101+
return undefined;
51102
}
52103

53104
[Symbol.iterator]() {
54105
return this.entries();
55106
}
107+
108+
/** @param {string} key */
109+
#key(key) {
110+
return this.#prefix + key;
111+
}
56112
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/** @import { CacheEntry } from '#shared' */
2+
3+
/**
4+
* @extends {Map<string, CacheEntry>}
5+
*/
6+
export class ObservableCache extends Map {
7+
/** @type {Set<(key: string, value: CacheEntry) => void>} */
8+
#insert_callbacks = new Set();
9+
10+
/** @type {Set<(key: string, value: CacheEntry, old_value: CacheEntry) => void>} */
11+
#update_callbacks = new Set();
12+
13+
/** @type {Set<(key: string, old_value: CacheEntry) => void>} */
14+
#delete_callbacks = new Set();
15+
16+
/**
17+
* @param {(key: string, value: CacheEntry) => void} callback
18+
* @returns {() => void} Function to unregister the callback
19+
*/
20+
on_insert(callback) {
21+
this.#insert_callbacks.add(callback);
22+
return () => this.#insert_callbacks.delete(callback);
23+
}
24+
25+
/**
26+
* @param {(key: string, value: CacheEntry, old_value: CacheEntry) => void} callback
27+
* @returns {() => void} Function to unregister the callback
28+
*/
29+
on_update(callback) {
30+
this.#update_callbacks.add(callback);
31+
return () => this.#update_callbacks.delete(callback);
32+
}
33+
34+
/**
35+
* @param {(key: string, old_value: CacheEntry) => void} callback
36+
* @returns {() => void} Function to unregister the callback
37+
*/
38+
on_delete(callback) {
39+
this.#delete_callbacks.add(callback);
40+
return () => this.#delete_callbacks.delete(callback);
41+
}
42+
43+
/**
44+
* @param {string} key
45+
* @param {CacheEntry} value
46+
* @returns {this}
47+
*/
48+
set(key, value) {
49+
const had = this.has(key);
50+
if (had) {
51+
const old_value = /** @type {CacheEntry} */ (super.get(key));
52+
super.set(key, value);
53+
for (const callback of this.#update_callbacks) {
54+
callback(key, value, old_value);
55+
}
56+
} else {
57+
super.set(key, value);
58+
for (const callback of this.#insert_callbacks) {
59+
callback(key, value);
60+
}
61+
}
62+
return this;
63+
}
64+
65+
/**
66+
* @param {string} key
67+
* @returns {boolean}
68+
*/
69+
delete(key) {
70+
const old_value = super.get(key);
71+
const deleted = super.delete(key);
72+
if (deleted) {
73+
for (const callback of this.#delete_callbacks) {
74+
callback(key, /** @type {CacheEntry} */ (old_value));
75+
}
76+
}
77+
return deleted;
78+
}
79+
80+
clear() {
81+
for (const [key, value] of this) {
82+
for (const callback of this.#delete_callbacks) {
83+
callback(key, value);
84+
}
85+
}
86+
super.clear();
87+
}
88+
}

packages/svelte/src/internal/shared/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ export type Resource<T> = {
4141
);
4242

4343
export type GetRequestInit = Omit<RequestInit, 'method' | 'body'> & { method?: 'GET' };
44+
45+
export type CacheEntry = { count: number; item: any };

packages/svelte/src/reactivity/index-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export { SvelteURLSearchParams } from './url-search-params.js';
77
export { MediaQuery } from './media-query.js';
88
export { createSubscriber } from './create-subscriber.js';
99
export { resource } from '../internal/client/reactivity/resource.js';
10-
export { cache } from '../internal/client/reactivity/cache.js';
10+
export { cache, CacheObserver } from '../internal/client/reactivity/cache.js';
1111
export { fetcher } from '../internal/client/reactivity/fetcher.js';
1212

1313
/**

packages/svelte/src/reactivity/index-server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/** @import { Resource as ResourceType } from '#shared' */
22
export { resource } from '../internal/server/reactivity/resource.js';
3-
export { cache } from '../internal/server/reactivity/cache.js';
3+
export { cache, CacheObserver } from '../internal/server/reactivity/cache.js';
44
export { fetcher } from '../internal/server/reactivity/fetcher.js';
55

66
export const SvelteDate = globalThis.Date;

0 commit comments

Comments
 (0)