@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.
@@ -0,0 +1,21 @@
1
+ // src/context.ts
2
+ import { inject } from "vue";
3
+ var AvsbInjectionKey = /* @__PURE__ */ Symbol("avsb-injection-key");
4
+ function useAvsbContext() {
5
+ const value = inject(AvsbInjectionKey);
6
+ if (value === void 0) {
7
+ throw new Error(
8
+ `[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_...' }).`
9
+ );
10
+ }
11
+ return value;
12
+ }
13
+ function hasAvsbContext() {
14
+ return inject(AvsbInjectionKey, null) !== null;
15
+ }
16
+
17
+ export {
18
+ AvsbInjectionKey,
19
+ useAvsbContext,
20
+ hasAvsbContext
21
+ };
@@ -0,0 +1,140 @@
1
+ import {
2
+ AvsbInjectionKey,
3
+ hasAvsbContext
4
+ } from "./chunk-4XBADQFO.js";
5
+
6
+ // src/AvsbProvider.ts
7
+ import { defineComponent, onUnmounted, provide } from "vue";
8
+ import { isDevEnvironment } from "@avsbhq/browser";
9
+ import { consoleLogger } from "@avsbhq/core";
10
+
11
+ // src/internal/providerState.ts
12
+ import { ref, shallowRef } from "vue";
13
+ 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.";
14
+ function createProviderState(client) {
15
+ const status = ref(client.isReady() ? "ready" : "loading");
16
+ const error = shallowRef(void 0);
17
+ const degraded = ref(false);
18
+ let stopped = false;
19
+ const applyInitResult = (result) => {
20
+ if (stopped) return;
21
+ if (result.success) {
22
+ status.value = "ready";
23
+ degraded.value = result.degraded === true;
24
+ error.value = result.error;
25
+ return;
26
+ }
27
+ status.value = "error";
28
+ error.value = result.error ?? new Error(UNEXPLAINED_FAILURE);
29
+ };
30
+ const settled = client.getInitResult();
31
+ if (settled) {
32
+ applyInitResult(settled);
33
+ } else {
34
+ void client.onReady().then(applyInitResult);
35
+ }
36
+ const offError = client.on("error", (e) => {
37
+ if (stopped) return;
38
+ error.value = e;
39
+ if (client.isReady()) {
40
+ degraded.value = true;
41
+ return;
42
+ }
43
+ status.value = "error";
44
+ });
45
+ const offReady = client.on("ready", applyInitResult);
46
+ return {
47
+ status,
48
+ error,
49
+ degraded,
50
+ stop: () => {
51
+ stopped = true;
52
+ offError();
53
+ offReady();
54
+ }
55
+ };
56
+ }
57
+
58
+ // src/internal/resolveClient.ts
59
+ import { AvsbClient } from "@avsbhq/browser";
60
+ 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.`;
61
+ function resolveClient(props) {
62
+ const injected = props.client;
63
+ if (injected !== void 0 && injected !== null) {
64
+ return { client: injected, owned: false };
65
+ }
66
+ const { sdkKey, context, bootstrap, options } = props;
67
+ if (typeof sdkKey !== "string" || sdkKey.length === 0) {
68
+ throw new Error(MISUSE_MESSAGE);
69
+ }
70
+ const clientOptions = {
71
+ ...options ?? {},
72
+ sdkKey,
73
+ ...context !== void 0 ? { context } : {},
74
+ ...bootstrap !== void 0 ? { bootstrap } : {}
75
+ };
76
+ return { client: new AvsbClient(clientOptions), owned: true };
77
+ }
78
+
79
+ // src/AvsbProvider.ts
80
+ 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.";
81
+ var AvsbProvider = defineComponent({
82
+ name: "AvsbProvider",
83
+ props: {
84
+ /** Mode B: a client you built and own. */
85
+ client: {
86
+ type: Object,
87
+ default: void 0
88
+ },
89
+ /** Mode A: the environment's SDK key. */
90
+ sdkKey: {
91
+ type: String,
92
+ default: void 0
93
+ },
94
+ /** Mode A: evaluation context. Omit for an anonymous visitor. */
95
+ context: {
96
+ type: Object,
97
+ default: void 0
98
+ },
99
+ /** Mode A: a datafile fetched on the server, for a real first paint. */
100
+ bootstrap: {
101
+ type: Object,
102
+ default: void 0
103
+ },
104
+ /** Mode A: everything else AvsbClient accepts. */
105
+ options: {
106
+ type: Object,
107
+ default: void 0
108
+ }
109
+ },
110
+ setup(props, { slots }) {
111
+ if (hasAvsbContext() && isDevEnvironment()) {
112
+ consoleLogger("warn").warn(NESTED_PROVIDER_WARNING);
113
+ }
114
+ const { client, owned } = resolveClient(props);
115
+ const state = createProviderState(client);
116
+ provide(AvsbInjectionKey, {
117
+ client,
118
+ get status() {
119
+ return state.status.value;
120
+ },
121
+ get error() {
122
+ return state.error.value;
123
+ },
124
+ get degraded() {
125
+ return state.degraded.value;
126
+ }
127
+ });
128
+ onUnmounted(() => {
129
+ state.stop();
130
+ if (owned) void client.close();
131
+ });
132
+ return () => slots.default?.();
133
+ }
134
+ });
135
+
136
+ export {
137
+ createProviderState,
138
+ resolveClient,
139
+ AvsbProvider
140
+ };
@@ -0,0 +1,69 @@
1
+ import { InitResult, FlagDatafile, EvalContext, AvsbFlagKey, Flag, TrackPayload, ClientEventName, ClientEventMap } from '@avsbhq/core';
2
+ import { GetFlagOptions, GetAllFlagsOptions } from '@avsbhq/browser';
3
+
4
+ /**
5
+ * The slice of `AvsbClient` (@avsbhq/browser) that @avsbhq/vue depends on.
6
+ *
7
+ * This is the store contract documented in the browser SDK's README under
8
+ * "The store contract, for framework adapters": `subscribe` + `getSnapshot`
9
+ * for render-safe reads, the typed getters with `{ readOnly: true }` for
10
+ * render paths, and `isReady()` / `getInitResult()` for a synchronous first
11
+ * pass. A real `AvsbClient` satisfies this interface structurally, so no cast
12
+ * is required at the boundary.
13
+ *
14
+ * Why an interface at all: composables take the client from the provider's
15
+ * injected context, so a test (or a Storybook decorator) can inject anything
16
+ * that speaks this surface. `@avsbhq/vue/testing` ships one that is a real
17
+ * `AvsbClient` with the network switched off.
18
+ */
19
+
20
+ interface VueAvsbClient {
21
+ /** Resolves when the init attempt settles. Never rejects. */
22
+ onReady(options?: {
23
+ timeout?: number;
24
+ }): Promise<InitResult>;
25
+ /** True when flags are readable right now (bootstrap, cache, or network). */
26
+ isReady(): boolean;
27
+ /** The settled init outcome, or null while the first attempt is in flight. */
28
+ getInitResult(): InitResult | null;
29
+ /** Adopt a datafile received out of band (SSR bootstrap). */
30
+ applyDatafileBootstrap(datafile: FlagDatafile): void;
31
+ refresh(): Promise<void>;
32
+ flush(): Promise<void>;
33
+ close(): Promise<void>;
34
+ identify(context: EvalContext): void;
35
+ updateAttributes(partial: Record<string, unknown>, contextKind?: string): void;
36
+ /**
37
+ * Queues one `avsb_alias` event and returns. Synchronous: there is nothing
38
+ * to await, and it does not rewrite assignments already made.
39
+ */
40
+ alias(previousContext: EvalContext, newContext: EvalContext): void;
41
+ getContext(): EvalContext;
42
+ /** New anonymous identity, overrides cleared. Call on sign-out. */
43
+ reset(): void;
44
+ getFlag<T>(flagKey: AvsbFlagKey, defaultValue: T, options?: GetFlagOptions): Flag<T>;
45
+ getBoolFlag(flagKey: AvsbFlagKey, defaultValue: boolean, options?: GetFlagOptions): Flag<boolean>;
46
+ getStringFlag(flagKey: AvsbFlagKey, defaultValue: string, options?: GetFlagOptions): Flag<string>;
47
+ getNumberFlag(flagKey: AvsbFlagKey, defaultValue: number, options?: GetFlagOptions): Flag<number>;
48
+ getJsonFlag<T>(flagKey: AvsbFlagKey, defaultValue: T, options?: GetFlagOptions): Flag<T>;
49
+ getAllFlags(options?: GetAllFlagsOptions): Record<string, Flag>;
50
+ /**
51
+ * Called when this flag's value changes, and once when the first datafile
52
+ * is applied. Returns its own unsubscribe function.
53
+ */
54
+ subscribe(flagKey: AvsbFlagKey, listener: () => void): () => void;
55
+ /**
56
+ * Render-safe read: the SAME frozen object until the value changes, no
57
+ * exposure, no `evaluation` event.
58
+ */
59
+ getSnapshot<T>(flagKey: AvsbFlagKey, defaultValue: T): Flag<T>;
60
+ track(eventKey: string, payload?: TrackPayload): void;
61
+ on<E extends ClientEventName>(event: E, cb: ClientEventMap[E]): () => void;
62
+ setOverrideForUser(flagKey: AvsbFlagKey, contextKey: string, variationKey: string, contextKind?: string): void;
63
+ setGlobalOverride(flagKey: AvsbFlagKey, variationKey: string): void;
64
+ clearOverrideForUser(flagKey: AvsbFlagKey, contextKey: string, contextKind?: string): void;
65
+ clearGlobalOverride(flagKey: AvsbFlagKey): void;
66
+ clearAllOverrides(): void;
67
+ }
68
+
69
+ export type { VueAvsbClient as V };
@@ -0,0 +1,69 @@
1
+ import { InitResult, FlagDatafile, EvalContext, AvsbFlagKey, Flag, TrackPayload, ClientEventName, ClientEventMap } from '@avsbhq/core';
2
+ import { GetFlagOptions, GetAllFlagsOptions } from '@avsbhq/browser';
3
+
4
+ /**
5
+ * The slice of `AvsbClient` (@avsbhq/browser) that @avsbhq/vue depends on.
6
+ *
7
+ * This is the store contract documented in the browser SDK's README under
8
+ * "The store contract, for framework adapters": `subscribe` + `getSnapshot`
9
+ * for render-safe reads, the typed getters with `{ readOnly: true }` for
10
+ * render paths, and `isReady()` / `getInitResult()` for a synchronous first
11
+ * pass. A real `AvsbClient` satisfies this interface structurally, so no cast
12
+ * is required at the boundary.
13
+ *
14
+ * Why an interface at all: composables take the client from the provider's
15
+ * injected context, so a test (or a Storybook decorator) can inject anything
16
+ * that speaks this surface. `@avsbhq/vue/testing` ships one that is a real
17
+ * `AvsbClient` with the network switched off.
18
+ */
19
+
20
+ interface VueAvsbClient {
21
+ /** Resolves when the init attempt settles. Never rejects. */
22
+ onReady(options?: {
23
+ timeout?: number;
24
+ }): Promise<InitResult>;
25
+ /** True when flags are readable right now (bootstrap, cache, or network). */
26
+ isReady(): boolean;
27
+ /** The settled init outcome, or null while the first attempt is in flight. */
28
+ getInitResult(): InitResult | null;
29
+ /** Adopt a datafile received out of band (SSR bootstrap). */
30
+ applyDatafileBootstrap(datafile: FlagDatafile): void;
31
+ refresh(): Promise<void>;
32
+ flush(): Promise<void>;
33
+ close(): Promise<void>;
34
+ identify(context: EvalContext): void;
35
+ updateAttributes(partial: Record<string, unknown>, contextKind?: string): void;
36
+ /**
37
+ * Queues one `avsb_alias` event and returns. Synchronous: there is nothing
38
+ * to await, and it does not rewrite assignments already made.
39
+ */
40
+ alias(previousContext: EvalContext, newContext: EvalContext): void;
41
+ getContext(): EvalContext;
42
+ /** New anonymous identity, overrides cleared. Call on sign-out. */
43
+ reset(): void;
44
+ getFlag<T>(flagKey: AvsbFlagKey, defaultValue: T, options?: GetFlagOptions): Flag<T>;
45
+ getBoolFlag(flagKey: AvsbFlagKey, defaultValue: boolean, options?: GetFlagOptions): Flag<boolean>;
46
+ getStringFlag(flagKey: AvsbFlagKey, defaultValue: string, options?: GetFlagOptions): Flag<string>;
47
+ getNumberFlag(flagKey: AvsbFlagKey, defaultValue: number, options?: GetFlagOptions): Flag<number>;
48
+ getJsonFlag<T>(flagKey: AvsbFlagKey, defaultValue: T, options?: GetFlagOptions): Flag<T>;
49
+ getAllFlags(options?: GetAllFlagsOptions): Record<string, Flag>;
50
+ /**
51
+ * Called when this flag's value changes, and once when the first datafile
52
+ * is applied. Returns its own unsubscribe function.
53
+ */
54
+ subscribe(flagKey: AvsbFlagKey, listener: () => void): () => void;
55
+ /**
56
+ * Render-safe read: the SAME frozen object until the value changes, no
57
+ * exposure, no `evaluation` event.
58
+ */
59
+ getSnapshot<T>(flagKey: AvsbFlagKey, defaultValue: T): Flag<T>;
60
+ track(eventKey: string, payload?: TrackPayload): void;
61
+ on<E extends ClientEventName>(event: E, cb: ClientEventMap[E]): () => void;
62
+ setOverrideForUser(flagKey: AvsbFlagKey, contextKey: string, variationKey: string, contextKind?: string): void;
63
+ setGlobalOverride(flagKey: AvsbFlagKey, variationKey: string): void;
64
+ clearOverrideForUser(flagKey: AvsbFlagKey, contextKey: string, contextKind?: string): void;
65
+ clearGlobalOverride(flagKey: AvsbFlagKey): void;
66
+ clearAllOverrides(): void;
67
+ }
68
+
69
+ export type { VueAvsbClient as V };