@vercel/flags-core 1.9.0 → 1.10.0-6128616-20261001141857

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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # @vercel/flags-core
2
2
 
3
+ ## 1.10.0-6128616-20261001141857
4
+
5
+ ### Minor Changes
6
+
7
+ - [#511](https://github.com/vercel/flags/pull/511) [`88c323c`](https://github.com/vercel/flags/commit/88c323c9539661fef385f1381ff7e3e89776fdc7) Thanks [@luismeyer](https://github.com/luismeyer)! - Add a header-driven `vercel` client mode, enabled by default when `VERCEL=1`. Initialization loads provided/bundled definitions; request versions trigger refreshes and an empty cache fetches on its first read. Explicit offline/build behavior is preserved.
8
+
9
+ Evaluations and `getDatafile()` require a valid positive version header for their own project. Missing, empty, malformed, or unrelated project entries permanently start streaming when enabled, otherwise polling; multiple clients select their sources independently. Concurrent reads share startup and pending HTTP refreshes. Accepted stream updates and valid confirmations cancel superseded refreshes; waiting reads use the confirmed cache, and late responses cannot change cache or authorization state. `getDatafile()` shares lazy initialization and the evaluation resolution path, including header assessment, SWR, blocking refresh, stale-if-error, and source fallback. Initialization alone does not trigger this switch. A cold shared fetch discovers project identity before accepting header evidence; failed discovery starts source fallback.
10
+
11
+ The controller supplies a source freshness-status callback to the cache and configures one shared fetch callback. HeaderSource keeps version observations; the cache handles serving, background/blocking refreshes, shared work, cancellation, and stale-if-error. The cache owns age and resets it on accepted updates or confirmations, without rewriting `fetchedAt`. Streaming becomes stale after 60 seconds and expires after 90 seconds. Polling becomes stale after its interval plus the 10-second fetch deadline and expires after two intervals plus that deadline (40/70 seconds by default). Stale reads refresh in the background; expired reads block on the shared refresh. Stream pings reset age and clear failures. Polling initialization honors its configured timeout while preserving the pending poll and interval; cached fallback does not renew cache age or stale-if-error.
12
+
13
+ Ping timeouts reconnect the stream internally, allowing suspended runtimes to resume without starting polling. Reconnection alone does not renew cache freshness. Connection errors, server closure, and exhausted retries trigger an immediate background poll followed by interval polling. Reads and polling share pending HTTP refreshes; stream recovery stops polling.
14
+
15
+ Use `staleWhileRevalidate` (default 10) and `staleIfError` (default Infinity) in **seconds**, including fractions. Setting either to `0` disables its stale allowance. `staleWhileRevalidate` controls header-driven refreshes; stream/poll freshness follows their update schedules. Datafiles preserve optional `fetchedAt` epoch-millisecond timestamps across serialization and bundled/provided reuse.
16
+
17
+ - [#540](https://github.com/vercel/flags/pull/540) [`1a38dee`](https://github.com/vercel/flags/commit/1a38dee43b41030496bd63290816e06e5d5890ae) Thanks [@vincent-derks](https://github.com/vincent-derks)! - Remove `projectId` from connection strings.
18
+
19
+ - [#510](https://github.com/vercel/flags/pull/510) [`3ef9a2e`](https://github.com/vercel/flags/commit/3ef9a2e8c99905142b1d349fa2d68ffa06e04445) Thanks [@luismeyer](https://github.com/luismeyer)! - Add `staleIfError` in seconds to bound cached runtime reads after the first consecutive stream/poll failure or stream disconnect. The default `Infinity` preserves unlimited fallback; finite nonnegative durations (including fractional seconds) use existing evaluation defaults and errors after expiry, and `getDatafile()` follows the same allowance. Accepted updates, valid equal-version responses, or matching stream primed revisions reset the allowance. Storing fallback data does not confirm freshness or renew the failure clock. Build/offline behavior and source scheduling remain unchanged.
20
+
21
+ ### Patch Changes
22
+
23
+ - [#518](https://github.com/vercel/flags/pull/518) [`7c434bf`](https://github.com/vercel/flags/commit/7c434bf866cf58b860345b717021af382b481862) Thanks [@luismeyer](https://github.com/luismeyer)! - Add detailed client lifecycle, cache freshness, and network diagnostics using the existing `DEBUG=@vercel/flags-core` environment variable. A shared global logger emits events without passing logger instances through the client. Every client diagnostic includes the configured `clientName`, including cache, source, and background network activity. Logs omit credentials, definitions, and raw errors.
24
+
25
+ State changes include their cause; refresh logs distinguish response receipt, cache application, shared work, stream confirmation, and shutdown cancellation. Startup and source logs expose timeout and freshness thresholds, and recovery is logged only when a failure clears.
26
+
27
+ - [#511](https://github.com/vercel/flags/pull/511) [`1971366`](https://github.com/vercel/flags/commit/1971366423fa61bf5c99023a0a946d79d5bf057e) Thanks [@luismeyer](https://github.com/luismeyer)! - Record `fetchedAt` when a datafile fetch completes and preserve it in generated flag definitions. Loading the bundle retains the original timestamp so the Flags SDK can determine its age.
28
+
29
+ Expose optional `fetchedAt` metadata on datafiles. Record it for accepted live updates and preserve valid timestamps when loading provided or bundled definitions, without mutating the input.
30
+
3
31
  ## 1.9.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -33,6 +33,101 @@ export default app;
33
33
 
34
34
  Outside Vercel, pass an SDK key explicitly: `createClient(process.env.FLAGS)`.
35
35
 
36
+ ## Header-driven reads on Vercel
37
+
38
+ When `VERCEL=1`, the client defaults to `vercel: true`. Initialization loads provided
39
+ or bundled definitions without starting a stream or polling. Request version headers
40
+ indicate when cached definitions need refreshing. Header mode requires a valid positive
41
+ version for the client’s own `projectId`. Missing, empty, malformed, or unrelated entries
42
+ permanently switch that client to streaming when enabled, otherwise polling. Clients
43
+ also switch when cached definitions have no valid positive config version to compare.
44
+ Clients with different projects select their sources independently within the same request.
45
+ Concurrent reads share that startup and later headers do
46
+ not switch the client back. Pending HTTP refreshes remain shared until the stream
47
+ delivers current data or confirms the cached version. That confirmation cancels the
48
+ superseded refresh, and waiting reads use the confirmed cache; late responses cannot
49
+ change cache or authorization state. With an empty cache, the first read uses a shared
50
+ fetch to load definitions and discover the client’s project. The next read assesses
51
+ that project’s header entry and starts the stream/poll fallback if it is unavailable.
52
+ If the cold fetch fails, the read rejects without switching sources. Fetch failures
53
+ use cached data only while stale-if-error permits it; source assessment errors start fallback.
54
+
55
+ ```ts
56
+ const client = createClient(process.env.FLAGS!, {
57
+ vercel: true,
58
+ staleWhileRevalidate: 10, // Seconds of background-refresh grace.
59
+ staleIfError: 60, // Seconds of cached fallback after a refresh failure.
60
+ });
61
+ ```
62
+
63
+ `staleWhileRevalidate` defaults to 10 seconds and accepts finite, nonnegative values,
64
+ including fractions. `0` makes header-driven refreshes block. The window starts at the latest
65
+ accepted fetch or valid confirmation, including an equal-version fetch response.
66
+ The cache tracks this age independently of `fetchedAt`. Bundled/provided definitions
67
+ preserve their original `fetchedAt`; unknown or expired cache age requires a blocking
68
+ refresh when a newer request version arrives. Refresh failures use `staleIfError`.
69
+ A newer-header read attempts blocking recovery after that failure allowance expires.
70
+
71
+ `getDatafile()` uses the same lazy initialization and resolution path as evaluations,
72
+ including header checks, background revalidation, blocking refresh, stale-if-error, and
73
+ source fallback. Concurrent calls share HTTP refreshes across both APIs.
74
+ Use `vercel: false` to select the stream/poll behavior. Disabling both
75
+ stream and polling still selects offline mode, and builds retain their existing loading.
76
+
77
+ ## Cached reads after errors
78
+
79
+ `staleIfError` controls how many seconds evaluations and `getDatafile()` may use
80
+ cached flag definitions after a stream/poll/header-refresh failure or stream disconnect:
81
+
82
+ ```ts
83
+ const client = createClient(process.env.FLAGS!, {
84
+ staleIfError: 60,
85
+ });
86
+ ```
87
+
88
+ The default is `Infinity`, preserving unlimited cached fallback. Use a finite
89
+ nonnegative number of seconds to bound fallback. Fractional seconds are supported
90
+ (for example, `0.5` allows 500 milliseconds). A positive window includes its exact
91
+ deadline; `0` disables cached fallback immediately after failure.
92
+ Negative values, `NaN`, and negative infinity throw when creating the client.
93
+
94
+ The allowance starts at the first consecutive failure. Repeated errors,
95
+ disconnects, and provided or bundled fallback data do not renew it. An accepted
96
+ source update, or a finite equal version for the same project and environment,
97
+ clears the outage. A stream `primed` message also clears it when its finite numeric
98
+ revision and identity match the cached entry. Pings clear failures too: the server
99
+ sends `primed` or a datafile before pings on each connection. Opening a connection
100
+ alone does not clear a failure. A later failure starts a new allowance.
101
+ Responses are observed in completion order, with existing version acceptance.
102
+
103
+ After expiry, `evaluate()` returns the caller's default with reason `error`, or
104
+ throws the first failure when no default is supplied. `bulkEvaluate()` returns
105
+ an error result for each requested flag, with its default value when provided.
106
+ `getDatafile()` follows the same allowance and throws after expiry. The entry is
107
+ retained for recovery, including its revision for stream reconnection. A clean
108
+ stream close records `stream: disconnected` if no earlier failure exists. Ping timeouts
109
+ reconnect quietly without recording a failure or starting polling, including after runtime
110
+ suspension. Genuine disconnections start an immediate poll, sharing pending read
111
+ refreshes, then continue at the configured interval. Every transition to polling
112
+ waits for the first poll or `polling.initTimeoutMs`, including reads with cached data.
113
+ On timeout, reads follow `staleIfError` while polling continues. A zero initialization
114
+ timeout waits for the poll, which still has a ten-second fetch deadline. Stream
115
+ recovery stops polling. `getFallbackDatafile()` remains an independent bundled-data export.
116
+
117
+ Streaming data becomes stale after 60 seconds and expires after 90 seconds, allowing
118
+ one missed 30-second ping before revalidation and matching the stream's ping
119
+ timeout. Polling data becomes stale after its interval plus the 10-second fetch
120
+ deadline, and expires after two intervals plus that deadline (40 and 70 seconds with
121
+ the default 30-second interval). These windows are independent of `staleWhileRevalidate`.
122
+ Stale evaluations and `getDatafile()` calls refresh in the background; expired reads
123
+ wait for the shared refresh. Refresh failures still follow `staleIfError`.
124
+
125
+ Accepted updates and valid confirmations reset cache age without rewriting `fetchedAt`.
126
+ Stream pings also reset age and clear any failure. Poll errors feed the shared failure
127
+ handler without logging each failed poll. An initialization timeout alone does not
128
+ start the failure allowance or reset cache age. It permits cached fallback while
129
+ the pending update continues; polling intervals remain active after startup timeout.
130
+
36
131
  ## Evaluation Metrics
37
132
 
38
133
  To associate evaluation metrics with an environment, pass the
@@ -64,3 +159,64 @@ const client = OpenFeature.getClient();
64
159
  - [Core Library Docs](https://vercel.com/docs/flags/vercel-flags/sdks/core)
65
160
  - [OpenFeature Provider Docs](https://vercel.com/docs/flags/vercel-flags/sdks/openfeature)
66
161
  - [Vercel Flags](https://vercel.com/docs/flags/vercel-flags)
162
+
163
+ ### Client debug logging
164
+
165
+ Set `DEBUG=@vercel/flags-core` to enable detailed diagnostics. This reuses the
166
+ existing ingest debug switch:
167
+
168
+ ```sh
169
+ DEBUG=@vercel/flags-core pnpm dev
170
+ ```
171
+
172
+ Client diagnostics use `console.debug` with the `@vercel/flags-core` prefix and
173
+ an object containing an `event`, the configured `clientName`, and its diagnostic
174
+ details. Set `clientName` when creating each client to distinguish their logs:
175
+
176
+ ```ts
177
+ const client = createClient(undefined, { clientName: 'checkout' });
178
+ ```
179
+
180
+ The controller, cache, and network sources all use the same global logging
181
+ function, including the client name on background work and reconnects. For example:
182
+
183
+ ```text
184
+ @vercel/flags-core { event: 'client.state', clientName: 'checkout', from: 'idle', to: 'vercel', reason: 'header-mode-enabled', ... }
185
+ @vercel/flags-core { event: 'cache.freshness', clientName: 'checkout', status: 'expired', revision: 42, ageMs: 15000, ... }
186
+ @vercel/flags-core { event: 'cache.refresh.blocking', clientName: 'checkout', reason: 'expired', ... }
187
+ @vercel/flags-core { event: 'datafile.fetch.attempt', clientName: 'checkout', attempt: 1, maxAttempts: 3 }
188
+ @vercel/flags-core { event: 'datafile.fetch.response', clientName: 'checkout', attempt: 1, status: 200 }
189
+ @vercel/flags-core { event: 'cache.update.accepted', clientName: 'checkout', revision: 43, ... }
190
+ @vercel/flags-core { event: 'cache.fetch.applied', clientName: 'checkout', revision: 43, ... }
191
+ @vercel/flags-core { event: 'client.read', clientName: 'checkout', state: 'vercel', cacheStatus: 'MISS', ... }
192
+ ```
193
+
194
+ Follow `client.state` for the previous state, next state, and transition reason.
195
+ `state` describes the internal lifecycle; `mode` is the public operating mode.
196
+ Read and cache events include the project, revision, and cache age when available.
197
+
198
+ | Event | What it explains |
199
+ | --- | --- |
200
+ | `cache.freshness` | The source's age/version assessment and whether failure policy permits serving the entry. |
201
+ | `cache.source-error` / `header.fallback` | The source cannot assess this project or its cached version; the controller starts fallback without changing the fetch-failure deadline. |
202
+ | `cache.refresh.background` / `.blocking` | Why a read returns stale data immediately or waits for refresh. |
203
+ | `cache.fetch.shared` | A caller joins an existing HTTP refresh instead of starting another. |
204
+ | `datafile.fetch.complete` / `cache.fetch.applied` | The response has been parsed / processed by the cache version guard. Only the latter finishes cache refresh work; an older response may be ignored. |
205
+ | `cache.fetch.cancel` / `cache.refresh.superseded` | A stream update or confirmation supersedes HTTP work / releases a waiting read using the confirmed cache. |
206
+ | `cache.fetch.aborted` | `source-confirmed` distinguishes superseded work from `cache-cleared` during shutdown. |
207
+ | `cache.stale-if-error.expired` / `cache.recovered` | The failure allowance prevents serving data / an actual failure has cleared. Ordinary confirmations do not log recovery. |
208
+ | `stream.initialize.timeout` / `poll.initialize.timeout` | Startup reached its deadline; `hasData` and `startupFallback` show whether cached fallback is available. |
209
+
210
+ Events cover initialization and shutdown, selected modes and state transitions,
211
+ evaluation/getDatafile results, cache versions and age, version acceptance/confirmation,
212
+ header timestamps, background/blocking/shared refreshes, stale-if-error expiry,
213
+ HTTP attempts and retry delays, polling, stream pings, timeouts, and reconnect reasons.
214
+ Stream/poll startup events include their stale and expired age thresholds.
215
+ Cache ages and delays are in milliseconds; `Infinity` denotes unknown age or an
216
+ unlimited stale-if-error window. The new client diagnostics omit credentials,
217
+ raw headers, flag definitions, evaluation entities, and raw error messages.
218
+
219
+ Logging is off by default. The logger checks `DEBUG` on each call; removing the
220
+ namespace disables client diagnostics, including for existing clients. Diagnostics
221
+ are verbose, including an event for each read, and event names/fields are internal
222
+ rather than a stable API. Configure your log collector to include `console.debug` output.