@traffical/core 0.1.2

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.
Files changed (68) hide show
  1. package/dist/dedup/decision-dedup.d.ts +74 -0
  2. package/dist/dedup/decision-dedup.d.ts.map +1 -0
  3. package/dist/dedup/decision-dedup.js +132 -0
  4. package/dist/dedup/decision-dedup.js.map +1 -0
  5. package/dist/dedup/index.d.ts +5 -0
  6. package/dist/dedup/index.d.ts.map +1 -0
  7. package/dist/dedup/index.js +5 -0
  8. package/dist/dedup/index.js.map +1 -0
  9. package/dist/edge/client.d.ts +109 -0
  10. package/dist/edge/client.d.ts.map +1 -0
  11. package/dist/edge/client.js +154 -0
  12. package/dist/edge/client.js.map +1 -0
  13. package/dist/edge/index.d.ts +7 -0
  14. package/dist/edge/index.d.ts.map +1 -0
  15. package/dist/edge/index.js +7 -0
  16. package/dist/edge/index.js.map +1 -0
  17. package/dist/hashing/bucket.d.ts +56 -0
  18. package/dist/hashing/bucket.d.ts.map +1 -0
  19. package/dist/hashing/bucket.js +89 -0
  20. package/dist/hashing/bucket.js.map +1 -0
  21. package/dist/hashing/fnv1a.d.ts +17 -0
  22. package/dist/hashing/fnv1a.d.ts.map +1 -0
  23. package/dist/hashing/fnv1a.js +27 -0
  24. package/dist/hashing/fnv1a.js.map +1 -0
  25. package/dist/hashing/index.d.ts +8 -0
  26. package/dist/hashing/index.d.ts.map +1 -0
  27. package/dist/hashing/index.js +8 -0
  28. package/dist/hashing/index.js.map +1 -0
  29. package/dist/ids/index.d.ts +83 -0
  30. package/dist/ids/index.d.ts.map +1 -0
  31. package/dist/ids/index.js +165 -0
  32. package/dist/ids/index.js.map +1 -0
  33. package/dist/index.d.ts +20 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +32 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/resolution/conditions.d.ts +81 -0
  38. package/dist/resolution/conditions.d.ts.map +1 -0
  39. package/dist/resolution/conditions.js +197 -0
  40. package/dist/resolution/conditions.js.map +1 -0
  41. package/dist/resolution/engine.d.ts +54 -0
  42. package/dist/resolution/engine.d.ts.map +1 -0
  43. package/dist/resolution/engine.js +382 -0
  44. package/dist/resolution/engine.js.map +1 -0
  45. package/dist/resolution/index.d.ts +8 -0
  46. package/dist/resolution/index.d.ts.map +1 -0
  47. package/dist/resolution/index.js +10 -0
  48. package/dist/resolution/index.js.map +1 -0
  49. package/dist/types/index.d.ts +440 -0
  50. package/dist/types/index.d.ts.map +1 -0
  51. package/dist/types/index.js +8 -0
  52. package/dist/types/index.js.map +1 -0
  53. package/package.json +51 -0
  54. package/src/dedup/decision-dedup.ts +175 -0
  55. package/src/dedup/index.ts +6 -0
  56. package/src/edge/client.ts +256 -0
  57. package/src/edge/index.ts +16 -0
  58. package/src/hashing/bucket.ts +115 -0
  59. package/src/hashing/fnv1a.test.ts +87 -0
  60. package/src/hashing/fnv1a.ts +31 -0
  61. package/src/hashing/index.ts +15 -0
  62. package/src/ids/index.ts +221 -0
  63. package/src/index.ts +136 -0
  64. package/src/resolution/conditions.ts +253 -0
  65. package/src/resolution/engine.test.ts +242 -0
  66. package/src/resolution/engine.ts +480 -0
  67. package/src/resolution/index.ts +32 -0
  68. package/src/types/index.ts +508 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * ID Generation Utilities
3
+ *
4
+ * Provides consistent ID generation with type prefixes for all entities and events.
5
+ *
6
+ * Entity IDs: 8-character NanoID with prefix (e.g., "proj_hVF1cCoC")
7
+ * - Compact and URL-friendly
8
+ * - 64^8 = 281 trillion combinations
9
+ * - With DB constraints, collisions are handled via retry
10
+ *
11
+ * Event IDs: ULID with prefix (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
12
+ * - Lexicographically sortable (time-ordered)
13
+ * - Contains millisecond timestamp for analytics
14
+ */
15
+
16
+ import { customAlphabet } from "nanoid";
17
+ import { ulid } from "ulid";
18
+
19
+ // =============================================================================
20
+ // NanoID Configuration
21
+ // =============================================================================
22
+
23
+ /**
24
+ * URL-safe alphabet for NanoID (64 characters).
25
+ * Includes: 0-9, A-Z, a-z (no special chars to avoid URL encoding issues)
26
+ */
27
+ const NANOID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
28
+
29
+ /**
30
+ * Default length for entity IDs (without prefix).
31
+ * 64^8 = 281,474,976,710,656 (~281 trillion) combinations.
32
+ */
33
+ const ENTITY_ID_LENGTH = 8;
34
+
35
+ /**
36
+ * NanoID generator with custom alphabet.
37
+ */
38
+ const nanoid = customAlphabet(NANOID_ALPHABET, ENTITY_ID_LENGTH);
39
+
40
+ // =============================================================================
41
+ // ID Prefixes
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Entity ID prefixes for each entity type.
46
+ */
47
+ export type EntityIdPrefix =
48
+ | "org" // Organization
49
+ | "proj" // Project
50
+ | "env" // Environment
51
+ | "ns" // Namespace
52
+ | "lay" // Layer
53
+ | "pol" // Policy
54
+ | "alloc" // Allocation
55
+ | "param" // Parameter
56
+ | "dom" // DOM Binding
57
+ | "ovr" // Environment Override
58
+ | "ak"; // API Key
59
+
60
+ /**
61
+ * Event ID prefixes for each event type.
62
+ */
63
+ export type EventIdPrefix = "dec" | "exp" | "trk";
64
+
65
+ // =============================================================================
66
+ // Generic ID Generation
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Generates a prefixed 8-char NanoID for the specified entity type.
71
+ *
72
+ * @param prefix - The entity type prefix
73
+ * @returns A prefixed NanoID string (e.g., "proj_hVF1cCoC")
74
+ */
75
+ export function generateEntityId(prefix: EntityIdPrefix): string {
76
+ return `${prefix}_${nanoid()}`;
77
+ }
78
+
79
+ /**
80
+ * Generates a prefixed ULID for the specified event type.
81
+ * Events use ULID for time-sortability in analytics.
82
+ *
83
+ * @param prefix - The event type prefix
84
+ * @returns A prefixed ULID string (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
85
+ */
86
+ export function generateEventId(prefix: EventIdPrefix): string {
87
+ return `${prefix}_${ulid()}`;
88
+ }
89
+
90
+ /**
91
+ * Generates a plain 8-char NanoID without prefix.
92
+ * Used for internal IDs that don't need type identification.
93
+ */
94
+ export function generateShortId(): string {
95
+ return nanoid();
96
+ }
97
+
98
+ // =============================================================================
99
+ // Entity ID Convenience Functions
100
+ // =============================================================================
101
+
102
+ /** Generates an Organization ID with "org_" prefix */
103
+ export function generateOrgId(): string {
104
+ return generateEntityId("org");
105
+ }
106
+
107
+ /** Generates a Project ID with "proj_" prefix */
108
+ export function generateProjectId(): string {
109
+ return generateEntityId("proj");
110
+ }
111
+
112
+ /** Generates an Environment ID with "env_" prefix */
113
+ export function generateEnvironmentId(): string {
114
+ return generateEntityId("env");
115
+ }
116
+
117
+ /** Generates a Namespace ID with "ns_" prefix */
118
+ export function generateNamespaceId(): string {
119
+ return generateEntityId("ns");
120
+ }
121
+
122
+ /** Generates a Layer ID with "lay_" prefix */
123
+ export function generateLayerId(): string {
124
+ return generateEntityId("lay");
125
+ }
126
+
127
+ /** Generates a Policy ID with "pol_" prefix */
128
+ export function generatePolicyId(): string {
129
+ return generateEntityId("pol");
130
+ }
131
+
132
+ /** Generates an Allocation ID with "alloc_" prefix */
133
+ export function generateAllocationId(): string {
134
+ return generateEntityId("alloc");
135
+ }
136
+
137
+ /** Generates a Parameter ID with "param_" prefix */
138
+ export function generateParameterId(): string {
139
+ return generateEntityId("param");
140
+ }
141
+
142
+ /** Generates a DOM Binding ID with "dom_" prefix */
143
+ export function generateDomBindingId(): string {
144
+ return generateEntityId("dom");
145
+ }
146
+
147
+ /** Generates an Environment Override ID with "ovr_" prefix */
148
+ export function generateOverrideId(): string {
149
+ return generateEntityId("ovr");
150
+ }
151
+
152
+ /** Generates an API Key ID with "ak_" prefix */
153
+ export function generateApiKeyId(): string {
154
+ return generateEntityId("ak");
155
+ }
156
+
157
+ // =============================================================================
158
+ // Event ID Convenience Functions (Keep ULID for time-sortability)
159
+ // =============================================================================
160
+
161
+ /** Generates a Decision event ID with "dec_" prefix (ULID) */
162
+ export function generateDecisionId(): string {
163
+ return generateEventId("dec");
164
+ }
165
+
166
+ /** Generates an Exposure event ID with "exp_" prefix (ULID) */
167
+ export function generateExposureId(): string {
168
+ return generateEventId("exp");
169
+ }
170
+
171
+ /** Generates a Track event ID with "trk_" prefix (ULID) */
172
+ export function generateTrackEventId(): string {
173
+ return generateEventId("trk");
174
+ }
175
+
176
+ // =============================================================================
177
+ // Utilities
178
+ // =============================================================================
179
+
180
+ /**
181
+ * Extracts the timestamp from a ULID-based ID.
182
+ * Only works for event IDs (ULID format).
183
+ *
184
+ * @param id - A prefixed ULID (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
185
+ * @returns The timestamp as a Date, or null if invalid
186
+ */
187
+ export function getIdTimestamp(id: string): Date | null {
188
+ // Extract the ULID part (after the prefix and underscore)
189
+ const parts = id.split("_");
190
+ if (parts.length < 2) {
191
+ return null;
192
+ }
193
+
194
+ const ulidPart = parts[1];
195
+ if (!ulidPart || ulidPart.length !== 26) {
196
+ return null;
197
+ }
198
+
199
+ // ULID timestamp is encoded in the first 10 characters (Crockford's Base32)
200
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
201
+ const TIME_LEN = 10;
202
+
203
+ let time = 0;
204
+ for (let i = 0; i < TIME_LEN; i++) {
205
+ const char = ulidPart.charAt(i).toUpperCase();
206
+ const index = ENCODING.indexOf(char);
207
+ if (index === -1) {
208
+ return null;
209
+ }
210
+ time = time * 32 + index;
211
+ }
212
+
213
+ return new Date(time);
214
+ }
215
+
216
+ /**
217
+ * @deprecated Use getIdTimestamp instead
218
+ */
219
+ export function getEventIdTimestamp(eventId: string): Date | null {
220
+ return getIdTimestamp(eventId);
221
+ }
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @traffical/core
3
+ *
4
+ * Pure TypeScript core for Traffical SDK.
5
+ * This package performs no I/O and can be used in any JavaScript environment.
6
+ *
7
+ * Key features:
8
+ * - Deterministic parameter resolution
9
+ * - FNV-1a hashing for bucket assignment
10
+ * - Condition evaluation for targeting
11
+ * - Defaults-based graceful degradation
12
+ */
13
+
14
+ // Types
15
+ export type {
16
+ // Base types
17
+ Timestamp,
18
+ Id,
19
+ ParameterType,
20
+ ParameterValue,
21
+ Context,
22
+ // Bundle types
23
+ ConfigBundle,
24
+ BundleHashingConfig,
25
+ BundleParameter,
26
+ BundleDOMBinding,
27
+ BundleLayer,
28
+ BundlePolicy,
29
+ BundleAllocation,
30
+ BundleCondition,
31
+ PolicyState,
32
+ PolicyKind,
33
+ ConditionOperator,
34
+ // Per-entity types
35
+ EntityConfig,
36
+ EntityWeights,
37
+ BundleEntityPolicyState,
38
+ // SDK types
39
+ ParameterDefaults,
40
+ DecisionResult,
41
+ DecisionMetadata,
42
+ LayerResolution,
43
+ // Event types
44
+ BaseEventFields,
45
+ ExposureEvent,
46
+ TrackEvent,
47
+ TrackAttribution,
48
+ DecisionEvent,
49
+ TrackableEvent,
50
+ // Client types
51
+ TrafficalClientOptions,
52
+ GetParamsOptions,
53
+ DecideOptions,
54
+ TrackOptions,
55
+ } from "./types/index.js";
56
+
57
+ // Hashing
58
+ export {
59
+ fnv1a,
60
+ computeBucket,
61
+ isInBucketRange,
62
+ findMatchingAllocation,
63
+ percentageToBucketRange,
64
+ createBucketRanges,
65
+ } from "./hashing/index.js";
66
+
67
+ // Resolution
68
+ export {
69
+ resolveParameters,
70
+ decide,
71
+ getUnitKeyValue,
72
+ evaluateCondition,
73
+ evaluateConditions,
74
+ // Condition builders
75
+ eq,
76
+ neq,
77
+ inValues,
78
+ notIn,
79
+ gt,
80
+ gte,
81
+ lt,
82
+ lte,
83
+ contains,
84
+ startsWith,
85
+ endsWith,
86
+ regex,
87
+ exists,
88
+ notExists,
89
+ } from "./resolution/index.js";
90
+
91
+ // Deduplication
92
+ export {
93
+ DecisionDeduplicator,
94
+ type DecisionDeduplicatorOptions,
95
+ } from "./dedup/index.js";
96
+
97
+ // ID Generation
98
+ export {
99
+ // Event IDs (ULID - time-sortable)
100
+ generateEventId,
101
+ generateDecisionId,
102
+ generateExposureId,
103
+ generateTrackEventId,
104
+ // Entity IDs (8-char NanoID)
105
+ generateEntityId,
106
+ generateShortId,
107
+ generateOrgId,
108
+ generateProjectId,
109
+ generateEnvironmentId,
110
+ generateNamespaceId,
111
+ generateLayerId,
112
+ generatePolicyId,
113
+ generateAllocationId,
114
+ generateParameterId,
115
+ generateDomBindingId,
116
+ generateOverrideId,
117
+ generateApiKeyId,
118
+ // Utilities
119
+ getIdTimestamp,
120
+ getEventIdTimestamp, // deprecated
121
+ // Types
122
+ type EventIdPrefix,
123
+ type EntityIdPrefix,
124
+ } from "./ids/index.js";
125
+
126
+ // Edge Client (for per-entity policies)
127
+ export {
128
+ EdgeClient,
129
+ createEdgeDecideRequest,
130
+ type EdgeClientConfig,
131
+ type EdgeDecideRequest,
132
+ type EdgeDecideResponse,
133
+ type EdgeBatchDecideRequest,
134
+ type EdgeBatchDecideResponse,
135
+ } from "./edge/index.js";
136
+
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Condition Evaluation
3
+ *
4
+ * Evaluates context predicates to determine policy eligibility.
5
+ * Conditions are AND-ed together: all must match for a policy to apply.
6
+ */
7
+
8
+ import type { Context, BundleCondition } from "../types/index.js";
9
+
10
+ /**
11
+ * Evaluates a single condition against a context.
12
+ *
13
+ * @param condition - The condition to evaluate
14
+ * @param context - The context to evaluate against
15
+ * @returns True if the condition matches
16
+ */
17
+ export function evaluateCondition(
18
+ condition: BundleCondition,
19
+ context: Context
20
+ ): boolean {
21
+ const { field, op, value, values } = condition;
22
+
23
+ // Get the context value using dot notation
24
+ const contextValue = getNestedValue(context, field);
25
+
26
+ switch (op) {
27
+ case "eq":
28
+ return contextValue === value;
29
+
30
+ case "neq":
31
+ return contextValue !== value;
32
+
33
+ case "in":
34
+ if (!Array.isArray(values)) return false;
35
+ return values.includes(contextValue);
36
+
37
+ case "nin":
38
+ if (!Array.isArray(values)) return true;
39
+ return !values.includes(contextValue);
40
+
41
+ case "gt":
42
+ return (
43
+ typeof contextValue === "number" && contextValue > (value as number)
44
+ );
45
+
46
+ case "gte":
47
+ return (
48
+ typeof contextValue === "number" && contextValue >= (value as number)
49
+ );
50
+
51
+ case "lt":
52
+ return (
53
+ typeof contextValue === "number" && contextValue < (value as number)
54
+ );
55
+
56
+ case "lte":
57
+ return (
58
+ typeof contextValue === "number" && contextValue <= (value as number)
59
+ );
60
+
61
+ case "contains":
62
+ return (
63
+ typeof contextValue === "string" &&
64
+ typeof value === "string" &&
65
+ contextValue.includes(value)
66
+ );
67
+
68
+ case "startsWith":
69
+ return (
70
+ typeof contextValue === "string" &&
71
+ typeof value === "string" &&
72
+ contextValue.startsWith(value)
73
+ );
74
+
75
+ case "endsWith":
76
+ return (
77
+ typeof contextValue === "string" &&
78
+ typeof value === "string" &&
79
+ contextValue.endsWith(value)
80
+ );
81
+
82
+ case "regex":
83
+ if (typeof contextValue !== "string" || typeof value !== "string") {
84
+ return false;
85
+ }
86
+ try {
87
+ const regex = new RegExp(value);
88
+ return regex.test(contextValue);
89
+ } catch {
90
+ return false;
91
+ }
92
+
93
+ case "exists":
94
+ return contextValue !== undefined && contextValue !== null;
95
+
96
+ case "notExists":
97
+ return contextValue === undefined || contextValue === null;
98
+
99
+ default:
100
+ // Unknown operator, fail safe by not matching
101
+ return false;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Evaluates all conditions against a context.
107
+ * All conditions must match (AND logic).
108
+ *
109
+ * @param conditions - Array of conditions
110
+ * @param context - The context to evaluate against
111
+ * @returns True if all conditions match (or if there are no conditions)
112
+ */
113
+ export function evaluateConditions(
114
+ conditions: BundleCondition[],
115
+ context: Context
116
+ ): boolean {
117
+ // Empty conditions = always match
118
+ if (conditions.length === 0) {
119
+ return true;
120
+ }
121
+
122
+ // All conditions must match (AND)
123
+ return conditions.every((condition) => evaluateCondition(condition, context));
124
+ }
125
+
126
+ /**
127
+ * Gets a nested value from an object using dot notation.
128
+ *
129
+ * @example
130
+ * getNestedValue({ user: { name: "Alice" } }, "user.name") // "Alice"
131
+ * getNestedValue({ tags: ["a", "b"] }, "tags.0") // "a"
132
+ */
133
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
134
+ const parts = path.split(".");
135
+ let current: unknown = obj;
136
+
137
+ for (const part of parts) {
138
+ if (current === null || current === undefined) {
139
+ return undefined;
140
+ }
141
+
142
+ if (typeof current === "object") {
143
+ current = (current as Record<string, unknown>)[part];
144
+ } else {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ return current;
150
+ }
151
+
152
+ // =============================================================================
153
+ // Condition Builder Helpers
154
+ // =============================================================================
155
+
156
+ /**
157
+ * Creates an equality condition.
158
+ */
159
+ export function eq(field: string, value: unknown): BundleCondition {
160
+ return { field, op: "eq", value };
161
+ }
162
+
163
+ /**
164
+ * Creates a not-equal condition.
165
+ */
166
+ export function neq(field: string, value: unknown): BundleCondition {
167
+ return { field, op: "neq", value };
168
+ }
169
+
170
+ /**
171
+ * Creates an "in" condition.
172
+ */
173
+ export function inValues(field: string, values: unknown[]): BundleCondition {
174
+ return { field, op: "in", values };
175
+ }
176
+
177
+ /**
178
+ * Creates a "not in" condition.
179
+ */
180
+ export function notIn(field: string, values: unknown[]): BundleCondition {
181
+ return { field, op: "nin", values };
182
+ }
183
+
184
+ /**
185
+ * Creates a greater-than condition.
186
+ */
187
+ export function gt(field: string, value: number): BundleCondition {
188
+ return { field, op: "gt", value };
189
+ }
190
+
191
+ /**
192
+ * Creates a greater-than-or-equal condition.
193
+ */
194
+ export function gte(field: string, value: number): BundleCondition {
195
+ return { field, op: "gte", value };
196
+ }
197
+
198
+ /**
199
+ * Creates a less-than condition.
200
+ */
201
+ export function lt(field: string, value: number): BundleCondition {
202
+ return { field, op: "lt", value };
203
+ }
204
+
205
+ /**
206
+ * Creates a less-than-or-equal condition.
207
+ */
208
+ export function lte(field: string, value: number): BundleCondition {
209
+ return { field, op: "lte", value };
210
+ }
211
+
212
+ /**
213
+ * Creates a string contains condition.
214
+ */
215
+ export function contains(field: string, value: string): BundleCondition {
216
+ return { field, op: "contains", value };
217
+ }
218
+
219
+ /**
220
+ * Creates a string starts-with condition.
221
+ */
222
+ export function startsWith(field: string, value: string): BundleCondition {
223
+ return { field, op: "startsWith", value };
224
+ }
225
+
226
+ /**
227
+ * Creates a string ends-with condition.
228
+ */
229
+ export function endsWith(field: string, value: string): BundleCondition {
230
+ return { field, op: "endsWith", value };
231
+ }
232
+
233
+ /**
234
+ * Creates a regex match condition.
235
+ */
236
+ export function regex(field: string, pattern: string): BundleCondition {
237
+ return { field, op: "regex", value: pattern };
238
+ }
239
+
240
+ /**
241
+ * Creates an exists condition.
242
+ */
243
+ export function exists(field: string): BundleCondition {
244
+ return { field, op: "exists" };
245
+ }
246
+
247
+ /**
248
+ * Creates a not-exists condition.
249
+ */
250
+ export function notExists(field: string): BundleCondition {
251
+ return { field, op: "notExists" };
252
+ }
253
+