@devvit/public-api 0.10.23-next-2024-06-28-567251747.0 → 0.10.23-next-2024-07-01-a6d03873c.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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;AAiBxD,qBAAa,cAAc;IAClB,YAAY,CACjB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,SAAS,EAClB,QAAQ,EAAE,QAAQ,GACjB,MAAM,CAAC,OAAO;CA2ClB"}
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 = UnimplementedProxy;
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":"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"}
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 ?? {}, "f");
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,5 @@
1
+ import { PromiseCache, SystemClock } from './promise_cache.js';
2
+ export function makeCache(redis, state, clock = SystemClock) {
3
+ const pc = new PromiseCache(redis, state, clock);
4
+ return pc.cache.bind(pc);
5
+ }
@@ -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": 6339,
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": 3122,
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": 4721
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": 1523
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": 14769344
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": 6339,
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": 3122,
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": 1151365
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": 2315
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": 605
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": 245925
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-06-28-567251747.0",
3
+ "version": "0.10.23-next-2024-07-01-a6d03873c.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-06-28-567251747.0",
34
- "@devvit/shared-types": "0.10.23-next-2024-06-28-567251747.0",
33
+ "@devvit/protos": "0.10.23-next-2024-07-01-a6d03873c.0",
34
+ "@devvit/shared-types": "0.10.23-next-2024-07-01-a6d03873c.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-06-28-567251747.0",
43
- "@devvit/repo-tools": "0.10.23-next-2024-06-28-567251747.0",
44
- "@devvit/tsconfig": "0.10.23-next-2024-06-28-567251747.0",
42
+ "@devvit/eslint-config": "0.10.23-next-2024-07-01-a6d03873c.0",
43
+ "@devvit/repo-tools": "0.10.23-next-2024-07-01-a6d03873c.0",
44
+ "@devvit/tsconfig": "0.10.23-next-2024-07-01-a6d03873c.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": "57fef409f5bed1298d694999487c070f32fdfb37"
67
+ "gitHead": "6f1cebd8c39aba1a2b93e87377969e3de53edd1a"
68
68
  }