Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bb72353
[flags] avoid re-imports
dferber90 Apr 1, 2026
ecfcc32
[flags] avoid iife microtask queue overhead
dferber90 Apr 1, 2026
df5281a
[flags] skip awaits where possible
dferber90 Apr 1, 2026
f693413
[flags] allow bulk eval
dferber90 Apr 1, 2026
fe9605a
wip
dferber90 May 13, 2026
099fda8
add bulk mode to playground
dferber90 May 13, 2026
aa0dfba
first try of bulk eval
dferber90 May 13, 2026
7cb1aeb
add bulk evaluation to adapters
dferber90 May 13, 2026
3af7129
Merge branch 'main' into bulk
dferber90 May 13, 2026
ea6d164
reuse
dferber90 May 14, 2026
cb04cde
simplify
dferber90 May 19, 2026
7fd2e5e
versions
dferber90 May 19, 2026
6321ab5
Merge branch 'main' into bulk
dferber90 May 19, 2026
e475198
rm outdated changeset
dferber90 May 19, 2026
2e1b113
rm outdated changeset
dferber90 May 19, 2026
930a9e0
revert playground
dferber90 May 19, 2026
35e518b
revert playground flags
dferber90 May 19, 2026
e076dd0
rm logs
dferber90 May 19, 2026
7a6c28d
reuse cache of resolved flags for bulk
dferber90 May 19, 2026
a576379
simplify
dferber90 May 19, 2026
5b2dc37
changesets
dferber90 May 19, 2026
380d725
support bulk([]) and bulk({})
dferber90 May 19, 2026
66b3a23
flagKey → key
dferber90 May 19, 2026
bf80336
allow any type in expectPermutations
dferber90 May 20, 2026
c65406b
avoid unnecessary mapping
dferber90 May 25, 2026
9575a4a
fix changeset
dferber90 May 25, 2026
254480e
validate package.json fields
dferber90 May 25, 2026
2225248
update package.json fields
dferber90 May 25, 2026
581a9c5
Merge branch 'main' into bulk
dferber90 May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/adapter-bulk-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@flags-sdk/vercel': minor
---

Faster evaluation of flags when using the Vercel adapter via `bulk()`.

This version of `flags-sdk/vercel` implements `bulkDecide` on the Vercel Flags adapter so flags can be evaluated together via `bulk()` from `flags/next`.

This improves performance by avoiding the per-flag overhead of separate `evaluate()` calls. We've seen a 10x improvement in evaluation time for large batches of flags.
20 changes: 20 additions & 0 deletions .changeset/core-bulk-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@vercel/flags-core': minor
---

Add `bulkEvaluate` method to `FlagsClient` for resolving multiple flags against shared entities in a single call.

```ts
const results = await client.bulkEvaluate(
[
{ key: 'a', defaultValue: false },
{ key: 'b', defaultValue: 'off' },
],
entities,
);

results.a; // EvaluationResult<boolean>
results.b; // EvaluationResult<string>
```

Avoids the per-flag overhead of separate `evaluate()` calls — the datafile is read once, entities are resolved once, and all flags share the same environment/segments lookup. Each entry in the returned record is a full `EvaluationResult` with `value`, `reason`, `outcomeType`, and `metrics`.
18 changes: 18 additions & 0 deletions .changeset/flags-bulk-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'flags': minor
---

Add `bulk()` function for evaluating multiple flags in a single call from the `flags/next` entry point.

```tsx
import { bulk } from 'flags/next';
import { flagA, flagB } from '../flags';

// pass a list of flags
const [valueA, valueB] = await bulk([flagA, flagB]);

// pass an object
const { a, b } = await bulk({ a: flagA, b: flagB });
```

Adapters can now opt into batched evaluation by implementing an optional `bulkDecide` method and setting a stable `adapterId`. When both are present, `bulk()` groups flags that share the same `adapterId` and `identify` source and invokes `bulkDecide` once per group instead of calling `decide` per flag. Flags without a bulk-capable adapter (or with an inline `decide`) still resolve through the normal per-flag path inside `bulk()` and benefit from the shared per-request headers, cookies, and overrides reads.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"shirt-shop": "pnpm dev -F shirt-shop",
"shirt-shop-api": "pnpm dev -F shirt-shop-api",
"snippets": "pnpm dev -F snippets",
"playground": "pnpm dev -F playground",
"svelte": "pnpm dev -F svelte-example",
"test": "turbo test",
"test:e2e": "turbo test:e2e",
Expand Down
66 changes: 66 additions & 0 deletions packages/adapter-vercel/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,72 @@ describe('createVercelAdapter', () => {
.toHaveProperty('entities')
.toEqualTypeOf<SampleEvaluationContext | undefined>();
});

describe('adapterId', () => {
it('shares one adapterId across all adapters from the same factory call', () => {
const adapter = createVercelAdapter(flagsClient);
const a = adapter();
const b = adapter();
expect(a).not.toBe(b);
expect(a.adapterId).toBeDefined();
expect(a.adapterId).toBe(b.adapterId);
});

it('uses different adapterIds across separate factory calls', () => {
const adapterA = createVercelAdapter('vf_client_key_a');
const adapterB = createVercelAdapter('vf_client_key_b');
expect(adapterA().adapterId).not.toBe(adapterB().adapterId);
});
});

describe('bulkDecide', () => {
it('forwards to flagsClient.bulkEvaluate with mapped flags and entities', async () => {
const bulkEvaluateMock = vi
.fn()
.mockResolvedValue({ a: { value: 'x' }, b: { value: 'y' } });
const fakeClient = {
origin: { provider: 'vercel', sdkKey: 'vf_x' },
bulkEvaluate: bulkEvaluateMock,
} as unknown as typeof flagsClient;

const adapter = createVercelAdapter(fakeClient)();
const result = await adapter.bulkDecide!({
flags: [{ key: 'a', defaultValue: 'da' }, { key: 'b' }],
entities: { user: { id: 'u1' } } as any,
headers: undefined as any,
cookies: undefined as any,
});

expect(bulkEvaluateMock).toHaveBeenCalledTimes(1);
expect(bulkEvaluateMock).toHaveBeenCalledWith(
[
{ key: 'a', defaultValue: 'da' },
{ key: 'b', defaultValue: undefined },
],
{ user: { id: 'u1' } },
);
expect(result).toEqual({ a: 'x', b: 'y' });
});

it('omits keys whose EvaluationResult.value is undefined', async () => {
const fakeClient = {
origin: { provider: 'vercel', sdkKey: 'vf_x' },
bulkEvaluate: vi.fn().mockResolvedValue({
a: { value: 'ok' },
b: { value: undefined, reason: 'error', errorMessage: 'nope' },
}),
} as unknown as typeof flagsClient;

const adapter = createVercelAdapter(fakeClient)();
const result = await adapter.bulkDecide!({
flags: [{ key: 'a' }, { key: 'b' }],
headers: undefined as any,
cookies: undefined as any,
});
expect(result).toEqual({ a: 'ok' });
expect('b' in result).toBe(false);
});
});
});

describe('when used with getProviderData', () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/adapter-vercel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,18 @@ export function createVercelAdapter(
? createClient(sdkKeyOrFlagsClient)
: sdkKeyOrFlagsClient;

// Stable identity for this adapter's underlying flagsClient. Captured in
// the closure so every adapter object the factory below returns shares it,
// letting `bulk()` group flags from multiple `vercelAdapter()` calls into
// a single `bulkDecide` invocation.
const adapterId = Symbol('vercelAdapter');

return function vercelAdapter<ValueType, EntitiesType>(): Adapter<
ValueType,
EntitiesType
> {
return {
adapterId,
origin: flagsClient.origin,
config: { reportValue: false },
async decide({ key, entities }): Promise<ValueType> {
Expand All @@ -57,6 +64,20 @@ export function createVercelAdapter(
// when there was an error but the defaultValue was set
return evaluationResult.value;
},
async bulkDecide({ flags, entities }) {
const results = await flagsClient.bulkEvaluate<ValueType, EntitiesType>(
flags,
entities,
);
const out: Record<string, ValueType> = {};
for (const key in results) {
const r = results[key]!;
// Omit undefined so the SDK applies the per-flag `defaultValue`
// fallback (matches single-decide semantics).
if (r.value !== undefined) out[key] = r.value;
}
return out;
Comment thread
dferber90 marked this conversation as resolved.
},
};
};
}
Expand Down
Loading
Loading