@launchwhitly/sdk 0.1.3

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,34 @@
1
+ import { evaluate } from './rollout';
2
+ import type { EvalContext, EvaluateResult, FlagState } from './types';
3
+ import type { BootstrapResponse, LaunchwhitlyClientOptions, SnapshotListener } from './types';
4
+ type ClientSnapshot = BootstrapResponse;
5
+ export declare class LaunchwhitlyClient {
6
+ private readonly baseUrl;
7
+ private readonly projectKey;
8
+ private readonly environmentKey;
9
+ private readonly fetchImpl;
10
+ private readonly pollingIntervalMs;
11
+ private readonly reconnectDelayMs;
12
+ private snapshot;
13
+ private listeners;
14
+ private closed;
15
+ private started;
16
+ private initPromise;
17
+ private streamAbortController;
18
+ private pollAbortController;
19
+ constructor(options: LaunchwhitlyClientOptions);
20
+ init(): Promise<ClientSnapshot>;
21
+ getSnapshot(): ClientSnapshot | null;
22
+ getFlag(flagKey: string): FlagState | undefined;
23
+ evaluate(flagKey: string, context: EvalContext): EvaluateResult;
24
+ evaluateAll(context: EvalContext): Record<string, ReturnType<typeof evaluate>>;
25
+ subscribe(listener: SnapshotListener): () => void;
26
+ refresh(): Promise<ClientSnapshot>;
27
+ close(): void;
28
+ private setSnapshot;
29
+ private runStreamLoop;
30
+ private runPollLoop;
31
+ }
32
+ export declare function createClient(options: LaunchwhitlyClientOptions): LaunchwhitlyClient;
33
+ export {};
34
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAe,MAAM,WAAW,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAa,SAAS,EAAE,MAAM,SAAS,CAAA;AAChF,OAAO,KAAK,EACV,iBAAiB,EACjB,yBAAyB,EACzB,gBAAgB,EACjB,MAAM,SAAS,CAAA;AAGhB,KAAK,cAAc,GAAG,iBAAiB,CAAA;AAwCvC,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,qBAAqB,CAA+B;IAC5D,OAAO,CAAC,mBAAmB,CAA+B;gBAE9C,OAAO,EAAE,yBAAyB;IASxC,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC;IAqBrC,WAAW,IAAI,cAAc,GAAG,IAAI;IAIpC,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI/C,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,cAAc;IAM/D,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;IAK9E,SAAS,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAU3C,OAAO,IAAI,OAAO,CAAC,cAAc,CAAC;IAWxC,KAAK;IAOL,OAAO,CAAC,WAAW;YAWL,aAAa;YAiDb,WAAW;CAmB1B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,yBAAyB,sBAE9D"}
package/dist/client.js ADDED
@@ -0,0 +1,179 @@
1
+ import { evaluate, evaluateAll } from './rollout';
2
+ import { buildUrl, openEventStream, requestJson } from './transport';
3
+ function sleep(ms, signal) {
4
+ return new Promise((resolve, reject) => {
5
+ const timeout = setTimeout(() => resolve(), ms);
6
+ if (!signal)
7
+ return;
8
+ const abortHandler = () => {
9
+ clearTimeout(timeout);
10
+ reject(new DOMException('Aborted', 'AbortError'));
11
+ };
12
+ if (signal.aborted) {
13
+ abortHandler();
14
+ return;
15
+ }
16
+ signal.addEventListener('abort', abortHandler, { once: true });
17
+ });
18
+ }
19
+ function makeHeaders(projectKey, environmentKey, lastEventId) {
20
+ const headers = new Headers({
21
+ accept: 'application/json',
22
+ 'x-launchwhitly-project-key': projectKey,
23
+ 'x-launchwhitly-environment-key': environmentKey,
24
+ });
25
+ if (lastEventId !== undefined) {
26
+ headers.set('last-event-id', String(lastEventId));
27
+ }
28
+ return headers;
29
+ }
30
+ export class LaunchwhitlyClient {
31
+ constructor(options) {
32
+ this.snapshot = null;
33
+ this.listeners = new Set();
34
+ this.closed = false;
35
+ this.started = false;
36
+ this.initPromise = null;
37
+ this.streamAbortController = null;
38
+ this.pollAbortController = null;
39
+ this.baseUrl = options.baseUrl;
40
+ this.projectKey = options.projectKey;
41
+ this.environmentKey = options.environmentKey;
42
+ this.fetchImpl = options.fetchImpl ?? fetch;
43
+ this.pollingIntervalMs = options.pollingIntervalMs ?? 30000;
44
+ this.reconnectDelayMs = options.reconnectDelayMs ?? 2000;
45
+ }
46
+ async init() {
47
+ if (this.started && this.snapshot)
48
+ return this.snapshot;
49
+ if (this.initPromise)
50
+ return this.initPromise;
51
+ this.initPromise = (async () => {
52
+ const snapshot = await this.refresh();
53
+ this.started = true;
54
+ if (!this.closed) {
55
+ void this.runStreamLoop();
56
+ void this.runPollLoop();
57
+ }
58
+ return snapshot;
59
+ })();
60
+ try {
61
+ return await this.initPromise;
62
+ }
63
+ finally {
64
+ this.initPromise = null;
65
+ }
66
+ }
67
+ getSnapshot() {
68
+ return this.snapshot;
69
+ }
70
+ getFlag(flagKey) {
71
+ return this.snapshot?.cache.flags[flagKey];
72
+ }
73
+ evaluate(flagKey, context) {
74
+ const flag = this.getFlag(flagKey);
75
+ if (!flag || !this.snapshot)
76
+ return null;
77
+ return evaluate(flag, context, this.snapshot.cache.segments);
78
+ }
79
+ evaluateAll(context) {
80
+ if (!this.snapshot)
81
+ return {};
82
+ return evaluateAll(this.snapshot.cache.flags, context, this.snapshot.cache.segments);
83
+ }
84
+ subscribe(listener) {
85
+ this.listeners.add(listener);
86
+ if (this.snapshot)
87
+ listener(this.snapshot);
88
+ void this.init().catch(() => undefined);
89
+ return () => {
90
+ this.listeners.delete(listener);
91
+ };
92
+ }
93
+ async refresh() {
94
+ const url = buildUrl(this.baseUrl, '/api/v1/sdk/config');
95
+ const snapshot = await requestJson(this.fetchImpl, url, {
96
+ method: 'GET',
97
+ headers: makeHeaders(this.projectKey, this.environmentKey),
98
+ });
99
+ this.setSnapshot(snapshot);
100
+ return snapshot;
101
+ }
102
+ close() {
103
+ this.closed = true;
104
+ this.streamAbortController?.abort();
105
+ this.pollAbortController?.abort();
106
+ this.listeners.clear();
107
+ }
108
+ setSnapshot(snapshot) {
109
+ const previousVersion = this.snapshot?.cache.version ?? 0;
110
+ this.snapshot = snapshot;
111
+ if (snapshot.cache.version <= previousVersion)
112
+ return;
113
+ for (const listener of this.listeners) {
114
+ listener(snapshot);
115
+ }
116
+ }
117
+ async runStreamLoop() {
118
+ while (!this.closed) {
119
+ this.streamAbortController?.abort();
120
+ this.streamAbortController = new AbortController();
121
+ try {
122
+ await openEventStream(this.fetchImpl, buildUrl(this.baseUrl, '/api/v1/sdk/stream'), {
123
+ method: 'GET',
124
+ signal: this.streamAbortController.signal,
125
+ headers: makeHeaders(this.projectKey, this.environmentKey, this.snapshot?.cache.version),
126
+ }, (event) => {
127
+ if (event.event !== 'config' || !event.data)
128
+ return;
129
+ const cache = JSON.parse(event.data);
130
+ this.setSnapshot({
131
+ project: this.snapshot?.project ?? {
132
+ id: '',
133
+ name: '',
134
+ slug: '',
135
+ sdkKeyPrefix: '',
136
+ },
137
+ environment: this.snapshot?.environment ?? {
138
+ id: cache.environmentId,
139
+ name: '',
140
+ slug: '',
141
+ color: '',
142
+ sdkKeyPrefix: '',
143
+ },
144
+ cache,
145
+ });
146
+ });
147
+ }
148
+ catch {
149
+ if (this.closed)
150
+ return;
151
+ }
152
+ if (this.closed)
153
+ return;
154
+ await sleep(this.reconnectDelayMs, this.streamAbortController.signal).catch(() => undefined);
155
+ }
156
+ }
157
+ async runPollLoop() {
158
+ this.pollAbortController = new AbortController();
159
+ while (!this.closed) {
160
+ try {
161
+ await sleep(this.pollingIntervalMs, this.pollAbortController.signal);
162
+ }
163
+ catch {
164
+ return;
165
+ }
166
+ if (this.closed)
167
+ return;
168
+ try {
169
+ await this.refresh();
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ }
175
+ }
176
+ }
177
+ export function createClient(options) {
178
+ return new LaunchwhitlyClient(options);
179
+ }
@@ -0,0 +1,4 @@
1
+ export { createClient, LaunchwhitlyClient } from './client';
2
+ export type { BootstrapResponse, LaunchwhitlyClientOptions, SnapshotListener, EvalContext, EvalResult, FlagCache, FlagState, } from './types';
3
+ export { evaluate, evaluateAll, getBucket, matchesSegment, } from './rollout';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAC3D,YAAY,EACV,iBAAiB,EACjB,yBAAyB,EACzB,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,SAAS,EACT,SAAS,GACV,MAAM,SAAS,CAAA;AAChB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,cAAc,GACf,MAAM,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createClient, LaunchwhitlyClient } from './client';
2
+ export { evaluate, evaluateAll, getBucket, matchesSegment, } from './rollout';
@@ -0,0 +1,2 @@
1
+ export declare function getBucket(userId: string, flagKey: string): number;
2
+ //# sourceMappingURL=bucketing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bucketing.d.ts","sourceRoot":"","sources":["../../src/rollout/bucketing.ts"],"names":[],"mappings":"AAcA,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjE"}
@@ -0,0 +1,15 @@
1
+ const FNV_OFFSET_BASIS = 0x811c9dc5;
2
+ const FNV_PRIME = 0x01000193;
3
+ function fnv1a32(input) {
4
+ let hash = FNV_OFFSET_BASIS;
5
+ for (let i = 0; i < input.length; i += 1) {
6
+ hash ^= input.charCodeAt(i);
7
+ hash = Math.imul(hash, FNV_PRIME);
8
+ }
9
+ return hash;
10
+ }
11
+ export function getBucket(userId, flagKey) {
12
+ if (!userId || !flagKey)
13
+ return 0;
14
+ return (fnv1a32(`${userId}:${flagKey}`) >>> 0) % 100;
15
+ }
@@ -0,0 +1,4 @@
1
+ import type { EvalContext, EvalResult, FlagState, SegmentDef } from './types';
2
+ export declare function evaluate(flag: FlagState, context: EvalContext, segments?: Record<string, SegmentDef>): EvalResult;
3
+ export declare function evaluateAll(flags: Record<string, FlagState>, context: EvalContext, segments?: Record<string, SegmentDef>): Record<string, EvalResult>;
4
+ //# sourceMappingURL=evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/rollout/evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAW,MAAM,SAAS,CAAA;AAkCtF,wBAAgB,QAAQ,CACtB,IAAI,EAAE,SAAS,EACf,OAAO,EAAE,WAAW,EACpB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,GACxC,UAAU,CAsDZ;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAChC,OAAO,EAAE,WAAW,EACpB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,GACxC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAgB5B"}
@@ -0,0 +1,90 @@
1
+ import { getBucket } from './bucketing';
2
+ import { evaluateRule } from './targeting';
3
+ function disabledResult(defaultValue, reason) {
4
+ return { enabled: false, value: defaultValue, reason };
5
+ }
6
+ function variantResult(variant) {
7
+ return {
8
+ enabled: true,
9
+ value: variant.value,
10
+ reason: 'targeting_match',
11
+ variant: variant.key,
12
+ };
13
+ }
14
+ function pickVariant(variants, bucket) {
15
+ if (variants.length === 0)
16
+ return undefined;
17
+ const totalWeight = variants.reduce((sum, variant) => sum + (variant.weight ?? 0), 0);
18
+ if (totalWeight <= 0)
19
+ return undefined;
20
+ const scaledBucket = (bucket / 100) * totalWeight;
21
+ let cursor = 0;
22
+ for (const variant of variants) {
23
+ cursor += variant.weight ?? 0;
24
+ if (scaledBucket < cursor)
25
+ return variant;
26
+ }
27
+ return [...variants].reverse().find((variant) => (variant.weight ?? 0) > 0);
28
+ }
29
+ export function evaluate(flag, context, segments = {}) {
30
+ if (!flag.enabled)
31
+ return disabledResult(flag.defaultValue, 'flag_disabled');
32
+ const bucket = getBucket(context.userId, flag.flagKey);
33
+ void segments;
34
+ let forceGlobalRollout = false;
35
+ for (const rule of flag.rules) {
36
+ if (!evaluateRule(rule, context))
37
+ continue;
38
+ if (rule.rolloutPct !== undefined && rule.rolloutPct !== null && bucket >= rule.rolloutPct) {
39
+ continue;
40
+ }
41
+ if (rule.serve === 'off') {
42
+ return disabledResult(flag.defaultValue, 'targeting_match');
43
+ }
44
+ if (rule.serve === 'on') {
45
+ forceGlobalRollout = true;
46
+ break;
47
+ }
48
+ const matchedVariant = flag.variants.find((variant) => variant.key === rule.serve);
49
+ if (matchedVariant)
50
+ return variantResult(matchedVariant);
51
+ return {
52
+ enabled: true,
53
+ value: flag.defaultValue,
54
+ reason: 'targeting_match',
55
+ variant: rule.serve,
56
+ };
57
+ }
58
+ if (!forceGlobalRollout && bucket >= flag.rolloutPct) {
59
+ return disabledResult(flag.defaultValue, 'rollout');
60
+ }
61
+ if (flag.variants.length > 0) {
62
+ const variant = pickVariant(flag.variants, bucket);
63
+ if (variant) {
64
+ return {
65
+ enabled: true,
66
+ value: variant.value,
67
+ reason: 'rollout',
68
+ variant: variant.key,
69
+ };
70
+ }
71
+ return disabledResult(flag.defaultValue, 'default');
72
+ }
73
+ return { enabled: true, value: true, reason: 'default' };
74
+ }
75
+ export function evaluateAll(flags, context, segments = {}) {
76
+ const results = {};
77
+ for (const [key, flag] of Object.entries(flags)) {
78
+ try {
79
+ results[key] = evaluate(flag, context, segments);
80
+ }
81
+ catch {
82
+ results[key] = {
83
+ enabled: false,
84
+ value: flag?.defaultValue ?? null,
85
+ reason: 'error',
86
+ };
87
+ }
88
+ }
89
+ return results;
90
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './bucketing';
3
+ export * from './targeting';
4
+ export * from './segments';
5
+ export * from './evaluator';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rollout/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './bucketing';
3
+ export * from './targeting';
4
+ export * from './segments';
5
+ export * from './evaluator';
@@ -0,0 +1,3 @@
1
+ import type { EvalContext, SegmentDef } from './types';
2
+ export declare function matchesSegment(segment: SegmentDef, context: EvalContext): boolean;
3
+ //# sourceMappingURL=segments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segments.d.ts","sourceRoot":"","sources":["../../src/rollout/segments.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGtD,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAGjF"}
@@ -0,0 +1,6 @@
1
+ import { evaluateRule } from './targeting';
2
+ export function matchesSegment(segment, context) {
3
+ if (segment.rules.length === 0)
4
+ return false;
5
+ return segment.rules.some((rule) => evaluateRule(rule, context));
6
+ }
@@ -0,0 +1,4 @@
1
+ import type { Condition, EvalContext, TargetingRule } from './types';
2
+ export declare function evaluateCondition(condition: Condition, context: EvalContext): boolean;
3
+ export declare function evaluateRule(rule: TargetingRule, context: EvalContext): boolean;
4
+ //# sourceMappingURL=targeting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targeting.d.ts","sourceRoot":"","sources":["../../src/rollout/targeting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAY,aAAa,EAAE,MAAM,SAAS,CAAA;AA4D9E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAMrF;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAG/E"}
@@ -0,0 +1,74 @@
1
+ function toStr(value) {
2
+ if (value === null || value === undefined)
3
+ return '';
4
+ return String(value);
5
+ }
6
+ function toNum(value) {
7
+ if (typeof value === 'number')
8
+ return value;
9
+ if (typeof value === 'string')
10
+ return Number.parseFloat(value);
11
+ return Number.NaN;
12
+ }
13
+ function looseEqual(a, b) {
14
+ if (a === null && b === null)
15
+ return true;
16
+ if (a === undefined && b === undefined)
17
+ return true;
18
+ return a === b;
19
+ }
20
+ function applyOperator(operator, attrValue, conditionValue) {
21
+ switch (operator) {
22
+ case 'exists':
23
+ return attrValue !== undefined && attrValue !== null;
24
+ case 'not_exists':
25
+ return attrValue === undefined || attrValue === null;
26
+ case 'is_true':
27
+ return attrValue === true;
28
+ case 'is_false':
29
+ return attrValue === false;
30
+ case 'equals':
31
+ return looseEqual(attrValue, conditionValue);
32
+ case 'not_equals':
33
+ return !looseEqual(attrValue, conditionValue);
34
+ case 'contains':
35
+ if (Array.isArray(attrValue))
36
+ return attrValue.some((item) => looseEqual(item, conditionValue));
37
+ return toStr(conditionValue).length > 0 && toStr(attrValue).includes(toStr(conditionValue));
38
+ case 'not_contains':
39
+ if (Array.isArray(attrValue))
40
+ return !attrValue.some((item) => looseEqual(item, conditionValue));
41
+ return toStr(conditionValue).length === 0 || !toStr(attrValue).includes(toStr(conditionValue));
42
+ case 'starts_with':
43
+ return toStr(conditionValue).length > 0 && toStr(attrValue).startsWith(toStr(conditionValue));
44
+ case 'ends_with':
45
+ return toStr(conditionValue).length > 0 && toStr(attrValue).endsWith(toStr(conditionValue));
46
+ case 'in':
47
+ return Array.isArray(conditionValue) && conditionValue.some((item) => looseEqual(attrValue, item));
48
+ case 'not_in':
49
+ return !Array.isArray(conditionValue) || !conditionValue.some((item) => looseEqual(attrValue, item));
50
+ case 'gt':
51
+ return !Number.isNaN(toNum(attrValue)) && !Number.isNaN(toNum(conditionValue)) && toNum(attrValue) > toNum(conditionValue);
52
+ case 'gte':
53
+ return !Number.isNaN(toNum(attrValue)) && !Number.isNaN(toNum(conditionValue)) && toNum(attrValue) >= toNum(conditionValue);
54
+ case 'lt':
55
+ return !Number.isNaN(toNum(attrValue)) && !Number.isNaN(toNum(conditionValue)) && toNum(attrValue) < toNum(conditionValue);
56
+ case 'lte':
57
+ return !Number.isNaN(toNum(attrValue)) && !Number.isNaN(toNum(conditionValue)) && toNum(attrValue) <= toNum(conditionValue);
58
+ default:
59
+ return false;
60
+ }
61
+ }
62
+ export function evaluateCondition(condition, context) {
63
+ try {
64
+ return applyOperator(condition.operator, context[condition.attribute], condition.value);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ export function evaluateRule(rule, context) {
71
+ if (rule.conditions.length === 0)
72
+ return true;
73
+ return rule.conditions.every((condition) => evaluateCondition(condition, context));
74
+ }
@@ -0,0 +1,48 @@
1
+ export interface EvalContext {
2
+ userId: string;
3
+ [attribute: string]: unknown;
4
+ }
5
+ export type Operator = 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'in' | 'not_in' | 'gt' | 'gte' | 'lt' | 'lte' | 'is_true' | 'is_false' | 'exists' | 'not_exists';
6
+ export interface Condition {
7
+ attribute: string;
8
+ operator: Operator;
9
+ value: unknown;
10
+ }
11
+ export interface TargetingRule {
12
+ id: string;
13
+ conditions: Condition[];
14
+ serve: 'on' | 'off' | string;
15
+ rolloutPct?: number;
16
+ }
17
+ export interface Variant {
18
+ key: string;
19
+ name: string;
20
+ weight: number;
21
+ value: unknown;
22
+ }
23
+ export interface FlagState {
24
+ flagKey: string;
25
+ enabled: boolean;
26
+ rolloutPct: number;
27
+ rules: TargetingRule[];
28
+ variants: Variant[];
29
+ defaultValue: unknown;
30
+ }
31
+ export type EvalReason = 'flag_disabled' | 'targeting_match' | 'rollout' | 'default' | 'error';
32
+ export interface EvalResult {
33
+ enabled: boolean;
34
+ value: unknown;
35
+ reason: EvalReason;
36
+ variant?: string;
37
+ }
38
+ export interface SegmentDef {
39
+ key: string;
40
+ rules: TargetingRule[];
41
+ }
42
+ export interface FlagCache {
43
+ environmentId: string;
44
+ version: number;
45
+ flags: Record<string, FlagState>;
46
+ segments: Record<string, SegmentDef>;
47
+ }
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rollout/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED,MAAM,MAAM,QAAQ,GAChB,QAAQ,GACR,YAAY,GACZ,UAAU,GACV,cAAc,GACd,aAAa,GACb,WAAW,GACX,IAAI,GACJ,QAAQ,GACR,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,SAAS,GACT,UAAU,GACV,QAAQ,GACR,YAAY,CAAA;AAEhB,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,KAAK,EAAE,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,MAAM,UAAU,GAClB,eAAe,GACf,iBAAiB,GACjB,SAAS,GACT,SAAS,GACT,OAAO,CAAA;AAEX,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,UAAU,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CACrC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export declare function buildUrl(baseUrl: string, path: string, searchParams?: Record<string, string>): URL;
2
+ export declare function requestJson<T>(fetchImpl: typeof fetch, url: URL, init: RequestInit): Promise<T>;
3
+ type StreamEvent = {
4
+ id: string;
5
+ event: string;
6
+ data: string;
7
+ };
8
+ export declare function openEventStream(fetchImpl: typeof fetch, url: URL, init: RequestInit, onEvent: (event: StreamEvent) => void): Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAU5F;AAED,wBAAsB,WAAW,CAAC,CAAC,EACjC,SAAS,EAAE,OAAO,KAAK,EACvB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,WAAW,GAChB,OAAO,CAAC,CAAC,CAAC,CAQZ;AAED,KAAK,WAAW,GAAG;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAqFD,wBAAsB,eAAe,CACnC,SAAS,EAAE,OAAO,KAAK,EACvB,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,iBAStC"}
@@ -0,0 +1,94 @@
1
+ function ensureTrailingSlash(baseUrl) {
2
+ return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
3
+ }
4
+ export function buildUrl(baseUrl, path, searchParams) {
5
+ const url = new URL(path, ensureTrailingSlash(baseUrl));
6
+ if (searchParams) {
7
+ for (const [key, value] of Object.entries(searchParams)) {
8
+ if (value)
9
+ url.searchParams.set(key, value);
10
+ }
11
+ }
12
+ return url;
13
+ }
14
+ export async function requestJson(fetchImpl, url, init) {
15
+ const response = await fetchImpl(url, init);
16
+ if (!response.ok) {
17
+ const body = await response.text().catch(() => '');
18
+ throw new Error(body || `Request failed with status ${response.status}`);
19
+ }
20
+ return (await response.json());
21
+ }
22
+ async function readEventStream(response, onEvent) {
23
+ if (!response.body) {
24
+ throw new Error('Streaming response does not expose a body');
25
+ }
26
+ const reader = response.body.getReader();
27
+ const decoder = new TextDecoder();
28
+ let buffer = '';
29
+ let eventName = 'message';
30
+ let eventId = '';
31
+ let dataLines = [];
32
+ const flushEvent = () => {
33
+ if (dataLines.length === 0 && eventName === 'message' && eventId.length === 0) {
34
+ return;
35
+ }
36
+ onEvent({
37
+ id: eventId,
38
+ event: eventName,
39
+ data: dataLines.join('\n'),
40
+ });
41
+ eventName = 'message';
42
+ eventId = '';
43
+ dataLines = [];
44
+ };
45
+ const consumeLine = (line) => {
46
+ if (line.length === 0) {
47
+ flushEvent();
48
+ return;
49
+ }
50
+ if (line.startsWith(':'))
51
+ return;
52
+ const separatorIndex = line.indexOf(':');
53
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
54
+ const value = separatorIndex === -1
55
+ ? ''
56
+ : line.slice(separatorIndex + 1).replace(/^ /, '');
57
+ if (field === 'event') {
58
+ eventName = value || 'message';
59
+ return;
60
+ }
61
+ if (field === 'id') {
62
+ eventId = value;
63
+ return;
64
+ }
65
+ if (field === 'data') {
66
+ dataLines.push(value);
67
+ }
68
+ };
69
+ while (true) {
70
+ const { done, value } = await reader.read();
71
+ if (done)
72
+ break;
73
+ buffer += decoder.decode(value, { stream: true });
74
+ let boundary = buffer.indexOf('\n');
75
+ while (boundary !== -1) {
76
+ const line = buffer.slice(0, boundary).replace(/\r$/, '');
77
+ buffer = buffer.slice(boundary + 1);
78
+ consumeLine(line);
79
+ boundary = buffer.indexOf('\n');
80
+ }
81
+ }
82
+ if (buffer.length > 0) {
83
+ consumeLine(buffer.replace(/\r$/, ''));
84
+ }
85
+ flushEvent();
86
+ }
87
+ export async function openEventStream(fetchImpl, url, init, onEvent) {
88
+ const response = await fetchImpl(url, init);
89
+ if (!response.ok) {
90
+ const body = await response.text().catch(() => '');
91
+ throw new Error(body || `Request failed with status ${response.status}`);
92
+ }
93
+ await readEventStream(response, onEvent);
94
+ }
@@ -0,0 +1,29 @@
1
+ import type { EvalContext, EvalResult, FlagCache, FlagState } from './rollout';
2
+ export type BootstrapResponse = {
3
+ project: {
4
+ id: string;
5
+ name: string;
6
+ slug: string;
7
+ sdkKeyPrefix: string;
8
+ };
9
+ environment: {
10
+ id: string;
11
+ name: string;
12
+ slug: string;
13
+ color: string;
14
+ sdkKeyPrefix: string;
15
+ };
16
+ cache: FlagCache;
17
+ };
18
+ export type LaunchwhitlyClientOptions = {
19
+ baseUrl: string;
20
+ projectKey: string;
21
+ environmentKey: string;
22
+ fetchImpl?: typeof fetch;
23
+ pollingIntervalMs?: number;
24
+ reconnectDelayMs?: number;
25
+ };
26
+ export type SnapshotListener = (snapshot: BootstrapResponse) => void;
27
+ export type EvaluateResult = EvalResult | null;
28
+ export type { EvalContext, EvalResult, FlagCache, FlagState };
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE9E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;QACZ,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,WAAW,EAAE;QACX,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,YAAY,EAAE,MAAM,CAAA;KACrB,CAAA;IACD,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAA;AAEpE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,IAAI,CAAA;AAE9C,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@launchwhitly/sdk",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "watch": "tsc -p tsconfig.json --watch"
20
+ }
21
+ }