@devvit/public-api 0.10.23-next-2024-06-28-567251747.0 → 0.10.23-next-2024-07-01-6fe6bfe05.0
Sign up to get free protection for your applications and to get access to all the features.
- package/devvit/internals/blocks/handler/ContextBuilder.d.ts.map +1 -1
- package/devvit/internals/blocks/handler/ContextBuilder.js +2 -7
- package/devvit/internals/blocks/handler/RenderContext.d.ts.map +1 -1
- package/devvit/internals/blocks/handler/RenderContext.js +12 -2
- package/devvit/internals/blocks/handler/cache.d.ts +9 -0
- package/devvit/internals/blocks/handler/cache.d.ts.map +1 -0
- package/devvit/internals/blocks/handler/cache.js +5 -0
- package/devvit/internals/blocks/handler/cache.test.d.ts.map +1 -0
- package/devvit/internals/blocks/handler/promise_cache.d.ts +67 -0
- package/devvit/internals/blocks/handler/promise_cache.d.ts.map +1 -0
- package/devvit/internals/blocks/handler/promise_cache.js +239 -0
- package/meta.json +38 -5
- package/meta.min.json +39 -6
- package/package.json +7 -7
- package/public-api.iife.js +243 -11
- package/public-api.min.js +5 -5
- package/public-api.min.js.map +4 -4
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"ContextBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/ContextBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAY1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
1
|
+
{"version":3,"file":"ContextBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/ContextBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAY1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQxD,qBAAa,cAAc;IAClB,YAAY,CACjB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,SAAS,EAClB,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAAC,OAAO;CA2ClB"}
|
@@ -14,12 +14,7 @@ import { useChannel } from './useChannel.js';
|
|
14
14
|
import { useForm } from './useForm.js';
|
15
15
|
import { useInterval } from './useInterval.js';
|
16
16
|
import { useState } from './useState.js';
|
17
|
-
|
18
|
-
const UnimplementedProxy = new Proxy({}, {
|
19
|
-
get: function (_target, prop, _receiver) {
|
20
|
-
throw new Error(`Unimplemented API: ${String(prop)}`);
|
21
|
-
},
|
22
|
-
});
|
17
|
+
import { makeCache } from './cache.js';
|
23
18
|
export class ContextBuilder {
|
24
19
|
buildContext(renderContext, request, metadata) {
|
25
20
|
const modLog = new ModLogClient(metadata);
|
@@ -32,7 +27,7 @@ export class ContextBuilder {
|
|
32
27
|
const media = new MediaClient(metadata);
|
33
28
|
const assets = new AssetsClient();
|
34
29
|
const realtime = new RealtimeClient(metadata);
|
35
|
-
const cache =
|
30
|
+
const cache = makeCache(redis, renderContext._state);
|
36
31
|
const apiClients = {
|
37
32
|
modLog,
|
38
33
|
kvStore,
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"RenderContext.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/RenderContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAexF;;;;;;;;;GASG;AACH,qBAAa,aAAc,YAAW,aAAa;;IACjD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAElC,SAAS,EAAE,CAAC,WAAW,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,CAAM;IACnD,MAAM,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAM;IACxC,WAAW,EAAE,MAAM,CAAM;IACzB,QAAQ,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAM;IACzC,QAAQ,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAM;IAC1C,qDAAqD;IACrD,cAAc,EAAE,OAAO,EAAE,CAAM;IAE/B,UAAU,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAM;IACxC,UAAU,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAM;IAC5C,MAAM,CAAC,0BAA0B,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAM;IACxE,oBAAoB,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAA;KAAE,CAAM;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAEhC,IAAI,aAAa,IAAI,MAAM,CAAC,OAAO,CAKlC;IAED,IAAI,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAExC;gBAEW,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ;
|
1
|
+
{"version":3,"file":"RenderContext.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/RenderContext.ts"],"names":[],"mappings":"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"}
|
@@ -49,19 +49,29 @@ export class RenderContext {
|
|
49
49
|
this._undeliveredHandlers = {};
|
50
50
|
this.request = request;
|
51
51
|
this.meta = meta;
|
52
|
-
__classPrivateFieldSet(this, _RenderContext_state, request.state ?? {
|
52
|
+
__classPrivateFieldSet(this, _RenderContext_state, request.state ?? {
|
53
|
+
__cache: {},
|
54
|
+
}, "f");
|
53
55
|
this._rootProps = request.props ?? {};
|
54
56
|
}
|
55
57
|
/** The state delta new to this render. */
|
56
58
|
get _changedState() {
|
57
|
-
const changed = {
|
59
|
+
const changed = {
|
60
|
+
__cache: __classPrivateFieldGet(this, _RenderContext_state, "f").__cache ?? {},
|
61
|
+
};
|
58
62
|
for (const key in this._changed)
|
59
63
|
changed[key] = this._state[key];
|
60
64
|
const unmounted = new Set(Object.keys(this._state));
|
61
65
|
Object.keys(this._hooks).forEach((key) => {
|
66
|
+
if (key === '__cache') {
|
67
|
+
return;
|
68
|
+
}
|
62
69
|
unmounted.delete(key);
|
63
70
|
});
|
64
71
|
unmounted.forEach((key) => {
|
72
|
+
if (key === '__cache') {
|
73
|
+
return;
|
74
|
+
}
|
65
75
|
const t = { __deleted: true };
|
66
76
|
this._state[key] = changed[key] = t;
|
67
77
|
});
|
@@ -0,0 +1,9 @@
|
|
1
|
+
import type { RedisClient } from '../../../../types/redis.js';
|
2
|
+
import type { JSONValue } from '@devvit/shared-types/json.js';
|
3
|
+
import type { CacheOptions, Clock, LocalCache } from './promise_cache.js';
|
4
|
+
import type { RenderContext } from './RenderContext.js';
|
5
|
+
export type CacheHelper = <T extends JSONValue>(fn: () => Promise<T>, options: CacheOptions) => Promise<T>;
|
6
|
+
export declare function makeCache(redis: RedisClient, state: RenderContext['_state'] & {
|
7
|
+
__cache?: LocalCache;
|
8
|
+
}, clock?: Clock): CacheHelper;
|
9
|
+
//# sourceMappingURL=cache.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,SAAS,EAC5C,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,EAAE,YAAY,KAClB,OAAO,CAAC,CAAC,CAAC,CAAC;AAEhB,wBAAgB,SAAS,CACvB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,UAAU,CAAA;CAAE,EACzD,KAAK,GAAE,KAAmB,GACzB,WAAW,CAGb"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"cache.test.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/cache.test.ts"],"names":[],"mappings":""}
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import type { JSONValue, RedisClient } from '../../../../index.js';
|
2
|
+
export type CacheEntry = {
|
3
|
+
value: JSONValue | null;
|
4
|
+
expires: number;
|
5
|
+
error: string | null;
|
6
|
+
errorTime: number | null;
|
7
|
+
checkedAt: number;
|
8
|
+
errorCount: number;
|
9
|
+
};
|
10
|
+
export type Clock = {
|
11
|
+
now(): Date;
|
12
|
+
};
|
13
|
+
export declare const SystemClock: Clock;
|
14
|
+
export type CacheOptions = {
|
15
|
+
/**
|
16
|
+
* Time to live in milliseconds.
|
17
|
+
*/
|
18
|
+
ttl: number;
|
19
|
+
/**
|
20
|
+
* Key to use for caching.
|
21
|
+
*/
|
22
|
+
key: string;
|
23
|
+
};
|
24
|
+
export type LocalCache = {
|
25
|
+
[key: string]: CacheEntry;
|
26
|
+
};
|
27
|
+
export declare function _namespaced(key: string): string;
|
28
|
+
export declare function _lock(key: string): string;
|
29
|
+
export declare const retryLimit = 3;
|
30
|
+
export declare const clientRetryDelay = 1000;
|
31
|
+
export declare const allowStaleFor = 30000;
|
32
|
+
type WithLocalCache = {
|
33
|
+
__cache?: LocalCache;
|
34
|
+
};
|
35
|
+
/**
|
36
|
+
* Refactored out into a class to allow for easier testing and clarity of purpose.
|
37
|
+
*
|
38
|
+
* This class is responsible for managing the caching of promises. It is a layered cache, meaning it will first check
|
39
|
+
* the local cache, then the redis cache, and finally the source of truth. It will also handle refreshing the cache according
|
40
|
+
* to the TTL and error handling.
|
41
|
+
*
|
42
|
+
* Please note that in order to prevent a stampede of requests to the source of truth, we use a lock in redis to ensure only one
|
43
|
+
* request is made to the source of truth at a time. If the lock is obtained, the cache will be updated and the lock will be released.
|
44
|
+
*
|
45
|
+
* Additionally, we use a polling mechanism to fetch the cache if the lock is not obtained. This is to prevent unnecessary errors.
|
46
|
+
*
|
47
|
+
* Finally, we also want to prevent stampedes against redis for the lock election and the retries. We use a ramping probability to ease in the
|
48
|
+
* attempts to get the lock, and not every error will trigger a retry.
|
49
|
+
*
|
50
|
+
* This means that the cache will be eventually consistent, but will not be immediately consistent. This is a tradeoff we are willing to make.
|
51
|
+
* Additionally, this means that the TTL is not precice. The cache may be updated a bit more often than the TTL, but it will not be updated less often.
|
52
|
+
*
|
53
|
+
*/
|
54
|
+
export declare class PromiseCache {
|
55
|
+
#private;
|
56
|
+
constructor(redis: RedisClient, state: WithLocalCache, clock?: Clock);
|
57
|
+
/**
|
58
|
+
* This is the public API for the cache. Call this method to cache a promise.
|
59
|
+
*
|
60
|
+
* @param closure
|
61
|
+
* @param options
|
62
|
+
* @returns
|
63
|
+
*/
|
64
|
+
cache<T extends JSONValue>(closure: () => Promise<T>, options: CacheOptions): Promise<T>;
|
65
|
+
}
|
66
|
+
export {};
|
67
|
+
//# sourceMappingURL=promise_cache.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"promise_cache.d.ts","sourceRoot":"","sources":["../../../../../src/devvit/internals/blocks/handler/promise_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnE,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAClB,GAAG,IAAI,IAAI,CAAC;CACb,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAIzB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE,CAAC;AAEvD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AACD,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzC;AAKD,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,aAAa,QAAS,CAAC;AAEpC,KAAK,cAAc,GAAG;IACpB,OAAO,CAAC,EAAE,UAAU,CAAC;CACtB,CAAC;AASF;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,YAAY;;gBAUX,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,GAAE,KAAmB;IAMjF;;;;;;OAMG;IACG,KAAK,CAAC,CAAC,SAAS,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC;CA6M/F"}
|
@@ -0,0 +1,239 @@
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
6
|
+
};
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
+
};
|
12
|
+
var _PromiseCache_instances, _PromiseCache_redis, _PromiseCache_localCache, _PromiseCache_clock, _PromiseCache_state, _PromiseCache_localCachedAnswer, _PromiseCache_maybeRefreshCache, _PromiseCache_refreshCache, _PromiseCache_pollForCache, _PromiseCache_updateCache, _PromiseCache_calculateRamp, _PromiseCache_redisEntry, _PromiseCache_enforceTTL;
|
13
|
+
export const SystemClock = {
|
14
|
+
now() {
|
15
|
+
return new Date();
|
16
|
+
},
|
17
|
+
};
|
18
|
+
export function _namespaced(key) {
|
19
|
+
return `__autocache__${key}`;
|
20
|
+
}
|
21
|
+
export function _lock(key) {
|
22
|
+
return `__lock__${key}`;
|
23
|
+
}
|
24
|
+
const pollEvery = 300; // milli
|
25
|
+
const maxPollingTimeout = 1000; // milli
|
26
|
+
const minTtlValue = 5000;
|
27
|
+
export const retryLimit = 3;
|
28
|
+
const errorRetryProbability = 0.1;
|
29
|
+
export const clientRetryDelay = 1000;
|
30
|
+
export const allowStaleFor = 30000;
|
31
|
+
function _unwrap(entry) {
|
32
|
+
if (entry.error) {
|
33
|
+
throw new Error(entry.error);
|
34
|
+
}
|
35
|
+
return entry.value;
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* Refactored out into a class to allow for easier testing and clarity of purpose.
|
39
|
+
*
|
40
|
+
* This class is responsible for managing the caching of promises. It is a layered cache, meaning it will first check
|
41
|
+
* the local cache, then the redis cache, and finally the source of truth. It will also handle refreshing the cache according
|
42
|
+
* to the TTL and error handling.
|
43
|
+
*
|
44
|
+
* Please note that in order to prevent a stampede of requests to the source of truth, we use a lock in redis to ensure only one
|
45
|
+
* request is made to the source of truth at a time. If the lock is obtained, the cache will be updated and the lock will be released.
|
46
|
+
*
|
47
|
+
* Additionally, we use a polling mechanism to fetch the cache if the lock is not obtained. This is to prevent unnecessary errors.
|
48
|
+
*
|
49
|
+
* Finally, we also want to prevent stampedes against redis for the lock election and the retries. We use a ramping probability to ease in the
|
50
|
+
* attempts to get the lock, and not every error will trigger a retry.
|
51
|
+
*
|
52
|
+
* This means that the cache will be eventually consistent, but will not be immediately consistent. This is a tradeoff we are willing to make.
|
53
|
+
* Additionally, this means that the TTL is not precice. The cache may be updated a bit more often than the TTL, but it will not be updated less often.
|
54
|
+
*
|
55
|
+
*/
|
56
|
+
export class PromiseCache {
|
57
|
+
constructor(redis, state, clock = SystemClock) {
|
58
|
+
_PromiseCache_instances.add(this);
|
59
|
+
_PromiseCache_redis.set(this, void 0);
|
60
|
+
/**
|
61
|
+
* LocalCache is just an aliased reference to this.#state. Mutations to
|
62
|
+
* this object will also mutate this.#state
|
63
|
+
*/
|
64
|
+
_PromiseCache_localCache.set(this, {});
|
65
|
+
_PromiseCache_clock.set(this, void 0);
|
66
|
+
_PromiseCache_state.set(this, void 0);
|
67
|
+
__classPrivateFieldSet(this, _PromiseCache_redis, redis, "f");
|
68
|
+
__classPrivateFieldSet(this, _PromiseCache_state, state, "f");
|
69
|
+
__classPrivateFieldSet(this, _PromiseCache_clock, clock, "f");
|
70
|
+
}
|
71
|
+
/**
|
72
|
+
* This is the public API for the cache. Call this method to cache a promise.
|
73
|
+
*
|
74
|
+
* @param closure
|
75
|
+
* @param options
|
76
|
+
* @returns
|
77
|
+
*/
|
78
|
+
async cache(closure, options) {
|
79
|
+
var _a;
|
80
|
+
(_a = __classPrivateFieldGet(this, _PromiseCache_state, "f")).__cache ?? (_a.__cache = {});
|
81
|
+
__classPrivateFieldSet(this, _PromiseCache_localCache, __classPrivateFieldGet(this, _PromiseCache_state, "f").__cache, "f");
|
82
|
+
__classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_enforceTTL).call(this, options);
|
83
|
+
const localCachedAnswer = __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_localCachedAnswer).call(this, options.key);
|
84
|
+
if (localCachedAnswer !== undefined) {
|
85
|
+
return localCachedAnswer;
|
86
|
+
}
|
87
|
+
const existing = await __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_redisEntry).call(this, options.key);
|
88
|
+
const entry = await __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_maybeRefreshCache).call(this, options, existing, closure);
|
89
|
+
return _unwrap(entry);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
_PromiseCache_redis = new WeakMap(), _PromiseCache_localCache = new WeakMap(), _PromiseCache_clock = new WeakMap(), _PromiseCache_state = new WeakMap(), _PromiseCache_instances = new WeakSet(), _PromiseCache_localCachedAnswer = function _PromiseCache_localCachedAnswer(key) {
|
93
|
+
const val = __classPrivateFieldGet(this, _PromiseCache_localCache, "f")[key];
|
94
|
+
if (val) {
|
95
|
+
const now = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime();
|
96
|
+
const hasRetryableError = val?.error &&
|
97
|
+
val?.errorTime &&
|
98
|
+
val.errorCount < retryLimit &&
|
99
|
+
Math.random() < errorRetryProbability &&
|
100
|
+
val.errorTime + clientRetryDelay < now;
|
101
|
+
const expired = val?.expires && val.expires < now && val.checkedAt + clientRetryDelay < now;
|
102
|
+
if (expired || hasRetryableError) {
|
103
|
+
delete __classPrivateFieldGet(this, _PromiseCache_localCache, "f")[key];
|
104
|
+
return undefined;
|
105
|
+
}
|
106
|
+
else {
|
107
|
+
return _unwrap(val);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
return undefined;
|
111
|
+
}, _PromiseCache_maybeRefreshCache =
|
112
|
+
/**
|
113
|
+
* If we've bothered to check redis, we're already on the backend. Let's see if the cache either (1) contains an error, (2)
|
114
|
+
* is expired, (3) is missing, or (4) is about to expire. If any of these are true, we'll refresh the cache based on heuristics.
|
115
|
+
*
|
116
|
+
* We'll always refresh if missing or expired, but its probabilistic if we'll refresh if about to expire or if we have an error.
|
117
|
+
*/
|
118
|
+
async function _PromiseCache_maybeRefreshCache(options, entry, closure) {
|
119
|
+
const expires = entry?.expires;
|
120
|
+
const rampProbability = expires ? __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_calculateRamp).call(this, expires) : 1;
|
121
|
+
if (!entry ||
|
122
|
+
(entry?.error && entry.errorCount < retryLimit && errorRetryProbability > Math.random()) ||
|
123
|
+
rampProbability > Math.random()) {
|
124
|
+
return __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_refreshCache).call(this, options, entry, closure);
|
125
|
+
}
|
126
|
+
else {
|
127
|
+
return entry;
|
128
|
+
}
|
129
|
+
}, _PromiseCache_refreshCache =
|
130
|
+
/**
|
131
|
+
* The conditions for refreshing the cache are handled in the calling method, which should be
|
132
|
+
* #maybeRefreshCache.
|
133
|
+
*
|
134
|
+
* If you don't win the lock, you'll poll for the cache. If you don't get the cache within maxPollingTimeout, you'll throw an error.
|
135
|
+
*/
|
136
|
+
async function _PromiseCache_refreshCache(options, entry, closure) {
|
137
|
+
const lockKey = _lock(options.key);
|
138
|
+
const now = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime();
|
139
|
+
/**
|
140
|
+
* The write lock should last for a while, but not the full TTL. Hopefully write attempts settle down after a while.
|
141
|
+
*/
|
142
|
+
const lockExpiration = new Date(now + options.ttl / 2);
|
143
|
+
const lockObtained = await __classPrivateFieldGet(this, _PromiseCache_redis, "f").set(lockKey, '1', {
|
144
|
+
expiration: lockExpiration,
|
145
|
+
nx: true,
|
146
|
+
});
|
147
|
+
if (lockObtained) {
|
148
|
+
return __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_updateCache).call(this, options.key, entry, closure, options.ttl);
|
149
|
+
}
|
150
|
+
else if (entry) {
|
151
|
+
// This entry is still valid, return it
|
152
|
+
return entry;
|
153
|
+
}
|
154
|
+
else {
|
155
|
+
const start = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now();
|
156
|
+
return __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_pollForCache).call(this, start, options.key, options.ttl);
|
157
|
+
}
|
158
|
+
}, _PromiseCache_pollForCache = async function _PromiseCache_pollForCache(start, key, ttl) {
|
159
|
+
const pollingTimeout = Math.min(ttl, maxPollingTimeout);
|
160
|
+
const existing = await __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_redisEntry).call(this, key);
|
161
|
+
if (existing) {
|
162
|
+
return existing;
|
163
|
+
}
|
164
|
+
if (__classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime() - start.getTime() >= pollingTimeout) {
|
165
|
+
throw new Error(`Cache request timed out trying to get data at key: ${key}`);
|
166
|
+
}
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, pollEvery));
|
168
|
+
return __classPrivateFieldGet(this, _PromiseCache_instances, "m", _PromiseCache_pollForCache).call(this, start, key, ttl);
|
169
|
+
}, _PromiseCache_updateCache =
|
170
|
+
/**
|
171
|
+
* Actually update the cache. This is the method that will be called if we have the lock.
|
172
|
+
*/
|
173
|
+
async function _PromiseCache_updateCache(key, entry, closure, ttl) {
|
174
|
+
const expires = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime() + ttl;
|
175
|
+
entry = entry ?? {
|
176
|
+
value: null,
|
177
|
+
expires,
|
178
|
+
errorCount: 0,
|
179
|
+
error: null,
|
180
|
+
errorTime: null,
|
181
|
+
checkedAt: 0,
|
182
|
+
};
|
183
|
+
try {
|
184
|
+
entry.value = await closure();
|
185
|
+
entry.error = null;
|
186
|
+
entry.errorCount = 0;
|
187
|
+
entry.errorTime = null;
|
188
|
+
}
|
189
|
+
catch (e) {
|
190
|
+
entry.value = null;
|
191
|
+
entry.error = e.message ?? 'Unknown error';
|
192
|
+
entry.errorTime = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime();
|
193
|
+
entry.errorCount++;
|
194
|
+
}
|
195
|
+
__classPrivateFieldGet(this, _PromiseCache_localCache, "f")[key] = entry;
|
196
|
+
await __classPrivateFieldGet(this, _PromiseCache_redis, "f").set(_namespaced(key), JSON.stringify(entry), {
|
197
|
+
expiration: new Date(expires + allowStaleFor),
|
198
|
+
});
|
199
|
+
/**
|
200
|
+
* Unlocking will allow retries to happen if there was an error. Otherwise we don't unlock, because the lock
|
201
|
+
* will expire on its own.
|
202
|
+
*/
|
203
|
+
if (entry.error && entry.errorCount < retryLimit) {
|
204
|
+
await __classPrivateFieldGet(this, _PromiseCache_redis, "f").del(_lock(key));
|
205
|
+
}
|
206
|
+
return entry;
|
207
|
+
}, _PromiseCache_calculateRamp = function _PromiseCache_calculateRamp(expiry) {
|
208
|
+
const now = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime();
|
209
|
+
const remaining = expiry - now;
|
210
|
+
if (remaining < 0) {
|
211
|
+
return 1;
|
212
|
+
}
|
213
|
+
else if (remaining < 1000) {
|
214
|
+
return 0.1;
|
215
|
+
}
|
216
|
+
else if (remaining < 2000) {
|
217
|
+
return 0.01;
|
218
|
+
}
|
219
|
+
else if (remaining < 3000) {
|
220
|
+
return 0.001;
|
221
|
+
}
|
222
|
+
else {
|
223
|
+
return 0;
|
224
|
+
}
|
225
|
+
}, _PromiseCache_redisEntry = async function _PromiseCache_redisEntry(key) {
|
226
|
+
const val = await __classPrivateFieldGet(this, _PromiseCache_redis, "f").get(_namespaced(key));
|
227
|
+
if (val) {
|
228
|
+
const entry = JSON.parse(val);
|
229
|
+
entry.checkedAt = __classPrivateFieldGet(this, _PromiseCache_clock, "f").now().getTime();
|
230
|
+
__classPrivateFieldGet(this, _PromiseCache_localCache, "f")[key] = entry;
|
231
|
+
return entry;
|
232
|
+
}
|
233
|
+
return undefined;
|
234
|
+
}, _PromiseCache_enforceTTL = function _PromiseCache_enforceTTL(options) {
|
235
|
+
if (options.ttl < minTtlValue) {
|
236
|
+
console.warn(`Cache TTL cannot be less than ${minTtlValue} milliseconds! Updating ttl value of ${options.ttl} to ${minTtlValue}.`);
|
237
|
+
options.ttl = minTtlValue;
|
238
|
+
}
|
239
|
+
};
|
package/meta.json
CHANGED
@@ -12191,7 +12191,7 @@
|
|
12191
12191
|
"format": "esm"
|
12192
12192
|
},
|
12193
12193
|
"src/devvit/internals/blocks/handler/RenderContext.ts": {
|
12194
|
-
"bytes":
|
12194
|
+
"bytes": 6522,
|
12195
12195
|
"imports": [
|
12196
12196
|
{
|
12197
12197
|
"path": "<runtime>",
|
@@ -12253,8 +12253,30 @@
|
|
12253
12253
|
],
|
12254
12254
|
"format": "esm"
|
12255
12255
|
},
|
12256
|
+
"src/devvit/internals/blocks/handler/promise_cache.ts": {
|
12257
|
+
"bytes": 9791,
|
12258
|
+
"imports": [
|
12259
|
+
{
|
12260
|
+
"path": "<runtime>",
|
12261
|
+
"kind": "import-statement",
|
12262
|
+
"external": true
|
12263
|
+
}
|
12264
|
+
],
|
12265
|
+
"format": "esm"
|
12266
|
+
},
|
12267
|
+
"src/devvit/internals/blocks/handler/cache.ts": {
|
12268
|
+
"bytes": 676,
|
12269
|
+
"imports": [
|
12270
|
+
{
|
12271
|
+
"path": "src/devvit/internals/blocks/handler/promise_cache.ts",
|
12272
|
+
"kind": "import-statement",
|
12273
|
+
"original": "./promise_cache.js"
|
12274
|
+
}
|
12275
|
+
],
|
12276
|
+
"format": "esm"
|
12277
|
+
},
|
12256
12278
|
"src/devvit/internals/blocks/handler/ContextBuilder.ts": {
|
12257
|
-
"bytes":
|
12279
|
+
"bytes": 2943,
|
12258
12280
|
"imports": [
|
12259
12281
|
{
|
12260
12282
|
"path": "../shared-types/dist/Header.js",
|
@@ -12335,6 +12357,11 @@
|
|
12335
12357
|
"path": "src/devvit/internals/blocks/handler/useState.ts",
|
12336
12358
|
"kind": "import-statement",
|
12337
12359
|
"original": "./useState.js"
|
12360
|
+
},
|
12361
|
+
{
|
12362
|
+
"path": "src/devvit/internals/blocks/handler/cache.ts",
|
12363
|
+
"kind": "import-statement",
|
12364
|
+
"original": "./cache.js"
|
12338
12365
|
}
|
12339
12366
|
],
|
12340
12367
|
"format": "esm"
|
@@ -14274,7 +14301,7 @@
|
|
14274
14301
|
"bytesInOutput": 5122
|
14275
14302
|
},
|
14276
14303
|
"src/devvit/internals/blocks/handler/RenderContext.ts": {
|
14277
|
-
"bytesInOutput":
|
14304
|
+
"bytesInOutput": 4936
|
14278
14305
|
},
|
14279
14306
|
"src/devvit/internals/blocks/handler/useInterval.ts": {
|
14280
14307
|
"bytesInOutput": 2357
|
@@ -14285,8 +14312,14 @@
|
|
14285
14312
|
"src/devvit/internals/blocks/handler/useState.ts": {
|
14286
14313
|
"bytesInOutput": 2314
|
14287
14314
|
},
|
14315
|
+
"src/devvit/internals/blocks/handler/promise_cache.ts": {
|
14316
|
+
"bytesInOutput": 9074
|
14317
|
+
},
|
14318
|
+
"src/devvit/internals/blocks/handler/cache.ts": {
|
14319
|
+
"bytesInOutput": 149
|
14320
|
+
},
|
14288
14321
|
"src/devvit/internals/blocks/handler/ContextBuilder.ts": {
|
14289
|
-
"bytesInOutput":
|
14322
|
+
"bytesInOutput": 1362
|
14290
14323
|
},
|
14291
14324
|
"src/devvit/internals/ui-request-handler.ts": {
|
14292
14325
|
"bytesInOutput": 618
|
@@ -14376,7 +14409,7 @@
|
|
14376
14409
|
"bytesInOutput": 4450
|
14377
14410
|
}
|
14378
14411
|
},
|
14379
|
-
"bytes":
|
14412
|
+
"bytes": 14800351
|
14380
14413
|
}
|
14381
14414
|
}
|
14382
14415
|
}
|
package/meta.min.json
CHANGED
@@ -3577,7 +3577,7 @@
|
|
3577
3577
|
"format": "esm"
|
3578
3578
|
},
|
3579
3579
|
"src/devvit/internals/blocks/handler/RenderContext.ts": {
|
3580
|
-
"bytes":
|
3580
|
+
"bytes": 6522,
|
3581
3581
|
"imports": [
|
3582
3582
|
{
|
3583
3583
|
"path": "<runtime>",
|
@@ -3639,8 +3639,30 @@
|
|
3639
3639
|
],
|
3640
3640
|
"format": "esm"
|
3641
3641
|
},
|
3642
|
+
"src/devvit/internals/blocks/handler/promise_cache.ts": {
|
3643
|
+
"bytes": 9791,
|
3644
|
+
"imports": [
|
3645
|
+
{
|
3646
|
+
"path": "<runtime>",
|
3647
|
+
"kind": "import-statement",
|
3648
|
+
"external": true
|
3649
|
+
}
|
3650
|
+
],
|
3651
|
+
"format": "esm"
|
3652
|
+
},
|
3653
|
+
"src/devvit/internals/blocks/handler/cache.ts": {
|
3654
|
+
"bytes": 676,
|
3655
|
+
"imports": [
|
3656
|
+
{
|
3657
|
+
"path": "src/devvit/internals/blocks/handler/promise_cache.ts",
|
3658
|
+
"kind": "import-statement",
|
3659
|
+
"original": "./promise_cache.js"
|
3660
|
+
}
|
3661
|
+
],
|
3662
|
+
"format": "esm"
|
3663
|
+
},
|
3642
3664
|
"src/devvit/internals/blocks/handler/ContextBuilder.ts": {
|
3643
|
-
"bytes":
|
3665
|
+
"bytes": 2943,
|
3644
3666
|
"imports": [
|
3645
3667
|
{
|
3646
3668
|
"path": "../shared-types/dist/Header.js",
|
@@ -3721,6 +3743,11 @@
|
|
3721
3743
|
"path": "src/devvit/internals/blocks/handler/useState.ts",
|
3722
3744
|
"kind": "import-statement",
|
3723
3745
|
"original": "./useState.js"
|
3746
|
+
},
|
3747
|
+
{
|
3748
|
+
"path": "src/devvit/internals/blocks/handler/cache.ts",
|
3749
|
+
"kind": "import-statement",
|
3750
|
+
"original": "./cache.js"
|
3724
3751
|
}
|
3725
3752
|
],
|
3726
3753
|
"format": "esm"
|
@@ -4662,7 +4689,7 @@
|
|
4662
4689
|
"imports": [],
|
4663
4690
|
"exports": [],
|
4664
4691
|
"inputs": {},
|
4665
|
-
"bytes":
|
4692
|
+
"bytes": 1167454
|
4666
4693
|
},
|
4667
4694
|
"dist/public-api.min.js": {
|
4668
4695
|
"imports": [
|
@@ -5359,7 +5386,7 @@
|
|
5359
5386
|
"bytesInOutput": 1184
|
5360
5387
|
},
|
5361
5388
|
"src/devvit/internals/blocks/handler/RenderContext.ts": {
|
5362
|
-
"bytesInOutput":
|
5389
|
+
"bytesInOutput": 2394
|
5363
5390
|
},
|
5364
5391
|
"src/devvit/internals/blocks/handler/types.ts": {
|
5365
5392
|
"bytesInOutput": 29
|
@@ -5367,8 +5394,14 @@
|
|
5367
5394
|
"src/devvit/internals/blocks/handler/useState.ts": {
|
5368
5395
|
"bytesInOutput": 1088
|
5369
5396
|
},
|
5397
|
+
"src/devvit/internals/blocks/handler/promise_cache.ts": {
|
5398
|
+
"bytesInOutput": 2835
|
5399
|
+
},
|
5400
|
+
"src/devvit/internals/blocks/handler/cache.ts": {
|
5401
|
+
"bytesInOutput": 65
|
5402
|
+
},
|
5370
5403
|
"src/devvit/internals/blocks/handler/ContextBuilder.ts": {
|
5371
|
-
"bytesInOutput":
|
5404
|
+
"bytesInOutput": 530
|
5372
5405
|
},
|
5373
5406
|
"src/apis/reddit/helpers/makeGettersEnumerable.ts": {
|
5374
5407
|
"bytesInOutput": 166
|
@@ -5452,7 +5485,7 @@
|
|
5452
5485
|
"bytesInOutput": 2135
|
5453
5486
|
}
|
5454
5487
|
},
|
5455
|
-
"bytes":
|
5488
|
+
"bytes": 248829
|
5456
5489
|
}
|
5457
5490
|
}
|
5458
5491
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@devvit/public-api",
|
3
|
-
"version": "0.10.23-next-2024-
|
3
|
+
"version": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
4
4
|
"license": "BSD-3-Clause",
|
5
5
|
"repository": {
|
6
6
|
"type": "git",
|
@@ -30,8 +30,8 @@
|
|
30
30
|
},
|
31
31
|
"types": "./index.d.ts",
|
32
32
|
"dependencies": {
|
33
|
-
"@devvit/protos": "0.10.23-next-2024-
|
34
|
-
"@devvit/shared-types": "0.10.23-next-2024-
|
33
|
+
"@devvit/protos": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
34
|
+
"@devvit/shared-types": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
35
35
|
"base64-js": "1.5.1",
|
36
36
|
"clone-deep": "4.0.1",
|
37
37
|
"core-js": "3.27.2",
|
@@ -39,9 +39,9 @@
|
|
39
39
|
},
|
40
40
|
"devDependencies": {
|
41
41
|
"@ampproject/filesize": "4.3.0",
|
42
|
-
"@devvit/eslint-config": "0.10.23-next-2024-
|
43
|
-
"@devvit/repo-tools": "0.10.23-next-2024-
|
44
|
-
"@devvit/tsconfig": "0.10.23-next-2024-
|
42
|
+
"@devvit/eslint-config": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
43
|
+
"@devvit/repo-tools": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
44
|
+
"@devvit/tsconfig": "0.10.23-next-2024-07-01-6fe6bfe05.0",
|
45
45
|
"@microsoft/api-extractor": "7.41.0",
|
46
46
|
"@reddit/faceplate-ui": "11.3.3",
|
47
47
|
"@types/clone-deep": "4.0.1",
|
@@ -64,5 +64,5 @@
|
|
64
64
|
}
|
65
65
|
},
|
66
66
|
"source": "./src/index.ts",
|
67
|
-
"gitHead": "
|
67
|
+
"gitHead": "31b9a70a857281d8a15faf9d6de83c76d5bb0479"
|
68
68
|
}
|