@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.
package/src/index.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @traffical/svelte
3
+ *
4
+ * Traffical SDK for Svelte 5 applications.
5
+ * Provides Provider component and hooks for parameter resolution and decision tracking.
6
+ *
7
+ * Features:
8
+ * - Full SSR/hydration support for SvelteKit
9
+ * - Svelte 5 runes for reactive, fine-grained updates
10
+ * - Browser-optimized with sendBeacon, localStorage persistence
11
+ * - Automatic stable ID for anonymous users
12
+ * - Plugin system support (DecisionTrackingPlugin enabled by default)
13
+ * - Decision and exposure deduplication
14
+ *
15
+ * @example
16
+ * ```svelte
17
+ * <!-- +layout.svelte -->
18
+ * <script>
19
+ * import { TrafficalProvider } from '@traffical/svelte';
20
+ *
21
+ * let { data, children } = $props();
22
+ * </script>
23
+ *
24
+ * <TrafficalProvider
25
+ * config={{
26
+ * orgId: 'org_123',
27
+ * projectId: 'proj_456',
28
+ * env: 'production',
29
+ * apiKey: 'pk_...',
30
+ * initialBundle: data.traffical?.bundle,
31
+ * }}
32
+ * >
33
+ * {@render children()}
34
+ * </TrafficalProvider>
35
+ * ```
36
+ *
37
+ * @example
38
+ * ```svelte
39
+ * <!-- MyComponent.svelte -->
40
+ * <script>
41
+ * import { useTraffical } from '@traffical/svelte';
42
+ *
43
+ * const { params, ready } = useTraffical({
44
+ * defaults: { 'ui.hero.title': 'Welcome' },
45
+ * });
46
+ * </script>
47
+ *
48
+ * {#if ready}
49
+ * <h1>{params['ui.hero.title']}</h1>
50
+ * {:else}
51
+ * <h1>Loading...</h1>
52
+ * {/if}
53
+ * ```
54
+ */
55
+
56
+ // =============================================================================
57
+ // Re-export from @traffical/core
58
+ // =============================================================================
59
+
60
+ export {
61
+ // Resolution functions
62
+ resolveParameters,
63
+ decide,
64
+ evaluateCondition,
65
+ evaluateConditions,
66
+ // Hashing utilities
67
+ fnv1a,
68
+ computeBucket,
69
+ isInBucketRange,
70
+ // ID generation
71
+ generateEventId,
72
+ generateDecisionId,
73
+ generateExposureId,
74
+ generateRewardId,
75
+ } from "@traffical/core";
76
+
77
+ // =============================================================================
78
+ // Re-export from @traffical/js-client
79
+ // =============================================================================
80
+
81
+ export {
82
+ // Client
83
+ TrafficalClient,
84
+ createTrafficalClient,
85
+ createTrafficalClientSync,
86
+ // Storage providers
87
+ LocalStorageProvider,
88
+ MemoryStorageProvider,
89
+ createStorageProvider,
90
+ // Plugins
91
+ createDOMBindingPlugin,
92
+ } from "@traffical/js-client";
93
+
94
+ // =============================================================================
95
+ // Svelte-specific exports
96
+ // =============================================================================
97
+
98
+ // Context
99
+ export {
100
+ initTraffical,
101
+ getTrafficalContext,
102
+ hasTrafficalContext,
103
+ } from "./context.svelte.js";
104
+
105
+ // Hooks
106
+ export {
107
+ useTraffical,
108
+ useTrafficalTrack,
109
+ useTrafficalReward,
110
+ useTrafficalClient,
111
+ useTrafficalPlugin,
112
+ } from "./hooks.svelte.js";
113
+
114
+ // Provider component
115
+ export { default as TrafficalProvider } from "./TrafficalProvider.svelte";
116
+
117
+ // =============================================================================
118
+ // Types
119
+ // =============================================================================
120
+
121
+ export type {
122
+ // Provider config
123
+ TrafficalProviderConfig,
124
+ TrafficalContextValue,
125
+ // Hook types
126
+ UseTrafficalOptions,
127
+ UseTrafficalResult,
128
+ BoundTrackOptions,
129
+ TrackEventOptions,
130
+ // Deprecated types (kept for backward compatibility)
131
+ BoundTrackRewardOptions,
132
+ TrackRewardOptions,
133
+ // SvelteKit types
134
+ LoadTrafficalBundleOptions,
135
+ LoadTrafficalBundleResult,
136
+ // Re-exported types
137
+ ConfigBundle,
138
+ Context,
139
+ DecisionResult,
140
+ ParameterValue,
141
+ TrafficalClient as TrafficalClientType,
142
+ TrafficalPlugin,
143
+ } from "./types.js";
144
+
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers Tests
3
+ */
4
+
5
+ import { describe, test, expect, mock } from "bun:test";
6
+ import { loadTrafficalBundle, resolveParamsSSR } from "./sveltekit.js";
7
+ import type { ConfigBundle } from "@traffical/core";
8
+
9
+ // =============================================================================
10
+ // Test Fixtures
11
+ // =============================================================================
12
+
13
+ const mockBundle: ConfigBundle = {
14
+ version: new Date().toISOString(),
15
+ orgId: "org_test",
16
+ projectId: "proj_test",
17
+ env: "test",
18
+ hashing: {
19
+ unitKey: "userId",
20
+ bucketCount: 10000,
21
+ },
22
+ parameters: [
23
+ {
24
+ key: "checkout.ctaText",
25
+ type: "string",
26
+ default: "Buy Now",
27
+ layerId: "layer_1",
28
+ namespace: "checkout",
29
+ },
30
+ ],
31
+ layers: [
32
+ {
33
+ id: "layer_1",
34
+ policies: [],
35
+ },
36
+ ],
37
+ domBindings: [],
38
+ };
39
+
40
+ // =============================================================================
41
+ // loadTrafficalBundle Tests
42
+ // =============================================================================
43
+
44
+ describe("loadTrafficalBundle", () => {
45
+ test("returns bundle on successful fetch", async () => {
46
+ const mockFetch = mock(() =>
47
+ Promise.resolve({
48
+ ok: true,
49
+ json: () => Promise.resolve(mockBundle),
50
+ } as Response)
51
+ ) as unknown as typeof fetch;
52
+
53
+ const result = await loadTrafficalBundle({
54
+ orgId: "org_123",
55
+ projectId: "proj_456",
56
+ env: "production",
57
+ apiKey: "pk_test",
58
+ fetch: mockFetch,
59
+ });
60
+
61
+ expect(result.bundle).toEqual(mockBundle);
62
+ expect(result.error).toBeUndefined();
63
+ });
64
+
65
+ test("returns null bundle on HTTP error", async () => {
66
+ const mockFetch = mock(() =>
67
+ Promise.resolve({
68
+ ok: false,
69
+ status: 404,
70
+ statusText: "Not Found",
71
+ } as Response)
72
+ ) as unknown as typeof fetch;
73
+
74
+ const result = await loadTrafficalBundle({
75
+ orgId: "org_123",
76
+ projectId: "proj_456",
77
+ env: "production",
78
+ apiKey: "pk_test",
79
+ fetch: mockFetch,
80
+ });
81
+
82
+ expect(result.bundle).toBeNull();
83
+ expect(result.error).toBe("HTTP 404: Not Found");
84
+ });
85
+
86
+ test("returns null bundle on network error", async () => {
87
+ const mockFetch = mock(() =>
88
+ Promise.reject(new Error("Network error"))
89
+ ) as unknown as typeof fetch;
90
+
91
+ const result = await loadTrafficalBundle({
92
+ orgId: "org_123",
93
+ projectId: "proj_456",
94
+ env: "production",
95
+ apiKey: "pk_test",
96
+ fetch: mockFetch,
97
+ });
98
+
99
+ expect(result.bundle).toBeNull();
100
+ expect(result.error).toBe("Network error");
101
+ });
102
+
103
+ test("uses correct URL format", async () => {
104
+ let capturedUrl = "";
105
+ const mockFetch = mock((url: string) => {
106
+ capturedUrl = url;
107
+ return Promise.resolve({
108
+ ok: true,
109
+ json: () => Promise.resolve(mockBundle),
110
+ } as Response);
111
+ }) as unknown as typeof fetch;
112
+
113
+ await loadTrafficalBundle({
114
+ orgId: "org_123",
115
+ projectId: "proj_456",
116
+ env: "staging",
117
+ apiKey: "pk_test",
118
+ fetch: mockFetch,
119
+ });
120
+
121
+ expect(capturedUrl).toBe(
122
+ "https://sdk.traffical.io/v1/config/proj_456?env=staging"
123
+ );
124
+ });
125
+
126
+ test("uses custom baseUrl when provided", async () => {
127
+ let capturedUrl = "";
128
+ const mockFetch = mock((url: string) => {
129
+ capturedUrl = url;
130
+ return Promise.resolve({
131
+ ok: true,
132
+ json: () => Promise.resolve(mockBundle),
133
+ } as Response);
134
+ }) as unknown as typeof fetch;
135
+
136
+ await loadTrafficalBundle({
137
+ orgId: "org_123",
138
+ projectId: "proj_456",
139
+ env: "production",
140
+ apiKey: "pk_test",
141
+ fetch: mockFetch,
142
+ baseUrl: "https://custom.api.com",
143
+ });
144
+
145
+ expect(capturedUrl).toBe(
146
+ "https://custom.api.com/v1/config/proj_456?env=production"
147
+ );
148
+ });
149
+
150
+ test("includes authorization header", async () => {
151
+ let capturedOptions: RequestInit | undefined;
152
+ const mockFetch = mock((_url: string, options?: RequestInit) => {
153
+ capturedOptions = options;
154
+ return Promise.resolve({
155
+ ok: true,
156
+ json: () => Promise.resolve(mockBundle),
157
+ } as Response);
158
+ }) as unknown as typeof fetch;
159
+
160
+ await loadTrafficalBundle({
161
+ orgId: "org_123",
162
+ projectId: "proj_456",
163
+ env: "production",
164
+ apiKey: "pk_secret_key",
165
+ fetch: mockFetch,
166
+ });
167
+
168
+ expect(capturedOptions?.headers).toEqual({
169
+ "Content-Type": "application/json",
170
+ Authorization: "Bearer pk_secret_key",
171
+ });
172
+ });
173
+ });
174
+
175
+ // =============================================================================
176
+ // resolveParamsSSR Tests
177
+ // =============================================================================
178
+
179
+ describe("resolveParamsSSR", () => {
180
+ test("returns defaults when bundle is null", () => {
181
+ const defaults = {
182
+ "checkout.ctaText": "Default",
183
+ };
184
+
185
+ const result = resolveParamsSSR(null, { userId: "user_123" }, defaults);
186
+
187
+ expect(result).toEqual(defaults);
188
+ });
189
+
190
+ test("resolves parameters from bundle", () => {
191
+ const defaults = {
192
+ "checkout.ctaText": "Fallback",
193
+ };
194
+
195
+ const result = resolveParamsSSR(
196
+ mockBundle,
197
+ { userId: "user_123" },
198
+ defaults
199
+ );
200
+
201
+ expect(result["checkout.ctaText"]).toBe("Buy Now");
202
+ });
203
+
204
+ test("uses context for resolution", () => {
205
+ const defaults = {
206
+ "checkout.ctaText": "Fallback",
207
+ };
208
+
209
+ // Both should resolve to the same value since we're using defaults
210
+ const result1 = resolveParamsSSR(mockBundle, { userId: "user_1" }, defaults);
211
+ const result2 = resolveParamsSSR(mockBundle, { userId: "user_2" }, defaults);
212
+
213
+ // Without active policies, results should be the same (bundle defaults)
214
+ expect(result1["checkout.ctaText"]).toBe("Buy Now");
215
+ expect(result2["checkout.ctaText"]).toBe("Buy Now");
216
+ });
217
+ });
218
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers
3
+ *
4
+ * Server-side utilities for SvelteKit load functions.
5
+ * Enables SSR with pre-fetched config bundles.
6
+ */
7
+
8
+ import { resolveParameters } from "@traffical/core";
9
+ import type { ConfigBundle, Context, ParameterValue } from "@traffical/core";
10
+ import type {
11
+ LoadTrafficalBundleOptions,
12
+ LoadTrafficalBundleResult,
13
+ } from "./types.js";
14
+
15
+ // =============================================================================
16
+ // Constants
17
+ // =============================================================================
18
+
19
+ const DEFAULT_BASE_URL = "https://sdk.traffical.io";
20
+
21
+ // =============================================================================
22
+ // Load Functions
23
+ // =============================================================================
24
+
25
+ /**
26
+ * Loads the Traffical config bundle in a SvelteKit load function.
27
+ *
28
+ * Call this in your +layout.server.ts or +page.server.ts to fetch the config
29
+ * bundle on the server, enabling SSR without FOOC (Flash of Original Content).
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // src/routes/+layout.server.ts
34
+ * import { loadTrafficalBundle } from '@traffical/svelte/sveltekit';
35
+ * import { TRAFFICAL_API_KEY } from '$env/static/private';
36
+ *
37
+ * export async function load({ fetch }) {
38
+ * const { bundle } = await loadTrafficalBundle({
39
+ * orgId: 'org_123',
40
+ * projectId: 'proj_456',
41
+ * env: 'production',
42
+ * apiKey: TRAFFICAL_API_KEY,
43
+ * fetch,
44
+ * });
45
+ *
46
+ * return {
47
+ * traffical: { bundle },
48
+ * };
49
+ * }
50
+ * ```
51
+ */
52
+ export async function loadTrafficalBundle(
53
+ options: LoadTrafficalBundleOptions
54
+ ): Promise<LoadTrafficalBundleResult> {
55
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
56
+ const url = `${baseUrl}/v1/config/${options.projectId}?env=${options.env}`;
57
+
58
+ try {
59
+ const response = await options.fetch(url, {
60
+ method: "GET",
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ Authorization: `Bearer ${options.apiKey}`,
64
+ },
65
+ });
66
+
67
+ if (!response.ok) {
68
+ return {
69
+ bundle: null,
70
+ error: `HTTP ${response.status}: ${response.statusText}`,
71
+ };
72
+ }
73
+
74
+ const bundle = (await response.json()) as ConfigBundle;
75
+ return { bundle };
76
+ } catch (err) {
77
+ return {
78
+ bundle: null,
79
+ error: err instanceof Error ? err.message : String(err),
80
+ };
81
+ }
82
+ }
83
+
84
+ // =============================================================================
85
+ // SSR Resolution
86
+ // =============================================================================
87
+
88
+ /**
89
+ * Resolves parameters on the server for SSR.
90
+ *
91
+ * Use this to pre-resolve specific parameters in your load function,
92
+ * enabling server-side rendering with the correct values.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // src/routes/checkout/+page.server.ts
97
+ * import { loadTrafficalBundle, resolveParamsSSR } from '@traffical/svelte/sveltekit';
98
+ *
99
+ * export async function load({ fetch, cookies }) {
100
+ * const { bundle } = await loadTrafficalBundle({ ... });
101
+ *
102
+ * // Get user context from cookies/session
103
+ * const userId = cookies.get('userId');
104
+ *
105
+ * // Pre-resolve params for this page
106
+ * const checkoutParams = resolveParamsSSR(
107
+ * bundle,
108
+ * { userId },
109
+ * {
110
+ * 'checkout.ctaText': 'Buy Now',
111
+ * 'checkout.ctaColor': '#000',
112
+ * }
113
+ * );
114
+ *
115
+ * return {
116
+ * traffical: { bundle },
117
+ * checkoutParams,
118
+ * };
119
+ * }
120
+ * ```
121
+ */
122
+ export function resolveParamsSSR<T extends Record<string, ParameterValue>>(
123
+ bundle: ConfigBundle | null,
124
+ context: Context,
125
+ defaults: T
126
+ ): T {
127
+ if (!bundle) {
128
+ return defaults;
129
+ }
130
+
131
+ return resolveParameters(bundle, context, defaults);
132
+ }
133
+
134
+ // =============================================================================
135
+ // Types Re-export
136
+ // =============================================================================
137
+
138
+ export type { LoadTrafficalBundleOptions, LoadTrafficalBundleResult };
139
+