@traffical/svelte 0.1.0

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,206 @@
1
+ /**
2
+ * @traffical/svelte - Type Definitions
3
+ *
4
+ * TypeScript types for the Svelte 5 SDK.
5
+ */
6
+ import type { TrafficalClient, TrafficalPlugin } from "@traffical/js-client";
7
+ import type { ConfigBundle, Context, DecisionResult, ParameterValue } from "@traffical/core";
8
+ /**
9
+ * Configuration for the TrafficalProvider component.
10
+ */
11
+ export interface TrafficalProviderConfig {
12
+ /** Organization ID */
13
+ orgId: string;
14
+ /** Project ID */
15
+ projectId: string;
16
+ /** Environment (e.g., "production", "staging") */
17
+ env: string;
18
+ /** API key for authentication */
19
+ apiKey: string;
20
+ /** Base URL for the SDK API (edge worker) */
21
+ baseUrl?: string;
22
+ /** Local config bundle for offline fallback */
23
+ localConfig?: ConfigBundle;
24
+ /** Refresh interval in milliseconds (default: 60000) */
25
+ refreshIntervalMs?: number;
26
+ /**
27
+ * Function to get the unit key value.
28
+ * If not provided, the SDK will use automatic stable ID generation.
29
+ */
30
+ unitKeyFn?: () => string;
31
+ /** Function to get additional context (optional) */
32
+ contextFn?: () => Context;
33
+ /**
34
+ * Whether to automatically track decision events (default: true).
35
+ * When enabled, every call to decide() automatically sends a DecisionEvent
36
+ * to the control plane, enabling intent-to-treat analysis.
37
+ */
38
+ trackDecisions?: boolean;
39
+ /**
40
+ * Decision deduplication TTL in milliseconds (default: 1 hour).
41
+ * Same user+assignment combination won't be tracked again within this window.
42
+ */
43
+ decisionDeduplicationTtlMs?: number;
44
+ /**
45
+ * Exposure deduplication session TTL in milliseconds (default: 30 minutes).
46
+ * Same user seeing same variant won't trigger multiple exposure events.
47
+ */
48
+ exposureSessionTtlMs?: number;
49
+ /**
50
+ * Plugins to register with the client.
51
+ * The DecisionTrackingPlugin is included by default unless trackDecisions is false.
52
+ */
53
+ plugins?: TrafficalPlugin[];
54
+ /** Max events before auto-flush (default: 10) */
55
+ eventBatchSize?: number;
56
+ /** Auto-flush interval in ms (default: 30000) */
57
+ eventFlushIntervalMs?: number;
58
+ /**
59
+ * Pre-fetched config bundle from server-side load function.
60
+ * When provided, the SDK is immediately ready without fetching.
61
+ */
62
+ initialBundle?: ConfigBundle | null;
63
+ /**
64
+ * Pre-resolved params from SSR for immediate hydration.
65
+ * Used to prevent FOOC (Flash of Original Content).
66
+ */
67
+ initialParams?: Record<string, unknown>;
68
+ }
69
+ /**
70
+ * Internal context value shared via Svelte's setContext/getContext.
71
+ */
72
+ export interface TrafficalContextValue {
73
+ /** The Traffical client instance (null during SSR) */
74
+ readonly client: TrafficalClient | null;
75
+ /** Whether the client is ready (config loaded) */
76
+ readonly ready: boolean;
77
+ /** Any initialization error */
78
+ readonly error: Error | null;
79
+ /** The current config bundle */
80
+ readonly bundle: ConfigBundle | null;
81
+ /** Function to get the unit key */
82
+ getUnitKey: () => string;
83
+ /** Function to get the full context */
84
+ getContext: () => Context;
85
+ /** Initial params from SSR for hydration */
86
+ initialParams?: Record<string, unknown>;
87
+ }
88
+ /**
89
+ * Options for the useTraffical hook.
90
+ */
91
+ export interface UseTrafficalOptions<T extends Record<string, ParameterValue> = Record<string, ParameterValue>> {
92
+ /** Default parameter values (required for type inference) */
93
+ defaults: T;
94
+ /** Additional context to merge (optional) */
95
+ context?: Context;
96
+ /**
97
+ * Tracking mode (default: "full")
98
+ * - "full": Track decision + exposure (default, recommended for UI)
99
+ * - "decision": Track decision only, manual exposure control
100
+ * - "none": No tracking (SSR, internal logic, tests)
101
+ */
102
+ tracking?: "full" | "decision" | "none";
103
+ }
104
+ /**
105
+ * Options for the bound track function returned by useTraffical.
106
+ */
107
+ export interface BoundTrackOptions {
108
+ /** Additional event properties */
109
+ properties?: Record<string, unknown>;
110
+ }
111
+ /**
112
+ * @deprecated Use BoundTrackOptions instead.
113
+ * Options for the bound trackReward function returned by useTraffical.
114
+ */
115
+ export interface BoundTrackRewardOptions {
116
+ /** The reward value (e.g., revenue amount, conversion count) */
117
+ reward: number;
118
+ /** Type of reward (e.g., "revenue", "conversion", "engagement") */
119
+ rewardType?: string;
120
+ /** Multiple reward values keyed by type */
121
+ rewards?: Record<string, number>;
122
+ }
123
+ /**
124
+ * Return value from the useTraffical hook.
125
+ * All properties are reactive via Svelte 5 runes.
126
+ */
127
+ export interface UseTrafficalResult<T extends Record<string, ParameterValue> = Record<string, ParameterValue>> {
128
+ /** Resolved parameter values (reactive) */
129
+ readonly params: T;
130
+ /** The full decision result (null when tracking="none") */
131
+ readonly decision: DecisionResult | null;
132
+ /** Whether the client is ready (config loaded) */
133
+ readonly ready: boolean;
134
+ /** Any error that occurred */
135
+ readonly error: Error | null;
136
+ /** Function to manually track exposure (no-op when tracking="none") */
137
+ trackExposure: () => void;
138
+ /**
139
+ * Track a user event. The decisionId is automatically bound.
140
+ * No-op if tracking="none" or no decision is available.
141
+ *
142
+ * @example
143
+ * track('purchase', { value: 99.99, orderId: 'ord_123' });
144
+ * track('add_to_cart', { itemId: 'sku_456' });
145
+ */
146
+ track: (event: string, properties?: Record<string, unknown>) => void;
147
+ /**
148
+ * @deprecated Use track() instead.
149
+ * Track a reward for this decision. The decisionId is automatically bound.
150
+ * No-op if tracking="none" or no decision is available.
151
+ */
152
+ trackReward: (options: BoundTrackRewardOptions) => void;
153
+ }
154
+ /**
155
+ * Options for tracking an event with the standalone hook.
156
+ */
157
+ export interface TrackEventOptions {
158
+ /** Event name (e.g., 'purchase', 'add_to_cart') */
159
+ event: string;
160
+ /** Additional event properties */
161
+ properties?: Record<string, unknown>;
162
+ /** Reference to the decision (optional, for attribution) */
163
+ decisionId?: string;
164
+ }
165
+ /**
166
+ * @deprecated Use TrackEventOptions instead.
167
+ * Options for tracking a reward.
168
+ */
169
+ export interface TrackRewardOptions {
170
+ /** Reference to the decision (optional, will use last decision if not provided) */
171
+ decisionId?: string;
172
+ /** The reward value (e.g., revenue amount, conversion count) */
173
+ reward: number;
174
+ /** Type of reward (e.g., "revenue", "conversion", "engagement") */
175
+ rewardType?: string;
176
+ /** Multiple reward values keyed by type */
177
+ rewards?: Record<string, number>;
178
+ }
179
+ /**
180
+ * Options for loading the Traffical config bundle in a SvelteKit load function.
181
+ */
182
+ export interface LoadTrafficalBundleOptions {
183
+ /** Organization ID */
184
+ orgId: string;
185
+ /** Project ID */
186
+ projectId: string;
187
+ /** Environment (e.g., "production", "staging") */
188
+ env: string;
189
+ /** API key for authentication */
190
+ apiKey: string;
191
+ /** SvelteKit's fetch function (from load context) */
192
+ fetch: typeof globalThis.fetch;
193
+ /** Base URL for the SDK API (optional, defaults to https://sdk.traffical.io) */
194
+ baseUrl?: string;
195
+ }
196
+ /**
197
+ * Result from loading the Traffical config bundle.
198
+ */
199
+ export interface LoadTrafficalBundleResult {
200
+ /** The fetched config bundle, or null if fetch failed */
201
+ bundle: ConfigBundle | null;
202
+ /** Error message if fetch failed */
203
+ error?: string;
204
+ }
205
+ export type { ConfigBundle, Context, DecisionResult, ParameterValue, TrafficalClient, TrafficalPlugin, };
206
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @traffical/svelte - Type Definitions
3
+ *
4
+ * TypeScript types for the Svelte 5 SDK.
5
+ */
6
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@traffical/svelte",
3
+ "version": "0.1.0",
4
+ "description": "Traffical SDK for Svelte 5 - Provider and hooks for parameter resolution with SSR support",
5
+ "type": "module",
6
+ "svelte": "./src/index.ts",
7
+ "main": "./src/index.ts",
8
+ "module": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "svelte": "./src/index.ts",
14
+ "bun": "./src/index.ts",
15
+ "import": "./src/index.ts"
16
+ },
17
+ "./sveltekit": {
18
+ "types": "./dist/sveltekit.d.ts",
19
+ "bun": "./src/sveltekit.ts",
20
+ "import": "./src/sveltekit.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "scripts": {
28
+ "build": "svelte-package --input src",
29
+ "dev": "svelte-package --input src --watch",
30
+ "test": "bun test",
31
+ "typecheck": "svelte-check --tsconfig ./tsconfig.json"
32
+ },
33
+ "dependencies": {
34
+ "@traffical/core": "workspace:^",
35
+ "@traffical/js-client": "workspace:^"
36
+ },
37
+ "devDependencies": {
38
+ "@sveltejs/package": "^2.3.0",
39
+ "@types/bun": "latest",
40
+ "svelte": "^5.46.4",
41
+ "svelte-check": "^4.0.0",
42
+ "typescript": "^5.3.0"
43
+ },
44
+ "peerDependencies": {
45
+ "svelte": "^5.0.0"
46
+ },
47
+ "keywords": [
48
+ "traffical",
49
+ "feature-flags",
50
+ "experimentation",
51
+ "a/b-testing",
52
+ "svelte",
53
+ "svelte5",
54
+ "sveltekit",
55
+ "runes",
56
+ "ssr"
57
+ ],
58
+ "license": "MIT",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/traffical/js-sdk"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }
67
+
@@ -0,0 +1,34 @@
1
+ <!--
2
+ @traffical/svelte - TrafficalProvider Component
3
+
4
+ Wrapper component that initializes the Traffical context.
5
+ Alternative to calling initTraffical() directly in your layout.
6
+ -->
7
+ <script lang="ts">
8
+ import type { Snippet } from "svelte";
9
+ import { initTraffical } from "./context.svelte.js";
10
+ import type { TrafficalProviderConfig } from "./types.js";
11
+
12
+ interface Props {
13
+ /** Configuration for the Traffical client */
14
+ config: TrafficalProviderConfig;
15
+ /** Child content */
16
+ children: Snippet;
17
+ }
18
+
19
+ let { config, children }: Props = $props();
20
+
21
+ // Initialize context - this sets up the client and makes it available to children
22
+ const context = initTraffical(config);
23
+
24
+ // Cleanup on component destroy
25
+ $effect(() => {
26
+ return () => {
27
+ // Destroy client when provider unmounts
28
+ context.client?.destroy();
29
+ };
30
+ });
31
+ </script>
32
+
33
+ {@render children()}
34
+
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @traffical/svelte - Context Layer
3
+ *
4
+ * SSR-safe context management using Svelte 5 runes and Svelte's context API.
5
+ * Initializes the TrafficalClient with environment-appropriate providers.
6
+ */
7
+
8
+ import { getContext, setContext } from "svelte";
9
+ import {
10
+ TrafficalClient,
11
+ createTrafficalClientSync,
12
+ MemoryStorageProvider,
13
+ LocalStorageProvider,
14
+ } from "@traffical/js-client";
15
+ import type { Context as TrafficalContext } from "@traffical/core";
16
+ import type {
17
+ TrafficalProviderConfig,
18
+ TrafficalContextValue,
19
+ } from "./types.js";
20
+
21
+ // =============================================================================
22
+ // Constants
23
+ // =============================================================================
24
+
25
+ const TRAFFICAL_CONTEXT_KEY = Symbol("traffical");
26
+
27
+ // =============================================================================
28
+ // Browser Detection
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Check if we're running in a browser environment.
33
+ * SSR-safe - returns false during server-side rendering.
34
+ */
35
+ function isBrowser(): boolean {
36
+ return typeof window !== "undefined" && typeof document !== "undefined";
37
+ }
38
+
39
+ // =============================================================================
40
+ // Context State Factory
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Creates the reactive Traffical context state.
45
+ * Uses $state for reactive properties that work with SSR.
46
+ */
47
+ function createTrafficalContextState(
48
+ config: TrafficalProviderConfig
49
+ ): TrafficalContextValue {
50
+ // Reactive state using Svelte 5 runes
51
+ let client = $state<TrafficalClient | null>(null);
52
+ let ready = $state(!!config.initialBundle); // Ready immediately if we have initial bundle
53
+ let error = $state<Error | null>(null);
54
+ let bundle = $state(config.initialBundle ?? null);
55
+
56
+ // Initialize client only in browser
57
+ if (isBrowser()) {
58
+ // Use localStorage in browser, memory storage would lose data
59
+ const storage = new LocalStorageProvider();
60
+
61
+ const clientInstance = createTrafficalClientSync({
62
+ orgId: config.orgId,
63
+ projectId: config.projectId,
64
+ env: config.env,
65
+ apiKey: config.apiKey,
66
+ baseUrl: config.baseUrl,
67
+ localConfig: config.initialBundle ?? config.localConfig,
68
+ refreshIntervalMs: config.refreshIntervalMs,
69
+ storage,
70
+ // Decision tracking options
71
+ trackDecisions: config.trackDecisions,
72
+ decisionDeduplicationTtlMs: config.decisionDeduplicationTtlMs,
73
+ // Exposure options
74
+ exposureSessionTtlMs: config.exposureSessionTtlMs,
75
+ // Event batching options
76
+ eventBatchSize: config.eventBatchSize,
77
+ eventFlushIntervalMs: config.eventFlushIntervalMs,
78
+ // Plugins
79
+ plugins: config.plugins,
80
+ });
81
+
82
+ client = clientInstance;
83
+
84
+ // Initialize asynchronously (fetches fresh config if needed)
85
+ clientInstance
86
+ .initialize()
87
+ .then(() => {
88
+ ready = true;
89
+ // Update bundle if client fetched a newer one
90
+ const configVersion = clientInstance.getConfigVersion();
91
+ if (configVersion) {
92
+ // Bundle is internal, but we track ready state
93
+ }
94
+ })
95
+ .catch((err) => {
96
+ error = err instanceof Error ? err : new Error(String(err));
97
+ // Still mark as ready - we'll use defaults/initial bundle
98
+ ready = true;
99
+ });
100
+ } else {
101
+ // On server, use memory storage and mark as ready if we have initial data
102
+ // The client won't actually be used for tracking on server
103
+ if (config.initialBundle) {
104
+ const storage = new MemoryStorageProvider();
105
+ const clientInstance = createTrafficalClientSync({
106
+ orgId: config.orgId,
107
+ projectId: config.projectId,
108
+ env: config.env,
109
+ apiKey: config.apiKey,
110
+ localConfig: config.initialBundle,
111
+ storage,
112
+ // Disable background operations on server
113
+ refreshIntervalMs: 0,
114
+ });
115
+ client = clientInstance;
116
+ ready = true;
117
+ }
118
+ }
119
+
120
+ // Unit key getter - uses config function or client's stable ID
121
+ function getUnitKey(): string {
122
+ if (config.unitKeyFn) {
123
+ return config.unitKeyFn();
124
+ }
125
+ // Fall back to client's auto-generated stable ID
126
+ return client?.getStableId() ?? "";
127
+ }
128
+
129
+ // Context getter - merges unit key with additional context
130
+ function getContext(): TrafficalContext {
131
+ const unitKey = getUnitKey();
132
+ const additionalContext = config.contextFn?.() ?? {};
133
+
134
+ return {
135
+ ...additionalContext,
136
+ // Include common unit key field names for compatibility
137
+ userId: unitKey,
138
+ deviceId: unitKey,
139
+ anonymousId: unitKey,
140
+ };
141
+ }
142
+
143
+ return {
144
+ get client() {
145
+ return client;
146
+ },
147
+ get ready() {
148
+ return ready;
149
+ },
150
+ get error() {
151
+ return error;
152
+ },
153
+ get bundle() {
154
+ return bundle;
155
+ },
156
+ getUnitKey,
157
+ getContext,
158
+ initialParams: config.initialParams,
159
+ };
160
+ }
161
+
162
+ // =============================================================================
163
+ // Public API
164
+ // =============================================================================
165
+
166
+ /**
167
+ * Initializes the Traffical context.
168
+ * Must be called at the root of your application (e.g., in +layout.svelte).
169
+ *
170
+ * @example
171
+ * ```svelte
172
+ * <script>
173
+ * import { initTraffical } from '@traffical/svelte';
174
+ *
175
+ * let { data, children } = $props();
176
+ *
177
+ * initTraffical({
178
+ * orgId: 'org_123',
179
+ * projectId: 'proj_456',
180
+ * env: 'production',
181
+ * apiKey: 'pk_...',
182
+ * initialBundle: data.traffical?.bundle,
183
+ * });
184
+ * </script>
185
+ *
186
+ * {@render children()}
187
+ * ```
188
+ */
189
+ export function initTraffical(
190
+ config: TrafficalProviderConfig
191
+ ): TrafficalContextValue {
192
+ const contextValue = createTrafficalContextState(config);
193
+ setContext(TRAFFICAL_CONTEXT_KEY, contextValue);
194
+ return contextValue;
195
+ }
196
+
197
+ /**
198
+ * Gets the Traffical context value.
199
+ * Must be called within a component tree where initTraffical() has been called.
200
+ *
201
+ * @throws Error if called outside of Traffical context
202
+ */
203
+ export function getTrafficalContext(): TrafficalContextValue {
204
+ const context = getContext<TrafficalContextValue | undefined>(
205
+ TRAFFICAL_CONTEXT_KEY
206
+ );
207
+
208
+ if (!context) {
209
+ throw new Error(
210
+ "getTrafficalContext() must be called within a component tree where initTraffical() has been called. " +
211
+ "Make sure to call initTraffical() in your root layout or wrap your app with <TrafficalProvider>."
212
+ );
213
+ }
214
+
215
+ return context;
216
+ }
217
+
218
+ /**
219
+ * Checks if Traffical context is available.
220
+ * Useful for conditional rendering or optional Traffical integration.
221
+ */
222
+ export function hasTrafficalContext(): boolean {
223
+ try {
224
+ const context = getContext<TrafficalContextValue | undefined>(
225
+ TRAFFICAL_CONTEXT_KEY
226
+ );
227
+ return context !== undefined;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+