@avsbhq/vue 1.0.2 → 1.1.1

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/dist/pinia.cjs ADDED
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/pinia.ts
21
+ var pinia_exports = {};
22
+ __export(pinia_exports, {
23
+ useAvsbStore: () => useAvsbStore
24
+ });
25
+ module.exports = __toCommonJS(pinia_exports);
26
+ var import_pinia = require("pinia");
27
+ var import_vue2 = require("vue");
28
+
29
+ // src/context.ts
30
+ var import_vue = require("vue");
31
+ var AvsbInjectionKey = /* @__PURE__ */ Symbol("avsb-injection-key");
32
+ function useAvsbContext() {
33
+ const value = (0, import_vue.inject)(AvsbInjectionKey);
34
+ if (value === void 0) {
35
+ throw new Error(
36
+ `[avsb] composables must be used inside <AvsbProvider> or app.use(AvsbPlugin). Wrap your component tree with <AvsbProvider sdk-key="sdk_production_..." :context="{ kind: 'user', key: userId }">, or install the plugin with app.use(AvsbPlugin, { sdkKey: 'sdk_production_...' }).`
37
+ );
38
+ }
39
+ return value;
40
+ }
41
+
42
+ // src/pinia.ts
43
+ var useAvsbStore = (0, import_pinia.defineStore)("avsb", () => {
44
+ const context = useAvsbContext();
45
+ const client = context.client;
46
+ const allFlags = (0, import_vue2.shallowRef)(client.getAllFlags());
47
+ const unsubscribe = client.on("flagChange", () => {
48
+ allFlags.value = client.getAllFlags();
49
+ });
50
+ (0, import_vue2.onScopeDispose)(unsubscribe);
51
+ return {
52
+ client: (0, import_vue2.computed)(() => client),
53
+ status: (0, import_vue2.computed)(() => context.status),
54
+ error: (0, import_vue2.computed)(() => context.error),
55
+ degraded: (0, import_vue2.computed)(() => context.degraded),
56
+ flags: (0, import_vue2.computed)(() => allFlags.value),
57
+ identify: (evalContext) => {
58
+ client.identify(evalContext);
59
+ },
60
+ alias: (previousContext, newContext) => {
61
+ client.alias(previousContext, newContext);
62
+ },
63
+ reset: () => {
64
+ client.reset();
65
+ },
66
+ track: (eventKey, payload) => {
67
+ client.track(eventKey, payload);
68
+ }
69
+ };
70
+ });
71
+ // Annotate the CommonJS export names for ESM import in node:
72
+ 0 && (module.exports = {
73
+ useAvsbStore
74
+ });
@@ -0,0 +1,23 @@
1
+ import * as pinia from 'pinia';
2
+ import { ComputedRef } from 'vue';
3
+ import { Flag, EvalContext, TrackPayload } from '@avsbhq/core';
4
+ import { V as VueAvsbClient } from './clientInterface-BMlQ7Uhe.cjs';
5
+ import { d as AvsbStatus } from './types-DECMgjjk.cjs';
6
+ import '@avsbhq/browser';
7
+
8
+ interface AvsbStoreState {
9
+ /** The client the provider built or adopted. */
10
+ client: ComputedRef<VueAvsbClient>;
11
+ status: ComputedRef<AvsbStatus>;
12
+ error: ComputedRef<Error | undefined>;
13
+ degraded: ComputedRef<boolean>;
14
+ /** Every flag, refreshed whenever any flag changes. */
15
+ flags: ComputedRef<Record<string, Flag>>;
16
+ identify: (context: EvalContext) => void;
17
+ alias: (previousContext: EvalContext, newContext: EvalContext) => void;
18
+ reset: () => void;
19
+ track: (eventKey: string, payload?: TrackPayload) => void;
20
+ }
21
+ declare const useAvsbStore: pinia.StoreDefinition<"avsb", Pick<AvsbStoreState, never>, Pick<AvsbStoreState, "flags" | "client" | "error" | "status" | "degraded">, Pick<AvsbStoreState, "reset" | "track" | "identify" | "alias">>;
22
+
23
+ export { type AvsbStoreState, useAvsbStore };
@@ -0,0 +1,23 @@
1
+ import * as pinia from 'pinia';
2
+ import { ComputedRef } from 'vue';
3
+ import { Flag, EvalContext, TrackPayload } from '@avsbhq/core';
4
+ import { V as VueAvsbClient } from './clientInterface-BMlQ7Uhe.js';
5
+ import { d as AvsbStatus } from './types-Di6G-dj0.js';
6
+ import '@avsbhq/browser';
7
+
8
+ interface AvsbStoreState {
9
+ /** The client the provider built or adopted. */
10
+ client: ComputedRef<VueAvsbClient>;
11
+ status: ComputedRef<AvsbStatus>;
12
+ error: ComputedRef<Error | undefined>;
13
+ degraded: ComputedRef<boolean>;
14
+ /** Every flag, refreshed whenever any flag changes. */
15
+ flags: ComputedRef<Record<string, Flag>>;
16
+ identify: (context: EvalContext) => void;
17
+ alias: (previousContext: EvalContext, newContext: EvalContext) => void;
18
+ reset: () => void;
19
+ track: (eventKey: string, payload?: TrackPayload) => void;
20
+ }
21
+ declare const useAvsbStore: pinia.StoreDefinition<"avsb", Pick<AvsbStoreState, never>, Pick<AvsbStoreState, "flags" | "client" | "error" | "status" | "degraded">, Pick<AvsbStoreState, "reset" | "track" | "identify" | "alias">>;
22
+
23
+ export { type AvsbStoreState, useAvsbStore };
package/dist/pinia.js ADDED
@@ -0,0 +1,38 @@
1
+ import {
2
+ useAvsbContext
3
+ } from "./chunk-4XBADQFO.js";
4
+
5
+ // src/pinia.ts
6
+ import { defineStore } from "pinia";
7
+ import { computed, onScopeDispose, shallowRef } from "vue";
8
+ var useAvsbStore = defineStore("avsb", () => {
9
+ const context = useAvsbContext();
10
+ const client = context.client;
11
+ const allFlags = shallowRef(client.getAllFlags());
12
+ const unsubscribe = client.on("flagChange", () => {
13
+ allFlags.value = client.getAllFlags();
14
+ });
15
+ onScopeDispose(unsubscribe);
16
+ return {
17
+ client: computed(() => client),
18
+ status: computed(() => context.status),
19
+ error: computed(() => context.error),
20
+ degraded: computed(() => context.degraded),
21
+ flags: computed(() => allFlags.value),
22
+ identify: (evalContext) => {
23
+ client.identify(evalContext);
24
+ },
25
+ alias: (previousContext, newContext) => {
26
+ client.alias(previousContext, newContext);
27
+ },
28
+ reset: () => {
29
+ client.reset();
30
+ },
31
+ track: (eventKey, payload) => {
32
+ client.track(eventKey, payload);
33
+ }
34
+ };
35
+ });
36
+ export {
37
+ useAvsbStore
38
+ };
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ AvsbTestProvider: () => AvsbTestProvider,
24
+ createTestClient: () => createTestClient,
25
+ createTestDatafile: () => createTestDatafile
26
+ });
27
+ module.exports = __toCommonJS(testing_exports);
28
+ var import_vue4 = require("vue");
29
+
30
+ // ../avsb-test/dist/fixtures.js
31
+ var import_browser = require("@avsbhq/browser");
32
+ var TEST_SDK_KEY = "sdk_test_0000000000000000000";
33
+ function flagType(value) {
34
+ if (typeof value === "boolean") return "boolean";
35
+ if (typeof value === "string") return "string";
36
+ if (typeof value === "number") return "number";
37
+ return "json";
38
+ }
39
+ function entryFor(key, value, index) {
40
+ const variationId = `var_${index}`;
41
+ return {
42
+ id: `flag_${index}`,
43
+ key,
44
+ type: flagType(value),
45
+ enabled: true,
46
+ defaultVariationId: variationId,
47
+ variations: [{ id: variationId, key: "test", value }],
48
+ overrides: [],
49
+ rules: [
50
+ {
51
+ id: `rule_${index}`,
52
+ type: "ab_test",
53
+ enabled: true,
54
+ audienceIds: [],
55
+ hashAttribute: "user.key",
56
+ trafficAllocation: null,
57
+ // `percentage` is a 0..1 proportion (the evaluator multiplies it by
58
+ // 10000), so this is the whole audience.
59
+ variations: [{ variationId, percentage: 1 }]
60
+ }
61
+ ]
62
+ };
63
+ }
64
+ function createTestDatafile(flags, options) {
65
+ const mapped = Object.entries(flags ?? {}).map(
66
+ ([key, value], index) => entryFor(key, value, index)
67
+ );
68
+ return {
69
+ version: 2,
70
+ projectType: "FEATURE_FLAG",
71
+ sdkKey: options?.sdkKey ?? TEST_SDK_KEY,
72
+ environmentKey: options?.environmentKey ?? "test",
73
+ publishedAt: options?.publishedAt ?? "2024-01-01T00:00:00.000Z",
74
+ // An ORIGIN, the way the publisher writes it: SDKs append the
75
+ // `/v1/collect/*` paths themselves through the core endpoint contract.
76
+ // The `.invalid` TLD is reserved and never resolves, so a stray flush in
77
+ // a test cannot reach a real host.
78
+ collectEndpoint: "https://collect.test.invalid",
79
+ flags: [...mapped, ...options?.entries ?? []],
80
+ audiences: [],
81
+ holdouts: [],
82
+ bandits: [],
83
+ // `bigSegments` deliberately absent: the field was deleted from the
84
+ // datafile type (breaking change B28) because nothing evaluated it.
85
+ contextKinds: ["user"],
86
+ exposureSampling: 1
87
+ };
88
+ }
89
+ var TEST_CONTEXT = { kind: "user", key: "test-user" };
90
+ function createTestClient(options) {
91
+ const datafile = options?.datafile ?? createTestDatafile(options?.flags ?? {});
92
+ const client = new import_browser.AvsbClient({
93
+ sdkKey: datafile.sdkKey || TEST_SDK_KEY,
94
+ context: options?.context ?? TEST_CONTEXT,
95
+ // No fetch at construction. The datafile arrives through the bootstrap
96
+ // handoff below, which is also what wires the tracker and settles
97
+ // `getInitResult()`, so `track` and `flush` behave as they do in
98
+ // production instead of queueing forever behind a tracker that a
99
+ // failed background fetch never created.
100
+ autoInit: false,
101
+ autoRefresh: false,
102
+ cache: false,
103
+ logLevel: "silent",
104
+ // Long enough that the automatic flush never fires mid-test. Assert on
105
+ // events with an explicit `await client.flush()`.
106
+ events: { flushInterval: 6e4 }
107
+ });
108
+ client.applyDatafileBootstrap(datafile);
109
+ return client;
110
+ }
111
+
112
+ // src/AvsbProvider.ts
113
+ var import_vue3 = require("vue");
114
+ var import_browser3 = require("@avsbhq/browser");
115
+ var import_core = require("@avsbhq/core");
116
+
117
+ // src/context.ts
118
+ var import_vue = require("vue");
119
+ var AvsbInjectionKey = /* @__PURE__ */ Symbol("avsb-injection-key");
120
+ function hasAvsbContext() {
121
+ return (0, import_vue.inject)(AvsbInjectionKey, null) !== null;
122
+ }
123
+
124
+ // src/internal/providerState.ts
125
+ var import_vue2 = require("vue");
126
+ var UNEXPLAINED_FAILURE = "[avsb] the SDK could not load a datafile and nothing was cached or bootstrapped, so every flag is returning the default you passed. Check the SDK key on app.avsb.cloud under Environments in the project sidebar.";
127
+ function createProviderState(client) {
128
+ const status = (0, import_vue2.ref)(client.isReady() ? "ready" : "loading");
129
+ const error = (0, import_vue2.shallowRef)(void 0);
130
+ const degraded = (0, import_vue2.ref)(false);
131
+ let stopped = false;
132
+ const applyInitResult = (result) => {
133
+ if (stopped) return;
134
+ if (result.success) {
135
+ status.value = "ready";
136
+ degraded.value = result.degraded === true;
137
+ error.value = result.error;
138
+ return;
139
+ }
140
+ status.value = "error";
141
+ error.value = result.error ?? new Error(UNEXPLAINED_FAILURE);
142
+ };
143
+ const settled = client.getInitResult();
144
+ if (settled) {
145
+ applyInitResult(settled);
146
+ } else {
147
+ void client.onReady().then(applyInitResult);
148
+ }
149
+ const offError = client.on("error", (e) => {
150
+ if (stopped) return;
151
+ error.value = e;
152
+ if (client.isReady()) {
153
+ degraded.value = true;
154
+ return;
155
+ }
156
+ status.value = "error";
157
+ });
158
+ const offReady = client.on("ready", applyInitResult);
159
+ return {
160
+ status,
161
+ error,
162
+ degraded,
163
+ stop: () => {
164
+ stopped = true;
165
+ offError();
166
+ offReady();
167
+ }
168
+ };
169
+ }
170
+
171
+ // src/internal/resolveClient.ts
172
+ var import_browser2 = require("@avsbhq/browser");
173
+ var MISUSE_MESSAGE = `[avsb] <AvsbProvider> needs either an sdk-key (Mode A) or a client (Mode B). Mode A: <AvsbProvider sdk-key="sdk_production_..." :context="{ kind: 'user', key: userId }">. Mode B: <AvsbProvider :client="myClient">. The key for each environment is on app.avsb.cloud under Environments in the project sidebar.`;
174
+ function resolveClient(props) {
175
+ const injected = props.client;
176
+ if (injected !== void 0 && injected !== null) {
177
+ return { client: injected, owned: false };
178
+ }
179
+ const { sdkKey, context, bootstrap, options } = props;
180
+ if (typeof sdkKey !== "string" || sdkKey.length === 0) {
181
+ throw new Error(MISUSE_MESSAGE);
182
+ }
183
+ const clientOptions = {
184
+ ...options ?? {},
185
+ sdkKey,
186
+ ...context !== void 0 ? { context } : {},
187
+ ...bootstrap !== void 0 ? { bootstrap } : {}
188
+ };
189
+ return { client: new import_browser2.AvsbClient(clientOptions), owned: true };
190
+ }
191
+
192
+ // src/AvsbProvider.ts
193
+ var NESTED_PROVIDER_WARNING = "[avsb] <AvsbProvider> is nested inside another <AvsbProvider>. The inner one shadows the outer for everything below it, so two clients are now running, each with its own visitor id and its own event queue. Mount one provider at the root of the app unless you meant to isolate a subtree.";
194
+ var AvsbProvider = (0, import_vue3.defineComponent)({
195
+ name: "AvsbProvider",
196
+ props: {
197
+ /** Mode B: a client you built and own. */
198
+ client: {
199
+ type: Object,
200
+ default: void 0
201
+ },
202
+ /** Mode A: the environment's SDK key. */
203
+ sdkKey: {
204
+ type: String,
205
+ default: void 0
206
+ },
207
+ /** Mode A: evaluation context. Omit for an anonymous visitor. */
208
+ context: {
209
+ type: Object,
210
+ default: void 0
211
+ },
212
+ /** Mode A: a datafile fetched on the server, for a real first paint. */
213
+ bootstrap: {
214
+ type: Object,
215
+ default: void 0
216
+ },
217
+ /** Mode A: everything else AvsbClient accepts. */
218
+ options: {
219
+ type: Object,
220
+ default: void 0
221
+ }
222
+ },
223
+ setup(props, { slots }) {
224
+ if (hasAvsbContext() && (0, import_browser3.isDevEnvironment)()) {
225
+ (0, import_core.consoleLogger)("warn").warn(NESTED_PROVIDER_WARNING);
226
+ }
227
+ const { client, owned } = resolveClient(props);
228
+ const state = createProviderState(client);
229
+ (0, import_vue3.provide)(AvsbInjectionKey, {
230
+ client,
231
+ get status() {
232
+ return state.status.value;
233
+ },
234
+ get error() {
235
+ return state.error.value;
236
+ },
237
+ get degraded() {
238
+ return state.degraded.value;
239
+ }
240
+ });
241
+ (0, import_vue3.onUnmounted)(() => {
242
+ state.stop();
243
+ if (owned) void client.close();
244
+ });
245
+ return () => slots.default?.();
246
+ }
247
+ });
248
+
249
+ // src/testing.ts
250
+ var AvsbTestProvider = (0, import_vue4.defineComponent)({
251
+ name: "AvsbTestProvider",
252
+ props: {
253
+ flags: {
254
+ type: Object,
255
+ default: void 0
256
+ },
257
+ context: {
258
+ type: Object,
259
+ default: void 0
260
+ },
261
+ client: {
262
+ type: Object,
263
+ default: void 0
264
+ }
265
+ },
266
+ setup(props, { slots }) {
267
+ const client = props.client ?? createTestClient({
268
+ ...props.flags !== void 0 ? { flags: props.flags } : {},
269
+ ...props.context !== void 0 ? { context: props.context } : {}
270
+ });
271
+ return () => (0, import_vue4.h)(AvsbProvider, { client }, { default: () => slots.default?.() });
272
+ }
273
+ });
274
+ // Annotate the CommonJS export names for ESM import in node:
275
+ 0 && (module.exports = {
276
+ AvsbTestProvider,
277
+ createTestClient,
278
+ createTestDatafile
279
+ });
@@ -0,0 +1,50 @@
1
+ import * as vue from 'vue';
2
+ import { PropType } from 'vue';
3
+ import { TestFlags } from '@avsbhq/test/fixtures';
4
+ export { CreateTestDatafileOptions, TestClientOptions, TestFlagValue, TestFlags, createTestClient, createTestDatafile } from '@avsbhq/test/fixtures';
5
+ import { EvalContext } from '@avsbhq/core';
6
+ import { V as VueAvsbClient } from './clientInterface-BMlQ7Uhe.cjs';
7
+ import '@avsbhq/browser';
8
+
9
+ /**
10
+ * `<AvsbTestProvider :flags="{ 'checkout-v2': true }">` mounts the real
11
+ * provider around a real client, so the components under test use the
12
+ * composables they use in production.
13
+ *
14
+ * Pass `client` instead of `flags` to drive the client yourself.
15
+ */
16
+ declare const AvsbTestProvider: vue.DefineComponent<vue.ExtractPropTypes<{
17
+ flags: {
18
+ type: PropType<TestFlags>;
19
+ default: undefined;
20
+ };
21
+ context: {
22
+ type: PropType<EvalContext>;
23
+ default: undefined;
24
+ };
25
+ client: {
26
+ type: PropType<VueAvsbClient>;
27
+ default: undefined;
28
+ };
29
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
30
+ [key: string]: any;
31
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
32
+ flags: {
33
+ type: PropType<TestFlags>;
34
+ default: undefined;
35
+ };
36
+ context: {
37
+ type: PropType<EvalContext>;
38
+ default: undefined;
39
+ };
40
+ client: {
41
+ type: PropType<VueAvsbClient>;
42
+ default: undefined;
43
+ };
44
+ }>> & Readonly<{}>, {
45
+ flags: TestFlags;
46
+ context: EvalContext;
47
+ client: VueAvsbClient;
48
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
49
+
50
+ export { AvsbTestProvider };
@@ -0,0 +1,50 @@
1
+ import * as vue from 'vue';
2
+ import { PropType } from 'vue';
3
+ import { TestFlags } from '@avsbhq/test/fixtures';
4
+ export { CreateTestDatafileOptions, TestClientOptions, TestFlagValue, TestFlags, createTestClient, createTestDatafile } from '@avsbhq/test/fixtures';
5
+ import { EvalContext } from '@avsbhq/core';
6
+ import { V as VueAvsbClient } from './clientInterface-BMlQ7Uhe.js';
7
+ import '@avsbhq/browser';
8
+
9
+ /**
10
+ * `<AvsbTestProvider :flags="{ 'checkout-v2': true }">` mounts the real
11
+ * provider around a real client, so the components under test use the
12
+ * composables they use in production.
13
+ *
14
+ * Pass `client` instead of `flags` to drive the client yourself.
15
+ */
16
+ declare const AvsbTestProvider: vue.DefineComponent<vue.ExtractPropTypes<{
17
+ flags: {
18
+ type: PropType<TestFlags>;
19
+ default: undefined;
20
+ };
21
+ context: {
22
+ type: PropType<EvalContext>;
23
+ default: undefined;
24
+ };
25
+ client: {
26
+ type: PropType<VueAvsbClient>;
27
+ default: undefined;
28
+ };
29
+ }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
30
+ [key: string]: any;
31
+ }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
32
+ flags: {
33
+ type: PropType<TestFlags>;
34
+ default: undefined;
35
+ };
36
+ context: {
37
+ type: PropType<EvalContext>;
38
+ default: undefined;
39
+ };
40
+ client: {
41
+ type: PropType<VueAvsbClient>;
42
+ default: undefined;
43
+ };
44
+ }>> & Readonly<{}>, {
45
+ flags: TestFlags;
46
+ context: EvalContext;
47
+ client: VueAvsbClient;
48
+ }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
49
+
50
+ export { AvsbTestProvider };
@@ -0,0 +1,120 @@
1
+ import {
2
+ AvsbProvider
3
+ } from "./chunk-FUADAPSW.js";
4
+ import "./chunk-4XBADQFO.js";
5
+
6
+ // src/testing.ts
7
+ import { defineComponent, h } from "vue";
8
+
9
+ // ../avsb-test/dist/fixtures.js
10
+ import { AvsbClient } from "@avsbhq/browser";
11
+ var TEST_SDK_KEY = "sdk_test_0000000000000000000";
12
+ function flagType(value) {
13
+ if (typeof value === "boolean") return "boolean";
14
+ if (typeof value === "string") return "string";
15
+ if (typeof value === "number") return "number";
16
+ return "json";
17
+ }
18
+ function entryFor(key, value, index) {
19
+ const variationId = `var_${index}`;
20
+ return {
21
+ id: `flag_${index}`,
22
+ key,
23
+ type: flagType(value),
24
+ enabled: true,
25
+ defaultVariationId: variationId,
26
+ variations: [{ id: variationId, key: "test", value }],
27
+ overrides: [],
28
+ rules: [
29
+ {
30
+ id: `rule_${index}`,
31
+ type: "ab_test",
32
+ enabled: true,
33
+ audienceIds: [],
34
+ hashAttribute: "user.key",
35
+ trafficAllocation: null,
36
+ // `percentage` is a 0..1 proportion (the evaluator multiplies it by
37
+ // 10000), so this is the whole audience.
38
+ variations: [{ variationId, percentage: 1 }]
39
+ }
40
+ ]
41
+ };
42
+ }
43
+ function createTestDatafile(flags, options) {
44
+ const mapped = Object.entries(flags ?? {}).map(
45
+ ([key, value], index) => entryFor(key, value, index)
46
+ );
47
+ return {
48
+ version: 2,
49
+ projectType: "FEATURE_FLAG",
50
+ sdkKey: options?.sdkKey ?? TEST_SDK_KEY,
51
+ environmentKey: options?.environmentKey ?? "test",
52
+ publishedAt: options?.publishedAt ?? "2024-01-01T00:00:00.000Z",
53
+ // An ORIGIN, the way the publisher writes it: SDKs append the
54
+ // `/v1/collect/*` paths themselves through the core endpoint contract.
55
+ // The `.invalid` TLD is reserved and never resolves, so a stray flush in
56
+ // a test cannot reach a real host.
57
+ collectEndpoint: "https://collect.test.invalid",
58
+ flags: [...mapped, ...options?.entries ?? []],
59
+ audiences: [],
60
+ holdouts: [],
61
+ bandits: [],
62
+ // `bigSegments` deliberately absent: the field was deleted from the
63
+ // datafile type (breaking change B28) because nothing evaluated it.
64
+ contextKinds: ["user"],
65
+ exposureSampling: 1
66
+ };
67
+ }
68
+ var TEST_CONTEXT = { kind: "user", key: "test-user" };
69
+ function createTestClient(options) {
70
+ const datafile = options?.datafile ?? createTestDatafile(options?.flags ?? {});
71
+ const client = new AvsbClient({
72
+ sdkKey: datafile.sdkKey || TEST_SDK_KEY,
73
+ context: options?.context ?? TEST_CONTEXT,
74
+ // No fetch at construction. The datafile arrives through the bootstrap
75
+ // handoff below, which is also what wires the tracker and settles
76
+ // `getInitResult()`, so `track` and `flush` behave as they do in
77
+ // production instead of queueing forever behind a tracker that a
78
+ // failed background fetch never created.
79
+ autoInit: false,
80
+ autoRefresh: false,
81
+ cache: false,
82
+ logLevel: "silent",
83
+ // Long enough that the automatic flush never fires mid-test. Assert on
84
+ // events with an explicit `await client.flush()`.
85
+ events: { flushInterval: 6e4 }
86
+ });
87
+ client.applyDatafileBootstrap(datafile);
88
+ return client;
89
+ }
90
+
91
+ // src/testing.ts
92
+ var AvsbTestProvider = defineComponent({
93
+ name: "AvsbTestProvider",
94
+ props: {
95
+ flags: {
96
+ type: Object,
97
+ default: void 0
98
+ },
99
+ context: {
100
+ type: Object,
101
+ default: void 0
102
+ },
103
+ client: {
104
+ type: Object,
105
+ default: void 0
106
+ }
107
+ },
108
+ setup(props, { slots }) {
109
+ const client = props.client ?? createTestClient({
110
+ ...props.flags !== void 0 ? { flags: props.flags } : {},
111
+ ...props.context !== void 0 ? { context: props.context } : {}
112
+ });
113
+ return () => h(AvsbProvider, { client }, { default: () => slots.default?.() });
114
+ }
115
+ });
116
+ export {
117
+ AvsbTestProvider,
118
+ createTestClient,
119
+ createTestDatafile
120
+ };