@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 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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"}
@@ -0,0 +1,3 @@
1
+ export * from './cached-getter.js';
2
+ export * from './simple-store.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=simple-store.js.map
@@ -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,2 @@
1
+ export * from './cached-getter.js'
2
+ export * from './simple-store.js'
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig/isomorphic.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["./src"]
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "include": [],
3
+ "references": [{ "path": "./tsconfig.build.json" }]
4
+ }