Skip to content

Commit 30d7263

Browse files
committed
initial work on next moize superset
1 parent 58287c7 commit 30d7263

File tree

13 files changed

+811
-6
lines changed

13 files changed

+811
-6
lines changed

DEV_ONLY/moize-next/Cache.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Cache as BaseCache, CacheEvent, CacheEventReason, CacheNode, Key, KeyTransformer } from '../../src';
2+
import type { NormalizedOptions, Options, StatsProfile } from './internalTypes';
3+
import { defaultArgumentSerializer, isSerializedKeyEqual } from './serialize';
4+
import { clearStats, getStats } from './stats';
5+
import { cloneKey, compose, getMaxArgsTransformKey } from './utils';
6+
7+
export interface ExpirationManager {
8+
a: boolean;
9+
c: () => void;
10+
e: () => void;
11+
i: ReturnType<typeof setTimeout> | null;
12+
s: () => void;
13+
t: number;
14+
u: (newExpiration?: number) => void;
15+
}
16+
17+
const expiringNodes = new Map<CacheNode<any>, ExpirationManager>();
18+
19+
export class Cache<Fn extends (...args: any[]) => any> extends BaseCache<Fn> {
20+
pn: string;
21+
s: boolean = false;
22+
x?: number;
23+
xo?: Options<Fn>['onExpire'];
24+
xr?: Options<Fn>['rescheduleExpiration'];
25+
26+
constructor(options: NormalizedOptions<Fn>) {
27+
super(options);
28+
29+
this.pn = options.profileName;
30+
31+
const { expiresAfter, maxArgs, serialize, transformKey } = options;
32+
33+
const keyTransformers: KeyTransformer<Fn>[] = [];
34+
35+
if (serialize) {
36+
if (!options.isKeyEqual) {
37+
this.m = isSerializedKeyEqual;
38+
}
39+
40+
keyTransformers.push(
41+
typeof serialize === 'function'
42+
? (args: IArguments | Key) => serialize(cloneKey<Fn>(args as Parameters<Fn>))
43+
: defaultArgumentSerializer,
44+
);
45+
}
46+
47+
if (transformKey) {
48+
keyTransformers.push(transformKey);
49+
}
50+
51+
if (maxArgs && maxArgs >= 0) {
52+
keyTransformers.push(getMaxArgsTransformKey(maxArgs));
53+
}
54+
55+
if (keyTransformers.length) {
56+
// @ts-expect-error - ignore for now
57+
this.k = compose(...keyTransformers);
58+
}
59+
60+
if (typeof expiresAfter === 'number' && !isNaN(expiresAfter) && expiresAfter > 0 && expiresAfter < Infinity) {
61+
this.x = expiresAfter;
62+
this.xo = options.onExpire;
63+
this.xr = options.rescheduleExpiration;
64+
65+
if (this.xr) {
66+
this.xu = this.xu.bind(this);
67+
68+
this.on('hit', this.xu);
69+
this.on('update', this.xu);
70+
}
71+
72+
this.clear = this.xc;
73+
this.d = this.xd;
74+
this.n = this.xn.bind(this, expiresAfter);
75+
}
76+
}
77+
78+
clearStats(): void {
79+
clearStats(this.pn);
80+
}
81+
82+
getStats(): StatsProfile {
83+
return getStats(this.pn);
84+
}
85+
86+
keys(): Key[] {
87+
return this.entries().map((entry) => entry[0]);
88+
}
89+
90+
values(): Array<ReturnType<Fn>> {
91+
return this.entries().map((entry) => entry[1]);
92+
}
93+
94+
xc() {
95+
expiringNodes.forEach((expiringMananger) => {
96+
expiringMananger.c();
97+
});
98+
99+
super.clear();
100+
}
101+
102+
xd(node: CacheNode<Fn>): void {
103+
super.d(node);
104+
105+
const expirationManager = expiringNodes.get(node);
106+
107+
if (expirationManager) {
108+
expirationManager.c();
109+
expiringNodes.delete(node);
110+
}
111+
}
112+
113+
xm(node: CacheNode<Fn>, expiration: number): ExpirationManager {
114+
// eslint-disable-next-line @typescript-eslint/no-this-alias
115+
const cache = this;
116+
117+
return {
118+
a: false,
119+
i: null,
120+
t: expiration,
121+
122+
c() {
123+
if (this.i) {
124+
clearTimeout(this.i);
125+
this.i = null;
126+
}
127+
},
128+
129+
e() {
130+
if (!this.a) {
131+
return;
132+
}
133+
134+
this.c();
135+
expiringNodes.delete(node);
136+
137+
this.a = false;
138+
139+
if (cache.xo) {
140+
const result = cache.xo([node.k, node.v]);
141+
142+
if (result === false) {
143+
return;
144+
}
145+
}
146+
147+
cache.d(node);
148+
cache.od && cache.od.n(node, 'expired' as unknown as CacheEventReason);
149+
},
150+
151+
s() {
152+
this.a = true;
153+
154+
expiringNodes.set(node, this);
155+
156+
this.i = setTimeout(() => {
157+
this.e();
158+
this.i = null;
159+
}, this.t);
160+
},
161+
162+
u(this: ExpirationManager, newExpiration: number = this.t) {
163+
this.a && this.c();
164+
165+
this.t = newExpiration;
166+
this.s();
167+
}
168+
169+
}
170+
}
171+
172+
xn(expiration: number, key: Key, value: any) {
173+
const node = super.n(key, value);
174+
175+
const expirationManager = this.xm(node, expiration);
176+
177+
expiringNodes.set(node, expirationManager);
178+
expirationManager.s();
179+
180+
return node;
181+
}
182+
183+
xu(event: CacheEvent<'hit' | 'update', Fn>): void {
184+
if (typeof this.xr === 'function' && !this.xr(event)) {
185+
return;
186+
}
187+
188+
const key = event.entry[0];
189+
const node = this.g(key);
190+
const expirationManager = node && expiringNodes.get(node);
191+
192+
if (!expirationManager) {
193+
return;
194+
}
195+
196+
expirationManager.u();
197+
}
198+
}

DEV_ONLY/moize-next/environment.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'core-js';
2+
import 'regenerator-runtime/runtime';
3+
// import moize from './moize';
4+
import type { Key, Moized, Options } from './internalTypes';
5+
6+
// moize.collectStats();
7+
8+
export function log(message: string, key: Key, value: any) {
9+
console.log(`result (${message})`, key, value);
10+
}
11+
12+
export function logCache<Fn extends (...args: any[]) => any>(memoized: Moized<Fn, Options<Fn>>) {
13+
console.log('cache', memoized.cache.entries());
14+
}
15+
16+
export function logStoredValue<Fn extends (...args: any[]) => any>(
17+
memoized: Moized<Fn, Options<Fn>>,
18+
message: number | string,
19+
key: Parameters<Fn>
20+
) {
21+
console.log(`result (${message})`, key, memoized.cache.get(key));
22+
}
23+
24+
export function createContainer() {
25+
const div = document.createElement('div');
26+
27+
div.textContent = 'Check the console for details.';
28+
29+
div.id = 'app-container';
30+
div.style.backgroundColor = '#1d1d1d';
31+
div.style.boxSizing = 'border-box';
32+
div.style.color = '#d5d5d5';
33+
div.style.height = '100vh';
34+
div.style.padding = '15px';
35+
div.style.width = '100vw';
36+
37+
document.body.style.margin = '0px';
38+
document.body.style.padding = '0px';
39+
40+
document.body.appendChild(div);
41+
42+
return div;
43+
}

DEV_ONLY/moize-next/index.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import moize from './moize';
2+
3+
document.body.style.backgroundColor = '#1d1d1d';
4+
document.body.style.color = '#d5d5d5';
5+
document.body.style.margin = '0px';
6+
document.body.style.padding = '0px';
7+
8+
const moized = moize((one: string, two: string, three) => {
9+
console.log('called');
10+
11+
return ({ one, two, three });
12+
}, {
13+
expiresAfter: 2000,
14+
maxArgs: 2,
15+
maxSize: 2,
16+
onExpire(entry) {
17+
console.log('expired', entry[0]);
18+
},
19+
rescheduleExpiration: true,
20+
serialize: true,
21+
transformKey(key) {
22+
console.log({ key})
23+
return [...key].reverse();
24+
}
25+
});
26+
27+
moized.cache.on('delete', (event) => {
28+
console.log('deleted', event.cache.oh);
29+
})
30+
31+
console.log(moized('one', 'two', 'three'));
32+
console.log(moized('one', 'three', 'two'));
33+
console.log(moized('two', 'three', 'one'));
34+
console.log(moized('one', 'two', 'three'));
35+
console.log(moized('one', 'two', 'three'));
36+
37+
console.log(moized.cache.oh);
38+
console.log(moized.cache.entries());
39+
40+
moized.cache.delete(['two', 'three', 'bunk']);
41+
42+
console.log(moized.cache.entries())

DEV_ONLY/moize-next/internalTypes.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
import type {
3+
CacheEntry,
4+
CacheEvent,
5+
Key,
6+
OptionsArgEqual as BaseOptionsArgEqual,
7+
OptionsKeyEqual as BaseOptionsKeyEqual,
8+
} from '../../src';
9+
import type { Cache } from './Cache';
10+
11+
export type * from '../../src';
12+
13+
interface OptionsArgEqual<Fn extends (...args: any[]) => any> extends Omit<BaseOptionsArgEqual<Fn>, 'isArgEqual'> {
14+
isArgEqual?: 'deep' | 'shallow' | BaseOptionsArgEqual<Fn>['isArgEqual'];
15+
}
16+
17+
type BaseOptions<Fn extends (...args: any[]) => any> = OptionsArgEqual<Fn> & BaseOptionsKeyEqual<Fn>;
18+
19+
export type Options<Fn extends (...args: any[]) => any> = BaseOptions<Fn> & {
20+
expiresAfter?: number;
21+
/**
22+
* Whether the function is a React component.
23+
*/
24+
maxArgs?: number;
25+
onExpire?: (entry: CacheEntry<Fn>) => any;
26+
profileName?: string;
27+
rescheduleExpiration?: boolean | ((event: CacheEvent<'hit' | 'update', Fn>) => boolean);
28+
serialize?: boolean | ((key: Key) => [string]);
29+
}
30+
31+
export type NormalizedOptions<Fn extends (...args: any[]) => any> = Options<Fn> & {
32+
isArgEqual: BaseOptions<Fn>['isArgEqual'];
33+
profileName: string;
34+
};
35+
36+
export interface Moized<
37+
Fn extends (...args: any[]) => any,
38+
Opts extends Options<Fn>,
39+
> {
40+
(...args: Parameters<Fn>): ReturnType<Fn>;
41+
42+
/**
43+
* The cache used for the memoized method.
44+
*/
45+
cache: Cache<Fn>;
46+
/**
47+
* The original method that is memoized.
48+
*/
49+
fn: Fn;
50+
/**
51+
* Simple identifier that the function has been memoized.
52+
*/
53+
isMoized: true;
54+
/**
55+
* Options passed for the memoized method.
56+
*/
57+
options: Opts;
58+
}
59+
60+
export interface Moize {
61+
<
62+
Fn extends Moized<
63+
(...args: any[]) => any,
64+
Options<(...args: any[]) => any>
65+
>,
66+
Opts extends Options<Fn['fn']>,
67+
>(
68+
fn: Fn,
69+
passedOptions: Opts,
70+
): Moized<Fn['fn'], Fn['options'] & Opts>;
71+
<
72+
Fn extends Moized<
73+
(...args: any[]) => any,
74+
Options<(...args: any[]) => any>
75+
>,
76+
>(
77+
fn: Fn,
78+
): Moized<Fn, Fn['options']>;
79+
<Fn extends (...args: any[]) => any, Opts extends Options<Fn>>(
80+
fn: Fn,
81+
passedOptions: Opts,
82+
): Moized<Fn, Opts>;
83+
// eslint-disable-next-line @typescript-eslint/ban-types
84+
<Fn extends (...args: any[]) => any>(fn: Fn): Moized<Fn, {}>;
85+
86+
isMoized: (fn: any) => fn is Moized<any, any>;
87+
}
88+
89+
export interface StatsCache {
90+
anonymousProfileNameCounter: number;
91+
isCollectingStats: boolean;
92+
profiles: Record<string, StatsProfile>;
93+
}
94+
95+
export interface StatsObject {
96+
calls: number;
97+
hits: number;
98+
usage: string;
99+
}
100+
101+
export interface GlobalStatsObject extends StatsObject {
102+
profiles?: Record<string, StatsProfile>;
103+
}
104+
105+
export interface StatsProfile {
106+
calls: number;
107+
hits: number;
108+
}

0 commit comments

Comments
 (0)