@fireweaveai/web-sdk 2.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.
Files changed (49) hide show
  1. package/dist/adapter.d.ts +87 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +2 -0
  4. package/dist/adapter.js.map +1 -0
  5. package/dist/adapters/inmemory.d.ts +51 -0
  6. package/dist/adapters/inmemory.d.ts.map +1 -0
  7. package/dist/adapters/inmemory.js +91 -0
  8. package/dist/adapters/inmemory.js.map +1 -0
  9. package/dist/adapters/local.d.ts +38 -0
  10. package/dist/adapters/local.d.ts.map +1 -0
  11. package/dist/adapters/local.js +51 -0
  12. package/dist/adapters/local.js.map +1 -0
  13. package/dist/adapters/remote.d.ts +45 -0
  14. package/dist/adapters/remote.d.ts.map +1 -0
  15. package/dist/adapters/remote.js +332 -0
  16. package/dist/adapters/remote.js.map +1 -0
  17. package/dist/client.d.ts +134 -0
  18. package/dist/client.d.ts.map +1 -0
  19. package/dist/client.js +0 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/context.d.ts +26 -0
  22. package/dist/context.d.ts.map +1 -0
  23. package/dist/context.js +106 -0
  24. package/dist/context.js.map +1 -0
  25. package/dist/errors.d.ts +37 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +93 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/hosts.d.ts +11 -0
  30. package/dist/hosts.d.ts.map +1 -0
  31. package/dist/hosts.js +79 -0
  32. package/dist/hosts.js.map +1 -0
  33. package/dist/index.d.ts +45 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/provider.d.ts +39 -0
  38. package/dist/provider.d.ts.map +1 -0
  39. package/dist/provider.js +93 -0
  40. package/dist/provider.js.map +1 -0
  41. package/dist/runtime.d.ts +62 -0
  42. package/dist/runtime.d.ts.map +1 -0
  43. package/dist/runtime.js +289 -0
  44. package/dist/runtime.js.map +1 -0
  45. package/dist/types.d.ts +90 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +10 -0
  48. package/dist/types.js.map +1 -0
  49. package/package.json +45 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * FireweaveWebRuntime — the async/sync boundary.
3
+ *
4
+ * `initialize()` and `setContext()` prefetch a decision cache asynchronously;
5
+ * `evaluateSync()` is a pure read of that cache. That split is what lets a
6
+ * synchronous OpenFeature web provider sit on top of an architecture that is
7
+ * otherwise async, without either side bending.
8
+ *
9
+ * ## Fail-open, not fail-silent
10
+ *
11
+ * A hung backend must not block app boot, so the prefetch races a ceiling. What
12
+ * happens when the ceiling wins is the design decision that matters: this
13
+ * runtime enters `STALE` — not `READY` — and serves defaults with reason
14
+ * `STALE`, so a timed-out boot is DISTINGUISHABLE from a successful one.
15
+ *
16
+ * The tempting alternative (resolve the race and carry on) makes a failed
17
+ * prefetch look exactly like a successful one where every control point
18
+ * happened to be off. Under a progressive rollout that is the difference
19
+ * between "nobody got the feature because the ramp is at 0%" and "nobody got
20
+ * the feature because the SDK never reached the server", and no operator can
21
+ * tell those apart after the fact.
22
+ */
23
+ import { FireweaveError, isFireweaveError } from './errors.js';
24
+ import { canonicalizeContext, DEFAULT_CONTEXT_LIMITS, mergeContexts } from './context.js';
25
+ /** Ceiling on the initial prefetch so a hung backend cannot block boot. */
26
+ export const DEFAULT_FLAGS_READY_TIMEOUT_MS = 5_000;
27
+ function matchesExpectedType(value, expected) {
28
+ switch (expected) {
29
+ case 'boolean':
30
+ return typeof value === 'boolean';
31
+ case 'string':
32
+ return typeof value === 'string';
33
+ case 'number':
34
+ return typeof value === 'number';
35
+ case 'object':
36
+ return typeof value === 'object' && value !== null;
37
+ }
38
+ }
39
+ export class FireweaveWebRuntime {
40
+ adapter;
41
+ config;
42
+ state = 'UNINITIALIZED';
43
+ cache = new Map();
44
+ globalContext;
45
+ limits;
46
+ releaseContext = {};
47
+ listeners = new Set();
48
+ constructor(adapter, config = {}) {
49
+ this.adapter = adapter;
50
+ this.config = config;
51
+ this.globalContext = { ...(config.globalContext ?? {}) };
52
+ this.limits = { ...DEFAULT_CONTEXT_LIMITS, ...(config.limits ?? {}) };
53
+ }
54
+ getState() {
55
+ return this.state;
56
+ }
57
+ onStateChange(listener) {
58
+ this.listeners.add(listener);
59
+ return () => this.listeners.delete(listener);
60
+ }
61
+ setState(next) {
62
+ this.state = next;
63
+ for (const l of this.listeners)
64
+ l(next);
65
+ }
66
+ setReleaseContext(ctx) {
67
+ this.releaseContext = { ...ctx };
68
+ }
69
+ getReleaseContext() {
70
+ return this.releaseContext;
71
+ }
72
+ /** Initialize the adapter and populate the cache. Never throws. */
73
+ async initialize(context) {
74
+ if (this.state === 'SHUTDOWN')
75
+ return;
76
+ this.setState('INITIALIZING');
77
+ if (context !== undefined)
78
+ this.globalContext = mergeContexts(this.globalContext, context);
79
+ try {
80
+ await this.adapter.initialize();
81
+ }
82
+ catch {
83
+ this.setState('ERROR');
84
+ return;
85
+ }
86
+ await this.refresh();
87
+ }
88
+ /**
89
+ * Replace the global context and re-prefetch — the identity-change path.
90
+ *
91
+ * Call this after sign-in with the user's stable id, so percentage ramps
92
+ * bucket on that id rather than on an anonymous one. Returns the keys whose
93
+ * decisions changed, so a provider can emit a targeted ConfigurationChanged.
94
+ */
95
+ async setContext(context) {
96
+ this.globalContext = { ...context };
97
+ const before = this.cache;
98
+ await this.refresh();
99
+ return this.changedKeys(before, this.cache);
100
+ }
101
+ /** Re-run the prefetch against the current global context. */
102
+ async refresh() {
103
+ if (this.state === 'SHUTDOWN')
104
+ return;
105
+ let canonical;
106
+ try {
107
+ canonical = canonicalizeContext(this.globalContext, this.limits);
108
+ }
109
+ catch {
110
+ this.setState('ERROR');
111
+ return;
112
+ }
113
+ const timeoutMs = this.config.flagsReadyTimeoutMs ?? DEFAULT_FLAGS_READY_TIMEOUT_MS;
114
+ let timer;
115
+ const TIMED_OUT = Symbol('timeout');
116
+ const prefetch = this.adapter.prefetch(canonical, this.config.flagKeys !== undefined ? { flagKeys: this.config.flagKeys } : undefined);
117
+ try {
118
+ const result = await Promise.race([
119
+ prefetch,
120
+ new Promise((resolve) => {
121
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
122
+ }),
123
+ ]);
124
+ if (result === TIMED_OUT) {
125
+ // Fail OPEN (boot continues) but not SILENT: reads will carry STALE and
126
+ // the lifecycle says so. Swallow the late rejection so a slow failure
127
+ // does not surface as an unhandled rejection minutes later.
128
+ void prefetch.catch(() => undefined);
129
+ this.setState('STALE');
130
+ return;
131
+ }
132
+ this.cache = result;
133
+ this.setState('READY');
134
+ }
135
+ catch {
136
+ this.setState('STALE');
137
+ }
138
+ finally {
139
+ if (timer !== undefined)
140
+ clearTimeout(timer);
141
+ }
142
+ }
143
+ /**
144
+ * Synchronous evaluation against the prefetched cache. Never throws —
145
+ * failures surface as ERROR decisions, exactly like the OpenFeature contract.
146
+ */
147
+ evaluateSync(flagKey, expectedType, defaultValue, invocationContext) {
148
+ if (this.state === 'SHUTDOWN') {
149
+ return this.errorDecision(flagKey, defaultValue, new FireweaveError('AlreadyClosed'));
150
+ }
151
+ if (this.state === 'UNINITIALIZED' || this.state === 'INITIALIZING') {
152
+ return this.errorDecision(flagKey, defaultValue, new FireweaveError('NotReady'));
153
+ }
154
+ if (this.state === 'ERROR') {
155
+ return this.errorDecision(flagKey, defaultValue, new FireweaveError('BackendUnavailable'));
156
+ }
157
+ const resolution = this.cache.get(flagKey);
158
+ if (resolution === undefined || resolution.found === false) {
159
+ // A cache miss while STALE is not a missing control point — it is an
160
+ // unanswered question. Reporting FLAG_NOT_FOUND there would send a caller
161
+ // hunting for a flag that may well exist.
162
+ if (this.state === 'STALE') {
163
+ return {
164
+ flagKey,
165
+ value: defaultValue,
166
+ reason: 'STALE',
167
+ variant: 'default',
168
+ metadata: { 'fireweave.stale': true },
169
+ };
170
+ }
171
+ return this.errorDecision(flagKey, defaultValue, new FireweaveError('FlagNotFound'));
172
+ }
173
+ const value = resolution.value ?? null;
174
+ if (!matchesExpectedType(value, expectedType)) {
175
+ return this.errorDecision(flagKey, defaultValue, new FireweaveError('TypeMismatch'));
176
+ }
177
+ const metadata = this.metadataFor(resolution);
178
+ const decision = {
179
+ flagKey,
180
+ value,
181
+ reason: this.reasonFor(resolution),
182
+ ...(resolution.variant !== undefined ? { variant: resolution.variant } : {}),
183
+ ...(metadata !== undefined ? { metadata } : {}),
184
+ };
185
+ if (this.config.sendExposure === true) {
186
+ this.recordExposureFor(decision, invocationContext);
187
+ }
188
+ return decision;
189
+ }
190
+ reasonFor(resolution) {
191
+ if (resolution.enabled === false)
192
+ return 'DISABLED';
193
+ if (resolution.reason !== undefined)
194
+ return resolution.reason;
195
+ if (this.state === 'STALE')
196
+ return 'STALE';
197
+ return 'TARGETING_MATCH';
198
+ }
199
+ metadataFor(resolution) {
200
+ const metadata = {};
201
+ if (resolution.version !== undefined)
202
+ metadata['fireweave.flagVersion'] = resolution.version;
203
+ if (resolution.reasonCode !== undefined)
204
+ metadata['fireweave.reasonCode'] = resolution.reasonCode;
205
+ if (resolution.quotaLimited === true)
206
+ metadata['fireweave.quotaLimited'] = true;
207
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
208
+ }
209
+ recordExposureFor(decision, invocationContext) {
210
+ if (this.adapter.recordExposure === undefined)
211
+ return;
212
+ const merged = mergeContexts(this.globalContext, invocationContext);
213
+ const targetingKey = typeof merged.targetingKey === 'string' ? merged.targetingKey : '';
214
+ if (targetingKey === '')
215
+ return;
216
+ const rc = this.releaseContext;
217
+ const exposure = {
218
+ flagKey: decision.flagKey,
219
+ targetingKey,
220
+ value: decision.value,
221
+ ...(decision.variant !== undefined ? { variant: decision.variant } : {}),
222
+ ...(rc.rolloutId !== undefined ? { rolloutId: rc.rolloutId } : {}),
223
+ ...(rc.changeId !== undefined ? { changeId: rc.changeId } : {}),
224
+ ...(rc.stampId !== undefined ? { stampId: rc.stampId } : {}),
225
+ };
226
+ this.adapter.recordExposure(exposure);
227
+ }
228
+ recordExposure(exposure) {
229
+ this.adapter.recordExposure?.(exposure);
230
+ }
231
+ recordSignal(signal) {
232
+ this.adapter.recordSignal?.(signal);
233
+ }
234
+ async registerTarget(targetingKey, options = {}) {
235
+ if (this.state === 'SHUTDOWN') {
236
+ return { ok: false, error: new FireweaveError('AlreadyClosed') };
237
+ }
238
+ if (this.adapter.registerTarget === undefined) {
239
+ return { ok: false, error: new FireweaveError('UnsupportedCapability') };
240
+ }
241
+ try {
242
+ return await this.adapter.registerTarget(targetingKey, options);
243
+ }
244
+ catch (err) {
245
+ return {
246
+ ok: false,
247
+ error: isFireweaveError(err) ? err : new FireweaveError('BackendUnavailable'),
248
+ };
249
+ }
250
+ }
251
+ async flush(options = {}) {
252
+ await this.adapter.flush?.(options);
253
+ }
254
+ async shutdown() {
255
+ if (this.state === 'SHUTDOWN')
256
+ return;
257
+ try {
258
+ await this.adapter.shutdown();
259
+ }
260
+ catch {
261
+ // never throw from shutdown
262
+ }
263
+ this.cache = new Map();
264
+ this.setState('SHUTDOWN');
265
+ }
266
+ changedKeys(before, after) {
267
+ const keys = new Set([...before.keys(), ...after.keys()]);
268
+ const changed = [];
269
+ for (const key of keys) {
270
+ const a = before.get(key);
271
+ const b = after.get(key);
272
+ if (JSON.stringify(a ?? null) !== JSON.stringify(b ?? null))
273
+ changed.push(key);
274
+ }
275
+ return changed;
276
+ }
277
+ errorDecision(flagKey, defaultValue, err) {
278
+ return {
279
+ flagKey,
280
+ value: defaultValue,
281
+ reason: 'ERROR',
282
+ errorCode: err.openFeatureErrorCode,
283
+ errorKind: err.kind,
284
+ errorMessage: err.message,
285
+ metadata: { 'fireweave.errorKind': err.kind, ...err.metadata },
286
+ };
287
+ }
288
+ }
289
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Canonical data shapes, mirroring `spec/decision.schema.json`,
3
+ * `spec/signal.schema.json`, and `spec/release-context.schema.json`.
4
+ *
5
+ * Identical in meaning to the server SDK's `types.ts`. Duplicated rather than
6
+ * shared so the browser package has no cross-package dependency to audit; the
7
+ * JSON Schemas in `spec/` are what keep the two honest.
8
+ */
9
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
10
+ [key: string]: JsonValue;
11
+ };
12
+ export type FlagValueType = 'boolean' | 'string' | 'number' | 'object';
13
+ export type DecisionReason = 'STATIC' | 'DEFAULT' | 'TARGETING_MATCH' | 'SPLIT' | 'CACHED' | 'STALE' | 'DISABLED' | 'UNKNOWN' | 'ERROR';
14
+ export interface Decision {
15
+ readonly flagKey: string;
16
+ readonly value: JsonValue;
17
+ readonly reason: DecisionReason;
18
+ readonly variant?: string;
19
+ readonly errorCode?: string;
20
+ readonly errorKind?: string;
21
+ readonly errorMessage?: string;
22
+ readonly metadata?: Record<string, string | number | boolean>;
23
+ }
24
+ /**
25
+ * A browser lifecycle has one state the server does not: STALE.
26
+ *
27
+ * It means "initialize completed, but the prefetch did not, so reads are being
28
+ * served from an empty or outdated cache". Collapsing it into READY would make
29
+ * a failed boot indistinguishable from a successful one — which is the specific
30
+ * failure this SDK is written to avoid.
31
+ */
32
+ export type LifecycleState = 'UNINITIALIZED' | 'INITIALIZING' | 'READY' | 'STALE' | 'ERROR' | 'SHUTDOWN';
33
+ export interface CanonicalContext {
34
+ readonly targetingKey?: string;
35
+ readonly attributes: Record<string, JsonValue>;
36
+ readonly groups?: Record<string, string>;
37
+ readonly groupProperties?: Record<string, Record<string, JsonValue>>;
38
+ }
39
+ export type SignalKind = 'health' | 'error' | 'metric' | 'outcome';
40
+ export interface Signal {
41
+ readonly kind: SignalKind;
42
+ readonly name: string;
43
+ readonly targetingKey?: string;
44
+ readonly flagKey?: string;
45
+ readonly variant?: string;
46
+ readonly status?: string;
47
+ readonly errorKind?: string;
48
+ readonly message?: string;
49
+ readonly value?: number;
50
+ readonly rolloutId?: string;
51
+ readonly changeId?: string;
52
+ readonly stampId?: string;
53
+ readonly timestamp?: string;
54
+ }
55
+ export interface Exposure {
56
+ readonly flagKey: string;
57
+ readonly targetingKey: string;
58
+ readonly value: JsonValue;
59
+ readonly variant?: string;
60
+ readonly rolloutId?: string;
61
+ readonly changeId?: string;
62
+ readonly stampId?: string;
63
+ }
64
+ export type ReleaseState = 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED';
65
+ export interface ReleaseContext {
66
+ readonly rolloutId?: string;
67
+ readonly changeId?: string;
68
+ readonly stampId?: string;
69
+ readonly environment?: string;
70
+ }
71
+ export interface Capabilities {
72
+ readonly static: {
73
+ readonly language: 'web';
74
+ readonly sdkVersion: string;
75
+ readonly specVersion: string;
76
+ readonly openFeature: {
77
+ readonly specFloor: string;
78
+ readonly providerName: string;
79
+ readonly serverOnly: boolean;
80
+ };
81
+ readonly features: Record<string, boolean>;
82
+ };
83
+ readonly runtime: {
84
+ readonly backend: 'fireweave' | 'inmemory' | 'none' | 'other';
85
+ readonly lifecycle: LifecycleState;
86
+ readonly features: Record<string, boolean>;
87
+ readonly limits: Record<string, number>;
88
+ };
89
+ }
90
+ //# 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,10 @@
1
+ /**
2
+ * Canonical data shapes, mirroring `spec/decision.schema.json`,
3
+ * `spec/signal.schema.json`, and `spec/release-context.schema.json`.
4
+ *
5
+ * Identical in meaning to the server SDK's `types.ts`. Duplicated rather than
6
+ * shared so the browser package has no cross-package dependency to audit; the
7
+ * JSON Schemas in `spec/` are what keep the two honest.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@fireweaveai/web-sdk",
3
+ "version": "2.1.0",
4
+ "description": "Fireweave release-engineering SDK for browsers: control points, target registration, exposures, and health/outcome signals — with an OpenFeature web provider. Remote-only; holds no secrets.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/FireWeave-HQ/fireweave-sdk.git",
10
+ "directory": "sdks/web/packages/sdk"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "homepage": "https://github.com/FireWeave-HQ/fireweave-sdk/tree/master/sdks/web/packages/sdk#readme",
16
+ "bugs": "https://github.com/FireWeave-HQ/fireweave-sdk/issues",
17
+ "sideEffects": false,
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json"
31
+ },
32
+ "peerDependencies": {
33
+ "@openfeature/web-sdk": "^1.9.0"
34
+ },
35
+ "keywords": [
36
+ "fireweave",
37
+ "release-engineering",
38
+ "progressive-delivery",
39
+ "control-points",
40
+ "feature-flags",
41
+ "openfeature",
42
+ "browser",
43
+ "web"
44
+ ]
45
+ }