@atproto-labs/simple-store 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE.txt +7 -0
- package/dist/cached-getter.d.ts +44 -0
- package/dist/cached-getter.d.ts.map +1 -0
- package/dist/cached-getter.js +130 -0
- package/dist/cached-getter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-store.d.ts +16 -0
- package/dist/simple-store.d.ts.map +1 -0
- package/dist/simple-store.js +3 -0
- package/dist/simple-store.js.map +1 -0
- package/package.json +32 -0
- package/src/cached-getter.ts +160 -0
- package/src/index.ts +2 -0
- package/src/simple-store.ts +16 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @atproto-labs/simple-store
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#2482](https://github.com/bluesky-social/atproto/pull/2482) [`a8d6c1123`](https://github.com/bluesky-social/atproto/commit/a8d6c112359f5c4c0cfbe2df63443ed275f2a646) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add OAuth provider capability & support for DPoP signed tokens
|
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Dual MIT/Apache-2.0 License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2024 Bluesky PBC, and Contributors
|
|
4
|
+
|
|
5
|
+
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
|
6
|
+
|
|
7
|
+
Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Awaitable, SimpleStore, Key, Value } from './simple-store.js';
|
|
2
|
+
export type GetCachedOptions = {
|
|
3
|
+
signal?: AbortSignal;
|
|
4
|
+
/**
|
|
5
|
+
* Do not use the cache to get the value. Always get a new value from the
|
|
6
|
+
* getter function.
|
|
7
|
+
*
|
|
8
|
+
* @default false
|
|
9
|
+
*/
|
|
10
|
+
noCache?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* When getting a value from the cache, allow the value to be returned even if
|
|
13
|
+
* it is stale.
|
|
14
|
+
*
|
|
15
|
+
* Has no effect if the `isStale` option was not provided to the CachedGetter.
|
|
16
|
+
*
|
|
17
|
+
* @default true // If the CachedGetter has an isStale option
|
|
18
|
+
* @default false // If no isStale option was provided to the CachedGetter
|
|
19
|
+
*/
|
|
20
|
+
allowStale?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type Getter<K, V> = (key: K, options: undefined | GetCachedOptions, storedValue: undefined | V) => Awaitable<V>;
|
|
23
|
+
export type CachedGetterOptions<K, V> = {
|
|
24
|
+
isStale?: (key: K, value: V) => boolean | PromiseLike<boolean>;
|
|
25
|
+
onStoreError?: (err: unknown, key: K, value: V) => void | PromiseLike<void>;
|
|
26
|
+
deleteOnError?: (err: unknown, key: K, value: V) => boolean | PromiseLike<boolean>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Wrapper utility that uses a store to speed up the retrieval of values from an
|
|
30
|
+
* (expensive) getter function.
|
|
31
|
+
*/
|
|
32
|
+
export declare class CachedGetter<K extends Key = string, V extends Value = Value> {
|
|
33
|
+
readonly getter: Getter<K, V>;
|
|
34
|
+
readonly store: SimpleStore<K, V>;
|
|
35
|
+
readonly options?: Readonly<CachedGetterOptions<K, V>> | undefined;
|
|
36
|
+
private pending;
|
|
37
|
+
constructor(getter: Getter<K, V>, store: SimpleStore<K, V>, options?: Readonly<CachedGetterOptions<K, V>> | undefined);
|
|
38
|
+
get(key: K, options?: GetCachedOptions): Promise<V>;
|
|
39
|
+
bind(key: K): (options?: GetCachedOptions) => Promise<V>;
|
|
40
|
+
getStored(key: K, options?: GetCachedOptions): Promise<V | undefined>;
|
|
41
|
+
setStored(key: K, value: V): Promise<void>;
|
|
42
|
+
delStored(key: K): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=cached-getter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cached-getter.d.ts","sourceRoot":"","sources":["../src/cached-getter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEtE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAA;IAEpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,CACzB,GAAG,EAAE,CAAC,EACN,OAAO,EAAE,SAAS,GAAG,gBAAgB,EACrC,WAAW,EAAE,SAAS,GAAG,CAAC,KACvB,SAAS,CAAC,CAAC,CAAC,CAAA;AAEjB,MAAM,MAAM,mBAAmB,CAAC,CAAC,EAAE,CAAC,IAAI;IACtC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;IAC9D,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAA;IAC3E,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,KACL,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;CACpC,CAAA;AAOD;;;GAGG;AACH,qBAAa,YAAY,CAAC,CAAC,SAAS,GAAG,GAAG,MAAM,EAAE,CAAC,SAAS,KAAK,GAAG,KAAK;IAIrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC;IALnB,OAAO,CAAC,OAAO,CAA+B;gBAGnC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,EACpB,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EACxB,OAAO,CAAC,iDAAqC;IAGlD,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,CAAC;IA6EzD,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC;IAIlD,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAQrE,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ1C,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAGvC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CachedGetter = void 0;
|
|
4
|
+
const returnTrue = () => true;
|
|
5
|
+
const returnFalse = () => false;
|
|
6
|
+
/**
|
|
7
|
+
* Wrapper utility that uses a store to speed up the retrieval of values from an
|
|
8
|
+
* (expensive) getter function.
|
|
9
|
+
*/
|
|
10
|
+
class CachedGetter {
|
|
11
|
+
constructor(getter, store, options) {
|
|
12
|
+
Object.defineProperty(this, "getter", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: getter
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "store", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: store
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "options", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: options
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "pending", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: new Map()
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async get(key, options) {
|
|
38
|
+
options?.signal?.throwIfAborted();
|
|
39
|
+
const isStale = this.options?.isStale;
|
|
40
|
+
const allowStored = options?.noCache
|
|
41
|
+
? returnFalse // Never allow stored values to be returned
|
|
42
|
+
: options?.allowStale || isStale == null
|
|
43
|
+
? returnTrue // Always allow stored values to be returned
|
|
44
|
+
: async (value) => !(await isStale(key, value));
|
|
45
|
+
// As long as concurrent requests are made for the same key, only one
|
|
46
|
+
// request will be made to the cache & getter function at a time. This works
|
|
47
|
+
// because there is no async operation between the while() loop and the
|
|
48
|
+
// pending.set() call. Because of the "single threaded" nature of
|
|
49
|
+
// JavaScript, the pending item will be set before the next iteration of the
|
|
50
|
+
// while loop.
|
|
51
|
+
let previousExecutionFlow;
|
|
52
|
+
while ((previousExecutionFlow = this.pending.get(key))) {
|
|
53
|
+
try {
|
|
54
|
+
const { isFresh, value } = await previousExecutionFlow;
|
|
55
|
+
if (isFresh)
|
|
56
|
+
return value;
|
|
57
|
+
if (await allowStored(value))
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore errors from previous execution flows (they will have been
|
|
62
|
+
// propagated by that flow).
|
|
63
|
+
}
|
|
64
|
+
options?.signal?.throwIfAborted();
|
|
65
|
+
}
|
|
66
|
+
const currentExecutionFlow = Promise.resolve()
|
|
67
|
+
.then(async () => {
|
|
68
|
+
const storedValue = await this.getStored(key, options);
|
|
69
|
+
if (storedValue !== undefined && (await allowStored(storedValue))) {
|
|
70
|
+
// Use the stored value as return value for the current execution
|
|
71
|
+
// flow. Notify other concurrent execution flows (that should be
|
|
72
|
+
// "stuck" in the loop before until this promise resolves) that we got
|
|
73
|
+
// a value, but that it came from the store (isFresh = false).
|
|
74
|
+
return { isFresh: false, value: storedValue };
|
|
75
|
+
}
|
|
76
|
+
return Promise.resolve()
|
|
77
|
+
.then(async () => (0, this.getter)(key, options, storedValue))
|
|
78
|
+
.catch(async (err) => {
|
|
79
|
+
if (storedValue !== undefined) {
|
|
80
|
+
if (await this.options?.deleteOnError?.(err, key, storedValue)) {
|
|
81
|
+
await this.delStored(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
})
|
|
86
|
+
.then(async (value) => {
|
|
87
|
+
// The value should be stored even is the signal was aborted.
|
|
88
|
+
await this.setStored(key, value);
|
|
89
|
+
return { isFresh: true, value };
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
.finally(() => {
|
|
93
|
+
this.pending.delete(key);
|
|
94
|
+
});
|
|
95
|
+
if (this.pending.has(key)) {
|
|
96
|
+
// This should never happen. Indeed, there must not be any 'await'
|
|
97
|
+
// statement between this and the loop iteration check meaning that
|
|
98
|
+
// this.pending.get returned undefined. It is there to catch bugs that
|
|
99
|
+
// would occur in future changes to the code.
|
|
100
|
+
throw new Error('Concurrent request for the same key');
|
|
101
|
+
}
|
|
102
|
+
this.pending.set(key, currentExecutionFlow);
|
|
103
|
+
const { value } = await currentExecutionFlow;
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
bind(key) {
|
|
107
|
+
return async (options) => this.get(key, options);
|
|
108
|
+
}
|
|
109
|
+
async getStored(key, options) {
|
|
110
|
+
try {
|
|
111
|
+
return await this.store.get(key, options);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async setStored(key, value) {
|
|
118
|
+
try {
|
|
119
|
+
await this.store.set(key, value);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
await this.options?.onStoreError?.(err, key, value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async delStored(key) {
|
|
126
|
+
await this.store.del(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.CachedGetter = CachedGetter;
|
|
130
|
+
//# sourceMappingURL=cached-getter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cached-getter.js","sourceRoot":"","sources":["../src/cached-getter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./cached-getter.js"), exports);
|
|
18
|
+
__exportStar(require("./simple-store.js"), exports);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qDAAkC;AAClC,oDAAiC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Awaitable<V> = V | PromiseLike<V>;
|
|
2
|
+
export type Key = string | number;
|
|
3
|
+
export type Value = NonNullable<unknown> | null;
|
|
4
|
+
export type GetOptions = {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
};
|
|
7
|
+
export interface SimpleStore<K extends Key = string, V extends Value = Value> {
|
|
8
|
+
/**
|
|
9
|
+
* @return undefined if the key is not in the store (which is why Value cannot contain "undefined").
|
|
10
|
+
*/
|
|
11
|
+
get: (key: K, options?: GetOptions) => Awaitable<undefined | V>;
|
|
12
|
+
set: (key: K, value: V) => Awaitable<void>;
|
|
13
|
+
del: (key: K) => Awaitable<void>;
|
|
14
|
+
clear?: () => Awaitable<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=simple-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simple-store.d.ts","sourceRoot":"","sources":["../src/simple-store.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;AAE7C,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,MAAM,CAAA;AACjC,MAAM,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;AAE/C,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,CAAA;AAEjD,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,GAAG,GAAG,MAAM,EAAE,CAAC,SAAS,KAAK,GAAG,KAAK;IAC1E;;OAEG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC,CAAA;IAC/D,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,CAAA;IAC1C,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,CAAA;IAChC,KAAK,CAAC,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,CAAA;CAC9B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simple-store.js","sourceRoot":"","sources":["../src/simple-store.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto-labs/simple-store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Simple store interfaces & utilities",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cache",
|
|
8
|
+
"isomorphic"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://atproto.com",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
14
|
+
"directory": "packages/internal/simple-store"
|
|
15
|
+
},
|
|
16
|
+
"type": "commonjs",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.3.3"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc --build tsconfig.build.json"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Awaitable, SimpleStore, Key, Value } from './simple-store.js'
|
|
2
|
+
|
|
3
|
+
export type GetCachedOptions = {
|
|
4
|
+
signal?: AbortSignal
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Do not use the cache to get the value. Always get a new value from the
|
|
8
|
+
* getter function.
|
|
9
|
+
*
|
|
10
|
+
* @default false
|
|
11
|
+
*/
|
|
12
|
+
noCache?: boolean
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* When getting a value from the cache, allow the value to be returned even if
|
|
16
|
+
* it is stale.
|
|
17
|
+
*
|
|
18
|
+
* Has no effect if the `isStale` option was not provided to the CachedGetter.
|
|
19
|
+
*
|
|
20
|
+
* @default true // If the CachedGetter has an isStale option
|
|
21
|
+
* @default false // If no isStale option was provided to the CachedGetter
|
|
22
|
+
*/
|
|
23
|
+
allowStale?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Getter<K, V> = (
|
|
27
|
+
key: K,
|
|
28
|
+
options: undefined | GetCachedOptions,
|
|
29
|
+
storedValue: undefined | V,
|
|
30
|
+
) => Awaitable<V>
|
|
31
|
+
|
|
32
|
+
export type CachedGetterOptions<K, V> = {
|
|
33
|
+
isStale?: (key: K, value: V) => boolean | PromiseLike<boolean>
|
|
34
|
+
onStoreError?: (err: unknown, key: K, value: V) => void | PromiseLike<void>
|
|
35
|
+
deleteOnError?: (
|
|
36
|
+
err: unknown,
|
|
37
|
+
key: K,
|
|
38
|
+
value: V,
|
|
39
|
+
) => boolean | PromiseLike<boolean>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type PendingItem<V> = Promise<{ value: V; isFresh: boolean }>
|
|
43
|
+
|
|
44
|
+
const returnTrue = () => true
|
|
45
|
+
const returnFalse = () => false
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wrapper utility that uses a store to speed up the retrieval of values from an
|
|
49
|
+
* (expensive) getter function.
|
|
50
|
+
*/
|
|
51
|
+
export class CachedGetter<K extends Key = string, V extends Value = Value> {
|
|
52
|
+
private pending = new Map<K, PendingItem<V>>()
|
|
53
|
+
|
|
54
|
+
constructor(
|
|
55
|
+
readonly getter: Getter<K, V>,
|
|
56
|
+
readonly store: SimpleStore<K, V>,
|
|
57
|
+
readonly options?: Readonly<CachedGetterOptions<K, V>>,
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
async get(key: K, options?: GetCachedOptions): Promise<V> {
|
|
61
|
+
options?.signal?.throwIfAborted()
|
|
62
|
+
|
|
63
|
+
const isStale = this.options?.isStale
|
|
64
|
+
|
|
65
|
+
const allowStored: (value: V) => Awaitable<boolean> = options?.noCache
|
|
66
|
+
? returnFalse // Never allow stored values to be returned
|
|
67
|
+
: options?.allowStale || isStale == null
|
|
68
|
+
? returnTrue // Always allow stored values to be returned
|
|
69
|
+
: async (value: V) => !(await isStale(key, value))
|
|
70
|
+
|
|
71
|
+
// As long as concurrent requests are made for the same key, only one
|
|
72
|
+
// request will be made to the cache & getter function at a time. This works
|
|
73
|
+
// because there is no async operation between the while() loop and the
|
|
74
|
+
// pending.set() call. Because of the "single threaded" nature of
|
|
75
|
+
// JavaScript, the pending item will be set before the next iteration of the
|
|
76
|
+
// while loop.
|
|
77
|
+
let previousExecutionFlow: undefined | PendingItem<V>
|
|
78
|
+
while ((previousExecutionFlow = this.pending.get(key))) {
|
|
79
|
+
try {
|
|
80
|
+
const { isFresh, value } = await previousExecutionFlow
|
|
81
|
+
|
|
82
|
+
if (isFresh) return value
|
|
83
|
+
if (await allowStored(value)) return value
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore errors from previous execution flows (they will have been
|
|
86
|
+
// propagated by that flow).
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
options?.signal?.throwIfAborted()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const currentExecutionFlow: PendingItem<V> = Promise.resolve()
|
|
93
|
+
.then(async () => {
|
|
94
|
+
const storedValue = await this.getStored(key, options)
|
|
95
|
+
if (storedValue !== undefined && (await allowStored(storedValue))) {
|
|
96
|
+
// Use the stored value as return value for the current execution
|
|
97
|
+
// flow. Notify other concurrent execution flows (that should be
|
|
98
|
+
// "stuck" in the loop before until this promise resolves) that we got
|
|
99
|
+
// a value, but that it came from the store (isFresh = false).
|
|
100
|
+
return { isFresh: false, value: storedValue }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Promise.resolve()
|
|
104
|
+
.then(async () => (0, this.getter)(key, options, storedValue))
|
|
105
|
+
.catch(async (err) => {
|
|
106
|
+
if (storedValue !== undefined) {
|
|
107
|
+
if (await this.options?.deleteOnError?.(err, key, storedValue)) {
|
|
108
|
+
await this.delStored(key)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw err
|
|
112
|
+
})
|
|
113
|
+
.then(async (value) => {
|
|
114
|
+
// The value should be stored even is the signal was aborted.
|
|
115
|
+
await this.setStored(key, value)
|
|
116
|
+
return { isFresh: true, value }
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
.finally(() => {
|
|
120
|
+
this.pending.delete(key)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (this.pending.has(key)) {
|
|
124
|
+
// This should never happen. Indeed, there must not be any 'await'
|
|
125
|
+
// statement between this and the loop iteration check meaning that
|
|
126
|
+
// this.pending.get returned undefined. It is there to catch bugs that
|
|
127
|
+
// would occur in future changes to the code.
|
|
128
|
+
throw new Error('Concurrent request for the same key')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.pending.set(key, currentExecutionFlow)
|
|
132
|
+
|
|
133
|
+
const { value } = await currentExecutionFlow
|
|
134
|
+
return value
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
bind(key: K): (options?: GetCachedOptions) => Promise<V> {
|
|
138
|
+
return async (options) => this.get(key, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getStored(key: K, options?: GetCachedOptions): Promise<V | undefined> {
|
|
142
|
+
try {
|
|
143
|
+
return await this.store.get(key, options)
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async setStored(key: K, value: V): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
await this.store.set(key, value)
|
|
152
|
+
} catch (err) {
|
|
153
|
+
await this.options?.onStoreError?.(err, key, value)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async delStored(key: K): Promise<void> {
|
|
158
|
+
await this.store.del(key)
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Awaitable<V> = V | PromiseLike<V>
|
|
2
|
+
|
|
3
|
+
export type Key = string | number
|
|
4
|
+
export type Value = NonNullable<unknown> | null
|
|
5
|
+
|
|
6
|
+
export type GetOptions = { signal?: AbortSignal }
|
|
7
|
+
|
|
8
|
+
export interface SimpleStore<K extends Key = string, V extends Value = Value> {
|
|
9
|
+
/**
|
|
10
|
+
* @return undefined if the key is not in the store (which is why Value cannot contain "undefined").
|
|
11
|
+
*/
|
|
12
|
+
get: (key: K, options?: GetOptions) => Awaitable<undefined | V>
|
|
13
|
+
set: (key: K, value: V) => Awaitable<void>
|
|
14
|
+
del: (key: K) => Awaitable<void>
|
|
15
|
+
clear?: () => Awaitable<void>
|
|
16
|
+
}
|
package/tsconfig.json
ADDED