@global-cache/core 0.2.3

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.
Files changed (162) hide show
  1. package/README.md +5 -0
  2. package/dist/client/api.d.ts +21 -0
  3. package/dist/client/api.d.ts.map +1 -0
  4. package/dist/client/api.js +43 -0
  5. package/dist/client/api.js.map +1 -0
  6. package/dist/client/index.d.ts +27 -0
  7. package/dist/client/index.d.ts.map +1 -0
  8. package/dist/client/index.js +135 -0
  9. package/dist/client/index.js.map +1 -0
  10. package/dist/client/types.d.ts +12 -0
  11. package/dist/client/types.d.ts.map +1 -0
  12. package/dist/client/types.js +3 -0
  13. package/dist/client/types.js.map +1 -0
  14. package/dist/client/utils/http-error.d.ts +3 -0
  15. package/dist/client/utils/http-error.d.ts.map +1 -0
  16. package/dist/client/utils/http-error.js +13 -0
  17. package/dist/client/utils/http-error.js.map +1 -0
  18. package/dist/client/utils/http-json.d.ts +14 -0
  19. package/dist/client/utils/http-json.d.ts.map +1 -0
  20. package/dist/client/utils/http-json.js +29 -0
  21. package/dist/client/utils/http-json.js.map +1 -0
  22. package/dist/client/utils/http-query.d.ts +9 -0
  23. package/dist/client/utils/http-query.d.ts.map +1 -0
  24. package/dist/client/utils/http-query.js +25 -0
  25. package/dist/client/utils/http-query.js.map +1 -0
  26. package/dist/client/utils/preview-value.d.ts +2 -0
  27. package/dist/client/utils/preview-value.d.ts.map +1 -0
  28. package/dist/client/utils/preview-value.js +16 -0
  29. package/dist/client/utils/preview-value.js.map +1 -0
  30. package/dist/config.d.ts +28 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +57 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +8 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/server/config.d.ts +15 -0
  39. package/dist/server/config.d.ts.map +1 -0
  40. package/dist/server/config.js +18 -0
  41. package/dist/server/config.js.map +1 -0
  42. package/dist/server/error.d.ts +3 -0
  43. package/dist/server/error.d.ts.map +1 -0
  44. package/dist/server/error.js +9 -0
  45. package/dist/server/error.js.map +1 -0
  46. package/dist/server/getter.d.ts +35 -0
  47. package/dist/server/getter.d.ts.map +1 -0
  48. package/dist/server/getter.js +109 -0
  49. package/dist/server/getter.js.map +1 -0
  50. package/dist/server/index.d.ts +14 -0
  51. package/dist/server/index.d.ts.map +1 -0
  52. package/dist/server/index.js +73 -0
  53. package/dist/server/index.js.map +1 -0
  54. package/dist/server/routes/clear.d.ts +2 -0
  55. package/dist/server/routes/clear.d.ts.map +1 -0
  56. package/dist/server/routes/clear.js +15 -0
  57. package/dist/server/routes/clear.js.map +1 -0
  58. package/dist/server/routes/get-stale-list.d.ts +7 -0
  59. package/dist/server/routes/get-stale-list.d.ts.map +1 -0
  60. package/dist/server/routes/get-stale-list.js +20 -0
  61. package/dist/server/routes/get-stale-list.js.map +1 -0
  62. package/dist/server/routes/get-stale.d.ts +7 -0
  63. package/dist/server/routes/get-stale.d.ts.map +1 -0
  64. package/dist/server/routes/get-stale.js +20 -0
  65. package/dist/server/routes/get-stale.js.map +1 -0
  66. package/dist/server/routes/get.d.ts +9 -0
  67. package/dist/server/routes/get.d.ts.map +1 -0
  68. package/dist/server/routes/get.js +23 -0
  69. package/dist/server/routes/get.js.map +1 -0
  70. package/dist/server/routes/set.d.ts +9 -0
  71. package/dist/server/routes/set.d.ts.map +1 -0
  72. package/dist/server/routes/set.js +18 -0
  73. package/dist/server/routes/set.js.map +1 -0
  74. package/dist/server/setter.d.ts +12 -0
  75. package/dist/server/setter.d.ts.map +1 -0
  76. package/dist/server/setter.js +37 -0
  77. package/dist/server/setter.js.map +1 -0
  78. package/dist/server/storage/index.d.ts +6 -0
  79. package/dist/server/storage/index.d.ts.map +1 -0
  80. package/dist/server/storage/index.js +14 -0
  81. package/dist/server/storage/index.js.map +1 -0
  82. package/dist/server/storage/multi-instance/index.d.ts +9 -0
  83. package/dist/server/storage/multi-instance/index.d.ts.map +1 -0
  84. package/dist/server/storage/multi-instance/index.js +10 -0
  85. package/dist/server/storage/multi-instance/index.js.map +1 -0
  86. package/dist/server/storage/single-instance/fs.d.ts +16 -0
  87. package/dist/server/storage/single-instance/fs.d.ts.map +1 -0
  88. package/dist/server/storage/single-instance/fs.js +59 -0
  89. package/dist/server/storage/single-instance/fs.js.map +1 -0
  90. package/dist/server/storage/single-instance/index.d.ts +11 -0
  91. package/dist/server/storage/single-instance/index.d.ts.map +1 -0
  92. package/dist/server/storage/single-instance/index.js +33 -0
  93. package/dist/server/storage/single-instance/index.js.map +1 -0
  94. package/dist/server/storage/single-instance/memory.d.ts +15 -0
  95. package/dist/server/storage/single-instance/memory.d.ts.map +1 -0
  96. package/dist/server/storage/single-instance/memory.js +41 -0
  97. package/dist/server/storage/single-instance/memory.js.map +1 -0
  98. package/dist/server/storage/types.d.ts +14 -0
  99. package/dist/server/storage/types.d.ts.map +1 -0
  100. package/dist/server/storage/types.js +3 -0
  101. package/dist/server/storage/types.js.map +1 -0
  102. package/dist/server/utils/express.d.ts +8 -0
  103. package/dist/server/utils/express.d.ts.map +1 -0
  104. package/dist/server/utils/express.js +15 -0
  105. package/dist/server/utils/express.js.map +1 -0
  106. package/dist/shared/debug.d.ts +4 -0
  107. package/dist/shared/debug.d.ts.map +1 -0
  108. package/dist/shared/debug.js +14 -0
  109. package/dist/shared/debug.js.map +1 -0
  110. package/dist/shared/logger.d.ts +2 -0
  111. package/dist/shared/logger.d.ts.map +1 -0
  112. package/dist/shared/logger.js +5 -0
  113. package/dist/shared/logger.js.map +1 -0
  114. package/dist/shared/sig.d.ts +23 -0
  115. package/dist/shared/sig.d.ts.map +1 -0
  116. package/dist/shared/sig.js +53 -0
  117. package/dist/shared/sig.js.map +1 -0
  118. package/dist/shared/ttl.d.ts +8 -0
  119. package/dist/shared/ttl.d.ts.map +1 -0
  120. package/dist/shared/ttl.js +25 -0
  121. package/dist/shared/ttl.js.map +1 -0
  122. package/dist/shared/utils/object.d.ts +2 -0
  123. package/dist/shared/utils/object.d.ts.map +1 -0
  124. package/dist/shared/utils/object.js +9 -0
  125. package/dist/shared/utils/object.js.map +1 -0
  126. package/dist/shared/value-info.d.ts +18 -0
  127. package/dist/shared/value-info.d.ts.map +1 -0
  128. package/dist/shared/value-info.js +32 -0
  129. package/dist/shared/value-info.js.map +1 -0
  130. package/package.json +41 -0
  131. package/src/client/api.ts +51 -0
  132. package/src/client/index.ts +137 -0
  133. package/src/client/types.ts +14 -0
  134. package/src/client/utils/http-error.ts +8 -0
  135. package/src/client/utils/http-json.ts +32 -0
  136. package/src/client/utils/http-query.ts +24 -0
  137. package/src/client/utils/preview-value.ts +10 -0
  138. package/src/config.ts +79 -0
  139. package/src/index.ts +2 -0
  140. package/src/server/config.ts +29 -0
  141. package/src/server/error.ts +7 -0
  142. package/src/server/getter.ts +138 -0
  143. package/src/server/index.ts +81 -0
  144. package/src/server/routes/clear.ts +15 -0
  145. package/src/server/routes/get-stale-list.ts +27 -0
  146. package/src/server/routes/get-stale.ts +27 -0
  147. package/src/server/routes/get.ts +31 -0
  148. package/src/server/routes/set.ts +27 -0
  149. package/src/server/setter.ts +35 -0
  150. package/src/server/storage/index.ts +16 -0
  151. package/src/server/storage/multi-instance/index.ts +7 -0
  152. package/src/server/storage/single-instance/fs.ts +64 -0
  153. package/src/server/storage/single-instance/index.ts +32 -0
  154. package/src/server/storage/single-instance/memory.ts +43 -0
  155. package/src/server/storage/types.ts +13 -0
  156. package/src/server/utils/express.ts +14 -0
  157. package/src/shared/debug.ts +7 -0
  158. package/src/shared/logger.ts +1 -0
  159. package/src/shared/sig.ts +84 -0
  160. package/src/shared/ttl.ts +19 -0
  161. package/src/shared/utils/object.ts +4 -0
  162. package/src/shared/value-info.ts +40 -0
@@ -0,0 +1,27 @@
1
+ import { Express, Router } from 'express';
2
+ import { getConfig } from '../config';
3
+ import { TestRunValueInfo } from '../../shared/value-info';
4
+ import { Setter } from '../setter';
5
+ import { getStorage } from '../storage';
6
+
7
+ export const router = Router();
8
+
9
+ export type SetValueParams = {
10
+ key: string;
11
+ value?: unknown; // The value to set.
12
+ error?: string; // An error occured during value computing.
13
+ };
14
+
15
+ export type SetValueResponse = TestRunValueInfo;
16
+
17
+ router.post('/run/:runId/set', async (req, res) => {
18
+ const { runId } = req.params;
19
+ const { key, value, error } = req.body as SetValueParams;
20
+ const config = getConfig(req.app as Express);
21
+
22
+ const { testRunStorage, persistentStorage } = getStorage(config, runId);
23
+ const setter = new Setter(testRunStorage, persistentStorage);
24
+ const valueInfo = await setter.set({ key, value, error });
25
+
26
+ res.json(valueInfo);
27
+ });
@@ -0,0 +1,35 @@
1
+ import { setComputed, setError } from '../shared/value-info';
2
+ import { IPersistentStorage, ITestRunStorage } from './storage/types';
3
+
4
+ export class Setter {
5
+ constructor(
6
+ private testRunStorage: ITestRunStorage,
7
+ private persistentStorage: IPersistentStorage,
8
+ ) {}
9
+
10
+ // eslint-disable-next-line visual/complexity, max-statements
11
+ async set({ key, value, error }: { key: string; value?: unknown; error?: string }) {
12
+ const valueInfo = await this.testRunStorage.load(key);
13
+
14
+ // todo: convert to warnings?
15
+ if (!valueInfo) {
16
+ throw new Error(`Cannot set value for key "${key}" that is not loaded.`);
17
+ }
18
+ if (valueInfo.state !== 'computing') {
19
+ throw new Error(`Cannot set value for key "${key}" that is not in "computing" state.`);
20
+ }
21
+
22
+ if (error) {
23
+ setError(valueInfo, error);
24
+ await this.testRunStorage.save(valueInfo, { notify: true });
25
+ // We delete file on error, as error may be related to the invalid value.
26
+ if (valueInfo.persistent) await this.persistentStorage.delete(key);
27
+ } else {
28
+ setComputed(valueInfo, value);
29
+ await this.testRunStorage.save(valueInfo, { notify: true });
30
+ if (valueInfo.persistent) await this.persistentStorage.save(valueInfo);
31
+ }
32
+
33
+ return valueInfo;
34
+ }
35
+ }
@@ -0,0 +1,16 @@
1
+ import { GlobalCacheServerConfigResolved } from '../config';
2
+ import { getFsStorage, getMemoryStorage } from './single-instance';
3
+
4
+ export function getStorage(
5
+ { multiInstance, basePath }: GlobalCacheServerConfigResolved,
6
+ runId: string,
7
+ ) {
8
+ if (multiInstance) {
9
+ throw new Error('Multi-instance mode is not implemented yet.');
10
+ }
11
+
12
+ return {
13
+ testRunStorage: getMemoryStorage(runId),
14
+ persistentStorage: getFsStorage(basePath),
15
+ };
16
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Multi-instance mode of Global Storage Server.
3
+ * - can't reliably write to fs
4
+ * - can't wait for value via memory, as "set" request can land on another instance
5
+ * - both non-persistent and persistent values are stored in the external storage (redis/firebase)
6
+ * - compute-listeners are implemented via redis/firebase notification features
7
+ */
@@ -0,0 +1,64 @@
1
+ /**
2
+ * File system storage implementation.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { IPersistentStorage } from '../types';
7
+ import { TestRunValueInfo } from '../../../shared/value-info';
8
+
9
+ export type StoredInfo = {
10
+ computedAt: number;
11
+ sig: string;
12
+ value: unknown;
13
+ };
14
+
15
+ export class FilesystemStorage implements IPersistentStorage {
16
+ constructor(private basePath: string) {}
17
+
18
+ async load(key: string): Promise<TestRunValueInfo | undefined> {
19
+ const filePath = this.getFilePath(key);
20
+ const fileExists = fs.existsSync(filePath);
21
+ if (!fileExists) return;
22
+
23
+ const content = await fs.promises.readFile(filePath, 'utf8');
24
+ // todo: json parse safe (with warning)
25
+ const storedInfo: StoredInfo = JSON.parse(content);
26
+
27
+ return {
28
+ key,
29
+ state: 'computed',
30
+ persistent: true,
31
+ computedAt: storedInfo.computedAt,
32
+ value: storedInfo.value,
33
+ sig: storedInfo.sig,
34
+ };
35
+ }
36
+
37
+ async save({ key, value, sig }: TestRunValueInfo) {
38
+ const computedAt = Date.now();
39
+ const storedInfo: StoredInfo = { computedAt, sig, value };
40
+ const content = JSON.stringify(storedInfo, null, 2);
41
+ const filePath = this.getFilePath(key);
42
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
43
+ await fs.promises.writeFile(filePath, content, 'utf8');
44
+ }
45
+
46
+ async delete(key: string) {
47
+ const filePath = this.getFilePath(key);
48
+ if (fs.existsSync(filePath)) {
49
+ await fs.promises.rm(filePath);
50
+ }
51
+ }
52
+
53
+ private getFilePath(key: string) {
54
+ return path.join(this.basePath, `${filenamify(key)}.json`);
55
+ }
56
+ }
57
+
58
+ // eslint-disable-next-line no-control-regex
59
+ const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g;
60
+ function filenamify(s: string, replacement = '-') {
61
+ return s
62
+ .replace(filenameReservedRegex, replacement) // prettier-ignore
63
+ .replace(/\s+/g, replacement); // additionally replace spaces
64
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Single-instance mode of Global Storage Server.
3
+ * - non-persistent values are stored in memory
4
+ * - presistent values are stored on file system
5
+ * - compute-listeners are implemented via memory
6
+ */
7
+ import { MemoryStorage } from './memory';
8
+ import { FilesystemStorage } from './fs';
9
+
10
+ // In-memory storage of test runs with values
11
+ const memoryStorageMap = new Map<string, MemoryStorage>();
12
+ const fsStorageMap = new Map<string, FilesystemStorage>();
13
+
14
+ // todo: cleanup old testRuns from the map
15
+
16
+ export function getMemoryStorage(runId: string) {
17
+ let storage = memoryStorageMap.get(runId);
18
+ if (!storage) {
19
+ storage = new MemoryStorage();
20
+ memoryStorageMap.set(runId, storage);
21
+ }
22
+ return storage;
23
+ }
24
+
25
+ export function getFsStorage(basePath: string) {
26
+ let storage = fsStorageMap.get(basePath);
27
+ if (!storage) {
28
+ storage = new FilesystemStorage(basePath);
29
+ fsStorageMap.set(basePath, storage);
30
+ }
31
+ return storage;
32
+ }
@@ -0,0 +1,43 @@
1
+ import { TestRunValueInfo } from '../../../shared/value-info';
2
+ import { ITestRunStorage } from '../types';
3
+
4
+ type Listener = (valueInfo: TestRunValueInfo) => void;
5
+
6
+ export class MemoryStorage implements ITestRunStorage {
7
+ private values = new Map<string, TestRunValueInfo>();
8
+ private listeners = new Map<string /* key */, Listener[]>();
9
+
10
+ async load(key: string) {
11
+ return this.values.get(key);
12
+ }
13
+
14
+ async loadByPrefix(prefix: string) {
15
+ return [...this.values.values()].filter((valueInfo) => valueInfo.key.startsWith(prefix));
16
+ }
17
+
18
+ async save(valueInfo: TestRunValueInfo, { notify = false } = {}) {
19
+ this.values.set(valueInfo.key, valueInfo);
20
+ if (notify) this.notify(valueInfo);
21
+ }
22
+
23
+ async clear() {
24
+ this.values.clear();
25
+ }
26
+
27
+ async wait(key: string) {
28
+ return new Promise<TestRunValueInfo>((resolve) => {
29
+ const listeners = this.listeners.get(key) || [];
30
+ listeners.push(resolve);
31
+ this.listeners.set(key, listeners);
32
+ });
33
+ }
34
+
35
+ private notify(valueInfo: TestRunValueInfo) {
36
+ const listeners = this.listeners.get(valueInfo.key);
37
+ try {
38
+ listeners?.forEach((resolve) => resolve(valueInfo));
39
+ } finally {
40
+ this.listeners.delete(valueInfo.key);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,13 @@
1
+ import { TestRunValueInfo } from '../../shared/value-info';
2
+
3
+ export type ITestRunStorage = {
4
+ load(key: string): Promise<TestRunValueInfo | undefined>;
5
+ save(valueInfo: TestRunValueInfo, options?: { notify?: boolean }): Promise<void>;
6
+ wait(key: string): Promise<TestRunValueInfo>;
7
+ };
8
+
9
+ export type IPersistentStorage = {
10
+ load(key: string): Promise<TestRunValueInfo | undefined>;
11
+ save(valueInfo: TestRunValueInfo): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ };
@@ -0,0 +1,14 @@
1
+ import express from 'express';
2
+ import http from 'node:http';
3
+
4
+ /**
5
+ * Start server.
6
+ * See: https://github.com/nodejs/node/issues/21482#issuecomment-626025579
7
+ */
8
+ export async function startExpressServer(app: express.Express, port = 0) {
9
+ return new Promise<http.Server>((resolve, reject) => {
10
+ const server = app.listen(port);
11
+ server.once('listening', () => resolve(server));
12
+ server.once('error', reject);
13
+ });
14
+ }
@@ -0,0 +1,7 @@
1
+ import Debug from 'debug';
2
+
3
+ export const debug = Debug('global-cache');
4
+ export const debugKey = (key: string, message: string) => {
5
+ const debug = Debug(`global-cache:${key}`);
6
+ debug(message);
7
+ };
@@ -0,0 +1 @@
1
+ export const logger = console;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Calculate the calling signature for a given key:
3
+ * ttl + stack + computeFn body
4
+ *
5
+ * Signature is used to invalidate the key in case of changes in the calling code.
6
+ * Signature is computed on client and sent to the server.
7
+ * Server compares the signature with the stored one and invalidates the key if they differ.
8
+ */
9
+
10
+ import { TTL } from './ttl';
11
+
12
+ type SingatureParams = {
13
+ fn: Function; // eslint-disable-line @typescript-eslint/no-unsafe-function-type
14
+ stack: string;
15
+ ttl?: TTL;
16
+ };
17
+
18
+ type SignatureObj = {
19
+ fn: string;
20
+ stack: string;
21
+ ttl?: TTL;
22
+ };
23
+
24
+ export type SignatureMismatch = {
25
+ field: 'fn' | 'stack' | 'ttl' | 'sig';
26
+ value1: unknown;
27
+ value2: unknown;
28
+ };
29
+
30
+ export function calcSignature({ ttl, stack, fn }: SingatureParams) {
31
+ // todo: use hashing?
32
+ // todo: remove spaces and newlines from stack and computeFn?
33
+ const fnBody = fn.toString().replace(/[\n\s]+/g, ' ');
34
+ const obj: SignatureObj = { ttl, stack, fn: fnBody };
35
+
36
+ return JSON.stringify(obj);
37
+ }
38
+
39
+ // eslint-disable-next-line visual/complexity
40
+ export function checkSignature(
41
+ key: string,
42
+ storedSigStr: string,
43
+ currentSigStr: string,
44
+ ): SignatureMismatch | undefined {
45
+ const storedSig = parseSignatureSafe(storedSigStr);
46
+ const currentSig = parseSignatureSafe(currentSigStr);
47
+
48
+ // fallback to string comparison if parsing failed for any of the signatures
49
+ // todo: remove in the future releases
50
+ if (!storedSig || !currentSig) {
51
+ return storedSigStr !== currentSigStr
52
+ ? createSignatureMismatch('sig', storedSigStr, currentSigStr)
53
+ : undefined;
54
+ }
55
+
56
+ // checking stack first -> allows to navigate to the code by click in IDE.
57
+ if (storedSig.stack !== currentSig.stack) {
58
+ return createSignatureMismatch('stack', storedSig.stack, currentSig.stack);
59
+ }
60
+
61
+ if (storedSig.ttl !== currentSig.ttl) {
62
+ return createSignatureMismatch('ttl', storedSig.ttl, currentSig.ttl);
63
+ }
64
+
65
+ if (storedSig.fn !== currentSig.fn) {
66
+ return createSignatureMismatch('fn', storedSig.fn, currentSig.fn);
67
+ }
68
+ }
69
+
70
+ function createSignatureMismatch(
71
+ field: SignatureMismatch['field'],
72
+ value1: unknown,
73
+ value2: unknown,
74
+ ): SignatureMismatch {
75
+ return { field, value1, value2 };
76
+ }
77
+
78
+ function parseSignatureSafe(sigStr: string) {
79
+ try {
80
+ return JSON.parse(sigStr) as SignatureObj;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
@@ -0,0 +1,19 @@
1
+ import ms, { StringValue } from 'ms';
2
+
3
+ // time to live, can be a number of milliseconds, a string like "1h" or "infinite".
4
+ export type TTL = number | StringValue | 'infinite';
5
+
6
+ /**
7
+ * TTL can be a stringified number or a string like "1h" or "infinite".
8
+ */
9
+ export function parseTTL(ttl: string | number | undefined) {
10
+ if (ttl === '' || ttl === undefined) return undefined;
11
+ if (ttl === 'infinite') return -1;
12
+ const ttlNumber = Number(ttl);
13
+ return Number.isNaN(ttlNumber) ? ms(ttl as StringValue) : ttlNumber;
14
+ }
15
+
16
+ export function isExpired(computedAt: number | undefined, ttl: number) {
17
+ if (!computedAt || ttl === -1) return false;
18
+ return Date.now() > computedAt + ttl;
19
+ }
@@ -0,0 +1,4 @@
1
+ export function removeUndefined<T extends Record<string, unknown>>(obj?: T): T | undefined {
2
+ if (!obj) return;
3
+ return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)) as T;
4
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Value info in the scope of particular test run.
3
+ */
4
+
5
+ export type TestRunValueInfo = {
6
+ key: string;
7
+ sig: string;
8
+ state: 'computing' | 'computed' | 'error';
9
+ computedAt?: number;
10
+ value?: unknown;
11
+ persistent?: boolean; // indicates if the value is stored persistently (e.g., in file system)
12
+ prevValue?: unknown; // previous value (applicable for persistent keys, used for cleanup)
13
+ errorMessage?: string; // error message in case of 'error' states
14
+ };
15
+
16
+ export function updateValueInfo(valueInfo: TestRunValueInfo, props: Partial<TestRunValueInfo>) {
17
+ return Object.assign(valueInfo, props);
18
+ }
19
+
20
+ export function setComputed(valueInfo: TestRunValueInfo, value: unknown) {
21
+ updateValueInfo(valueInfo, {
22
+ state: 'computed',
23
+ value,
24
+ computedAt: Date.now(),
25
+ errorMessage: undefined,
26
+ });
27
+ }
28
+
29
+ export function setError(valueInfo: TestRunValueInfo, errorMessage: string) {
30
+ updateValueInfo(valueInfo, {
31
+ state: 'error',
32
+ errorMessage,
33
+ value: undefined,
34
+ computedAt: undefined,
35
+ });
36
+ }
37
+
38
+ export function getStaleValue(valueInfo?: TestRunValueInfo | null) {
39
+ return valueInfo?.persistent ? valueInfo.prevValue : valueInfo?.value;
40
+ }