-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathCacheInitializer.ts
More file actions
118 lines (105 loc) · 3.9 KB
/
CacheInitializer.ts
File metadata and controls
118 lines (105 loc) · 3.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { Context, Crypto, internal, LDLogger, Storage } from '@launchdarkly/js-sdk-common';
import { readFreshness } from '../../storage/freshness';
import { loadCachedFlags } from '../../storage/loadCachedFlags';
import { Flag } from '../../types';
import {
changeSet,
errorInfoFromUnknown,
FDv2SourceResult,
interrupted,
shutdown,
} from './FDv2SourceResult';
import { Initializer } from './Initializer';
import { InitializerFactory } from './SourceManager';
/**
* Configuration for creating a cache initializer.
*/
export interface CacheInitializerConfig {
/** Platform storage for reading cached data. */
storage: Storage | undefined;
/** Platform crypto for computing storage keys. */
crypto: Crypto;
/** Environment namespace (hashed SDK key). */
environmentNamespace: string;
/** The context to load cached data for. */
context: Context;
/** Optional logger. */
logger?: LDLogger;
}
/**
* Strips the `version` field from a stored {@link Flag} to produce the
* `FlagEvaluationResult` shape expected in an FDv2 `Update.object`.
*
* The version is carried on the `Update` envelope, not on the object itself.
*/
function flagToEvaluationResult(flag: Flag): Omit<Flag, 'version'> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { version, ...evalResult } = flag;
return evalResult;
}
/**
* Reads cached flag data and freshness from platform storage and returns
* them as an {@link FDv2SourceResult}.
*/
async function loadFromCache(config: CacheInitializerConfig): Promise<FDv2SourceResult> {
const { storage, crypto, environmentNamespace, context, logger } = config;
if (!storage) {
logger?.debug('No storage available for cache initializer');
return interrupted(errorInfoFromUnknown('No storage available'), false);
}
const cached = await loadCachedFlags(storage, crypto, environmentNamespace, context, logger);
if (!cached) {
logger?.debug('Cache miss for context');
return interrupted(errorInfoFromUnknown('Cache miss'), false);
}
const updates: internal.Update[] = Object.entries(cached.flags).map(
([key, flag]): internal.Update => ({
kind: 'flag-eval',
key,
version: flag.version,
object: flagToEvaluationResult(flag),
}),
);
const payload: internal.Payload = {
id: 'cache',
version: 0,
// No `state` field. The orchestrator sees a changeSet without a selector,
// records dataReceived=true, and continues to the next initializer.
type: 'full',
updates,
};
const freshness = await readFreshness(storage, crypto, environmentNamespace, context, logger);
logger?.debug('Loaded cached flag evaluations via cache initializer');
return changeSet(payload, false, undefined, freshness);
}
/**
* Creates an {@link InitializerFactory} that produces cache initializers.
*
* The cache initializer reads flag data and freshness from persistent storage
* for the given context and returns them as a changeSet without a selector.
* This allows the orchestrator to provide cached data immediately while
* continuing to the next initializer for network-verified data.
*
* Per spec Requirement 4.1.2, the payload has `persist=false` semantics
* (no selector) so the consuming layer should not re-persist it.
*
* @internal
*/
export function createCacheInitializerFactory(config: CacheInitializerConfig): InitializerFactory {
// The selectorGetter is ignored — cache data has no selector.
return (_selectorGetter: () => string | undefined): Initializer => {
let shutdownResolve: ((result: FDv2SourceResult) => void) | undefined;
const shutdownPromise = new Promise<FDv2SourceResult>((resolve) => {
shutdownResolve = resolve;
});
return {
async run(): Promise<FDv2SourceResult> {
return Promise.race([shutdownPromise, loadFromCache(config)]);
},
close(): void {
shutdownResolve?.(shutdown());
shutdownResolve = undefined;
},
};
};
}