@vercel/flags-core 1.9.0 → 1.10.0-4925dc8-20261001115512

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-4925dc8-20261001115512
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
+ An evaluation with a missing or empty version header permanently starts streaming when enabled, otherwise polling. 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. Initialization and snapshot reads do not trigger this switch.
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 evaluations refresh in the background; expired evaluations 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 still trigger the existing polling fallback.
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) [`0810c82`](https://github.com/vercel/flags/commit/0810c82c42250602d083c2ce9a30f29c78188a44) 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. 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,87 @@ 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. If an evaluation has no version
41
+ header (or an empty one), the client permanently switches to streaming when enabled,
42
+ otherwise polling. Concurrent new evaluations share that startup and later headers do
43
+ not switch the client back. Pending HTTP refreshes remain shared until the stream
44
+ delivers current data or confirms the cached version. That confirmation cancels the
45
+ superseded refresh, and waiting reads use the confirmed cache; late responses cannot
46
+ change cache or authorization state. A present but malformed or unrelated header keeps the
47
+ existing cached-read behavior, fetching only when the cache is empty.
48
+
49
+ ```ts
50
+ const client = createClient(process.env.FLAGS!, {
51
+ vercel: true,
52
+ staleWhileRevalidate: 10, // Seconds of background-refresh grace.
53
+ staleIfError: 60, // Seconds of cached fallback after a refresh failure.
54
+ });
55
+ ```
56
+
57
+ `staleWhileRevalidate` defaults to 10 seconds and accepts finite, nonnegative values,
58
+ including fractions. `0` makes header-driven refreshes block. The window starts at the latest
59
+ accepted fetch or valid confirmation, including an equal-version fetch response.
60
+ The cache tracks this age independently of `fetchedAt`. Bundled/provided definitions
61
+ preserve their original `fetchedAt`; unknown or expired cache age requires a blocking
62
+ refresh when a newer request version arrives. Refresh failures use `staleIfError`.
63
+ A newer-header read attempts blocking recovery after that failure allowance expires.
64
+
65
+ `getDatafile()` remains a snapshot read: it applies stale-if-error but does not inspect
66
+ headers. Use `vercel: false` to select the existing stream/poll behavior. Disabling both
67
+ stream and polling still selects offline mode, and builds retain their existing loading.
68
+
69
+ ## Cached reads after errors
70
+
71
+ `staleIfError` controls how many seconds evaluations and `getDatafile()` may use
72
+ cached flag definitions after a stream/poll/header-refresh failure or stream disconnect:
73
+
74
+ ```ts
75
+ const client = createClient(process.env.FLAGS!, {
76
+ staleIfError: 60,
77
+ });
78
+ ```
79
+
80
+ The default is `Infinity`, preserving unlimited cached fallback. Use a finite
81
+ nonnegative number of seconds to bound fallback. Fractional seconds are supported
82
+ (for example, `0.5` allows 500 milliseconds). A positive window includes its exact
83
+ deadline; `0` disables cached fallback immediately after failure.
84
+ Negative values, `NaN`, and negative infinity throw when creating the client.
85
+
86
+ The allowance starts at the first consecutive failure. Repeated errors,
87
+ disconnects, and provided or bundled fallback data do not renew it. An accepted
88
+ source update, or a finite equal version for the same project and environment,
89
+ clears the outage. A stream `primed` message also clears it when its finite numeric
90
+ revision and identity match the cached entry. Pings clear failures too: the server
91
+ sends `primed` or a datafile before pings on each connection. Opening a connection
92
+ alone does not clear a failure. A later failure starts a new allowance.
93
+ Responses are observed in completion order, with existing version acceptance.
94
+
95
+ After expiry, `evaluate()` returns the caller's default with reason `error`, or
96
+ throws the first failure when no default is supplied. `bulkEvaluate()` returns
97
+ an error result for each requested flag, with its default value when provided.
98
+ `getDatafile()` follows the same allowance and throws after expiry. The entry is
99
+ retained for recovery, including its revision for stream reconnection. A clean
100
+ stream close or ping timeout records `stream: disconnected` if no earlier failure
101
+ exists. `getFallbackDatafile()` remains an independent bundled-data export.
102
+
103
+ Streaming data becomes stale after 60 seconds and expires after 90 seconds, allowing
104
+ one missed 30-second ping before revalidation and matching the stream's disconnect
105
+ timeout. Polling data becomes stale after its interval plus the 10-second fetch
106
+ deadline, and expires after two intervals plus that deadline (40 and 70 seconds with
107
+ the default 30-second interval). These windows are independent of `staleWhileRevalidate`.
108
+ Stale evaluations refresh in the background; expired evaluations wait for the shared
109
+ refresh. Refresh failures still follow `staleIfError`.
110
+
111
+ Accepted updates and valid confirmations reset cache age without rewriting `fetchedAt`.
112
+ Stream pings also reset age and clear any failure. Poll errors feed the shared failure
113
+ handler without logging each failed poll. An initialization timeout alone does not
114
+ start the failure allowance or reset cache age. It permits cached fallback while
115
+ the pending update continues; polling intervals remain active after startup timeout.
116
+
36
117
  ## Evaluation Metrics
37
118
 
38
119
  To associate evaluation metrics with an environment, pass the
@@ -64,3 +145,56 @@ const client = OpenFeature.getClient();
64
145
  - [Core Library Docs](https://vercel.com/docs/flags/vercel-flags/sdks/core)
65
146
  - [OpenFeature Provider Docs](https://vercel.com/docs/flags/vercel-flags/sdks/openfeature)
66
147
  - [Vercel Flags](https://vercel.com/docs/flags/vercel-flags)
148
+
149
+ ### Client debug logging
150
+
151
+ Set `DEBUG=@vercel/flags-core` to enable detailed diagnostics. This reuses the
152
+ existing ingest debug switch:
153
+
154
+ ```sh
155
+ DEBUG=@vercel/flags-core pnpm dev
156
+ ```
157
+
158
+ Client diagnostics use `console.debug` with the `@vercel/flags-core` prefix and
159
+ an object containing an `event` and its diagnostic details. The controller, cache,
160
+ and network sources all use the same global logging function. For example:
161
+
162
+ ```text
163
+ @vercel/flags-core { event: 'client.state', from: 'idle', to: 'vercel', reason: 'header-mode-enabled', ... }
164
+ @vercel/flags-core { event: 'cache.freshness', status: 'expired', revision: 42, ageMs: 15000, ... }
165
+ @vercel/flags-core { event: 'cache.refresh.blocking', reason: 'expired', ... }
166
+ @vercel/flags-core { event: 'datafile.fetch.attempt', attempt: 1, maxAttempts: 3 }
167
+ @vercel/flags-core { event: 'datafile.fetch.response', attempt: 1, status: 200 }
168
+ @vercel/flags-core { event: 'cache.update.accepted', revision: 43, ... }
169
+ @vercel/flags-core { event: 'cache.fetch.applied', revision: 43, ... }
170
+ @vercel/flags-core { event: 'client.read', state: 'vercel', cacheStatus: 'MISS', ... }
171
+ ```
172
+
173
+ Follow `client.state` for the previous state, next state, and transition reason.
174
+ `state` describes the internal lifecycle; `mode` is the public operating mode.
175
+ Read and cache events include the project, revision, and cache age when available.
176
+
177
+ | Event | What it explains |
178
+ | --- | --- |
179
+ | `cache.freshness` | The source's age/version assessment and whether failure policy permits serving the entry. |
180
+ | `cache.refresh.background` / `.blocking` | Why a read returns stale data immediately or waits for refresh. |
181
+ | `cache.fetch.shared` | A caller joins an existing HTTP refresh instead of starting another. |
182
+ | `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. |
183
+ | `cache.fetch.cancel` / `cache.refresh.superseded` | A stream update or confirmation supersedes HTTP work / releases a waiting read using the confirmed cache. |
184
+ | `cache.fetch.aborted` | `source-confirmed` distinguishes superseded work from `cache-cleared` during shutdown. |
185
+ | `cache.stale-if-error.expired` / `cache.recovered` | The failure allowance prevents serving data / an actual failure has cleared. Ordinary confirmations do not log recovery. |
186
+ | `stream.initialize.timeout` / `poll.initialize.timeout` | Startup reached its deadline; `hasData` and `startupFallback` show whether cached fallback is available. |
187
+
188
+ Events cover initialization and shutdown, selected modes and state transitions,
189
+ read/snapshot results, cache versions and age, version acceptance/confirmation,
190
+ header timestamps, background/blocking/shared refreshes, stale-if-error expiry,
191
+ HTTP attempts and retry delays, polling, stream pings, timeouts, and reconnect reasons.
192
+ Stream/poll startup events include their stale and expired age thresholds.
193
+ Cache ages and delays are in milliseconds; `Infinity` denotes unknown age or an
194
+ unlimited stale-if-error window. The new client diagnostics omit credentials,
195
+ raw headers, flag definitions, evaluation entities, and raw error messages.
196
+
197
+ Logging is off by default. The logger checks `DEBUG` on each call; removing the
198
+ namespace disables client diagnostics, including for existing clients. Diagnostics
199
+ are verbose, including an event for each read, and event names/fields are internal
200
+ rather than a stable API. Configure your log collector to include `console.debug` output.