@fireweaveai/web-sdk-cjs 0.0.0-reserve.0 → 0.0.0-stage
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/README.md +2 -51
- package/package.json +4 -66
- package/LICENSE +0 -21
- package/dist/application/client.d.ts +0 -104
- package/dist/application/client.js +0 -147
- package/dist/application/mode.d.ts +0 -99
- package/dist/application/mode.js +0 -101
- package/dist/application/ports.d.ts +0 -101
- package/dist/application/ports.js +0 -2
- package/dist/application/runtime.d.ts +0 -70
- package/dist/application/runtime.js +0 -348
- package/dist/build/node/define.d.ts +0 -14
- package/dist/build/node/define.js +0 -38
- package/dist/build/node/shared.d.ts +0 -19
- package/dist/build/node/shared.js +0 -39
- package/dist/build/node/vite.d.ts +0 -53
- package/dist/build/node/vite.js +0 -106
- package/dist/build/src/start/build-info.d.ts +0 -2
- package/dist/build/src/start/build-info.js +0 -9
- package/dist/build/src/start/names.d.ts +0 -38
- package/dist/build/src/start/names.js +0 -41
- package/dist/build/src/start/policy.d.ts +0 -70
- package/dist/build/src/start/policy.js +0 -128
- package/dist/domain/context.d.ts +0 -51
- package/dist/domain/context.js +0 -72
- package/dist/domain/errors.d.ts +0 -36
- package/dist/domain/errors.js +0 -97
- package/dist/domain/target.d.ts +0 -2
- package/dist/domain/target.js +0 -2
- package/dist/domain/types.d.ts +0 -89
- package/dist/domain/types.js +0 -10
- package/dist/domain/validation.d.ts +0 -128
- package/dist/domain/validation.js +0 -358
- package/dist/index.d.ts +0 -50
- package/dist/index.js +0 -66
- package/dist/infrastructure/adapters/inmemory.d.ts +0 -50
- package/dist/infrastructure/adapters/inmemory.js +0 -94
- package/dist/infrastructure/adapters/local.d.ts +0 -79
- package/dist/infrastructure/adapters/local.js +0 -98
- package/dist/infrastructure/adapters/remote.d.ts +0 -44
- package/dist/infrastructure/adapters/remote.js +0 -329
- package/dist/infrastructure/hosts.d.ts +0 -10
- package/dist/infrastructure/hosts.js +0 -84
- package/dist/start/build-info.d.ts +0 -2
- package/dist/start/build-info.js +0 -9
- package/dist/start/control-points.d.ts +0 -24
- package/dist/start/control-points.js +0 -61
- package/dist/start/fw.d.ts +0 -42
- package/dist/start/fw.js +0 -137
- package/dist/start/identity.d.ts +0 -19
- package/dist/start/identity.js +0 -93
- package/dist/start/index.d.ts +0 -34
- package/dist/start/index.js +0 -38
- package/dist/start/names.d.ts +0 -38
- package/dist/start/names.js +0 -41
- package/dist/start/policy.d.ts +0 -70
- package/dist/start/policy.js +0 -128
- package/dist/start/state.d.ts +0 -103
- package/dist/start/state.js +0 -375
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend adapter boundary for the browser.
|
|
3
|
-
*
|
|
4
|
-
* One shape differs from the server port, and it is the difference that makes
|
|
5
|
-
* a synchronous OpenFeature provider possible: the server port has
|
|
6
|
-
* `resolve(controlPointKey)` — one call per control point — while this port has
|
|
7
|
-
* `prefetch(context)`, which returns EVERY decision for a context in one round
|
|
8
|
-
* trip. Evaluation then becomes a synchronous map lookup in the runtime.
|
|
9
|
-
*
|
|
10
|
-
* The reason is not performance, it is the OpenFeature web contract:
|
|
11
|
-
* `resolve*Evaluation` must return `ResolutionDetails`, not a Promise. Anything
|
|
12
|
-
* the provider needs at read time must already be in memory.
|
|
13
|
-
*
|
|
14
|
-
* Adapters translate canonical requests to the Fireweave remote protocol; they
|
|
15
|
-
* never see OpenFeature types.
|
|
16
|
-
*/
|
|
17
|
-
import type { CanonicalContext, DecisionReason, Exposure, FlagValueType, JsonValue, Signal } from '../domain/types.js';
|
|
18
|
-
import type { FireweaveError } from '../domain/errors.js';
|
|
19
|
-
import type { TargetKind } from '../domain/target.js';
|
|
20
|
-
export interface AdapterResolution {
|
|
21
|
-
found: boolean;
|
|
22
|
-
enabled?: boolean;
|
|
23
|
-
value?: JsonValue;
|
|
24
|
-
variant?: string;
|
|
25
|
-
flagType?: FlagValueType;
|
|
26
|
-
reason?: DecisionReason;
|
|
27
|
-
reasonCode?: string;
|
|
28
|
-
version?: number;
|
|
29
|
-
payload?: JsonValue;
|
|
30
|
-
quotaLimited?: boolean;
|
|
31
|
-
}
|
|
32
|
-
/** Every decision the backend returned for one context, keyed by control point. */
|
|
33
|
-
export type PrefetchResult = ReadonlyMap<string, AdapterResolution>;
|
|
34
|
-
export interface PrefetchOptions {
|
|
35
|
-
/** Restrict the batch to these keys; omit to let the backend return all it knows. */
|
|
36
|
-
readonly controlPointKeys?: readonly string[];
|
|
37
|
-
readonly signal?: AbortSignal;
|
|
38
|
-
}
|
|
39
|
-
export interface RegisterTargetOptions {
|
|
40
|
-
readonly kind?: TargetKind;
|
|
41
|
-
readonly properties?: Record<string, JsonValue>;
|
|
42
|
-
readonly environment?: string;
|
|
43
|
-
readonly signal?: AbortSignal;
|
|
44
|
-
}
|
|
45
|
-
export interface RegisterTargetResult {
|
|
46
|
-
/**
|
|
47
|
-
* `false` means the target was NOT registered — rules depending on its
|
|
48
|
-
* properties will not match until a later attempt succeeds. Sign-in paths
|
|
49
|
-
* normally ignore this; a careful caller logs it, because a silently
|
|
50
|
-
* unregistered target is exactly how targeting ends up matching nobody.
|
|
51
|
-
*/
|
|
52
|
-
readonly ok: boolean;
|
|
53
|
-
readonly error?: FireweaveError;
|
|
54
|
-
}
|
|
55
|
-
export interface AdapterRuntimeFeatures {
|
|
56
|
-
remoteEvaluation?: boolean;
|
|
57
|
-
/** Always false in the browser, structurally — see ADR-0009. */
|
|
58
|
-
localEvaluation?: boolean;
|
|
59
|
-
localOnly?: boolean;
|
|
60
|
-
exposureEmission?: boolean;
|
|
61
|
-
sideEffectFreeReads?: boolean;
|
|
62
|
-
groupAnalytics?: boolean;
|
|
63
|
-
}
|
|
64
|
-
export interface WebBackendAdapter {
|
|
65
|
-
readonly name: 'fireweave' | 'inmemory' | 'other';
|
|
66
|
-
/**
|
|
67
|
-
* Miss-reason override for a control point ABSENT from the prefetch result
|
|
68
|
-
* (spec/modes.md "Behaviour per mode": local mode's unknown-key row is
|
|
69
|
-
* `default`/reason `DEFAULT`, not an error — unlike remote's
|
|
70
|
-
* `default`/`ERROR`/`ControlPointNotFound`).
|
|
71
|
-
*
|
|
72
|
-
* Node's per-call `resolve()` lets a miss carry its own `reason: 'DEFAULT'`
|
|
73
|
-
* on the resolution object itself. Web's adapter returns EVERY decision for
|
|
74
|
-
* a context in one batch (`prefetch`), so there is no per-key resolution
|
|
75
|
-
* object for a key that was never in the batch at all — the seam instead
|
|
76
|
-
* lives on the adapter. `FireweaveLocalWebAdapter` sets this to `'DEFAULT'`;
|
|
77
|
-
* every other adapter leaves it undefined and keeps the ControlPointNotFound/ERROR
|
|
78
|
-
* path (`FireweaveWebRuntime.evaluateSync` checks it with strict `===`).
|
|
79
|
-
*/
|
|
80
|
-
readonly missReason?: 'DEFAULT';
|
|
81
|
-
/** Bring the backend to a usable state. Reject with FireweaveError on failure. */
|
|
82
|
-
initialize(signal?: AbortSignal): Promise<void>;
|
|
83
|
-
/** Fetch every decision for a context. Throws FireweaveError on transport faults. */
|
|
84
|
-
prefetch(context: CanonicalContext, options?: PrefetchOptions): Promise<PrefetchResult>;
|
|
85
|
-
/** Register a user or device for durable targeting. Resolves rather than throws. */
|
|
86
|
-
registerTarget?(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
|
|
87
|
-
recordExposure?(exposure: Exposure): void;
|
|
88
|
-
recordSignal?(signal: Signal): void;
|
|
89
|
-
/**
|
|
90
|
-
* Deliver queued telemetry.
|
|
91
|
-
*
|
|
92
|
-
* `beacon: true` asks for an unload-safe transport (`sendBeacon` /
|
|
93
|
-
* `keepalive`) because the page may be seconds from gone. Delivery is
|
|
94
|
-
* best-effort by definition on that path — neither transport reports failure.
|
|
95
|
-
*/
|
|
96
|
-
flush?(options?: {
|
|
97
|
-
beacon?: boolean;
|
|
98
|
-
}): Promise<void>;
|
|
99
|
-
shutdown(): Promise<void>;
|
|
100
|
-
features(): AdapterRuntimeFeatures;
|
|
101
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { ContextInput, ContextLimits } from '../domain/context.js';
|
|
2
|
-
import type { ExpectedFlagType } from '../domain/validation.js';
|
|
3
|
-
import type { RegisterTargetOptions, RegisterTargetResult, WebBackendAdapter } from './ports.js';
|
|
4
|
-
import type { Decision, Exposure, JsonValue, LifecycleState, ReleaseContext, Signal } from '../domain/types.js';
|
|
5
|
-
export type { ExpectedFlagType } from '../domain/validation.js';
|
|
6
|
-
/** Ceiling on the initial prefetch so a hung backend cannot block boot. */
|
|
7
|
-
export declare const DEFAULT_FLAGS_READY_TIMEOUT_MS = 5000;
|
|
8
|
-
export interface FireweaveWebRuntimeConfig {
|
|
9
|
-
/** Context applied to every evaluation unless overridden per call. */
|
|
10
|
-
readonly globalContext?: ContextInput;
|
|
11
|
-
readonly limits?: Partial<ContextLimits>;
|
|
12
|
-
/** Additional reserved attribute keys, beyond DEFAULT_RESERVED_ATTRIBUTE_KEYS. */
|
|
13
|
-
readonly reservedAttributeKeys?: readonly string[];
|
|
14
|
-
readonly flagsReadyTimeoutMs?: number;
|
|
15
|
-
/** Restrict prefetch to a known set of control points. */
|
|
16
|
-
readonly controlPointKeys?: readonly string[];
|
|
17
|
-
/** Emit a Fireweave exposure on each successful evaluation. Default false. */
|
|
18
|
-
readonly sendExposure?: boolean;
|
|
19
|
-
}
|
|
20
|
-
export declare class FireweaveWebRuntime {
|
|
21
|
-
readonly adapter: WebBackendAdapter;
|
|
22
|
-
readonly config: FireweaveWebRuntimeConfig;
|
|
23
|
-
private state;
|
|
24
|
-
private cache;
|
|
25
|
-
private globalContext;
|
|
26
|
-
private readonly contextPolicy;
|
|
27
|
-
private releaseContext;
|
|
28
|
-
private readonly listeners;
|
|
29
|
-
constructor(adapter: WebBackendAdapter, config?: FireweaveWebRuntimeConfig);
|
|
30
|
-
getState(): LifecycleState;
|
|
31
|
-
onStateChange(listener: (state: LifecycleState) => void): () => void;
|
|
32
|
-
private setState;
|
|
33
|
-
setReleaseContext(ctx: ReleaseContext): void;
|
|
34
|
-
getReleaseContext(): ReleaseContext;
|
|
35
|
-
/** Initialize the adapter and populate the cache. Never throws. */
|
|
36
|
-
initialize(context?: ContextInput): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* Replace the global context and re-prefetch — the identity-change path.
|
|
39
|
-
*
|
|
40
|
-
* Call this after sign-in with the user's stable id, so percentage ramps
|
|
41
|
-
* bucket on that id rather than on an anonymous one. Returns the keys whose
|
|
42
|
-
* decisions changed, so a provider can emit a targeted ConfigurationChanged.
|
|
43
|
-
*/
|
|
44
|
-
setContext(context: ContextInput): Promise<readonly string[]>;
|
|
45
|
-
/** Re-run the prefetch against the current global context. */
|
|
46
|
-
refresh(): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Synchronous evaluation against the prefetched cache. Never throws —
|
|
49
|
-
* failures surface as ERROR decisions, exactly like the OpenFeature
|
|
50
|
-
* contract.
|
|
51
|
-
*
|
|
52
|
-
* Validates in the fixed order spec/control-points.md "Validation, before
|
|
53
|
-
* any I/O" names — key, default-vs-type, context, lifecycle — stopping at
|
|
54
|
-
* the first failure. Only once all four pass does this consult the cache.
|
|
55
|
-
*/
|
|
56
|
-
evaluateSync(controlPointKey: string, expectedType: ExpectedFlagType, defaultValue: JsonValue, invocationContext?: ContextInput): Decision;
|
|
57
|
-
private reasonFor;
|
|
58
|
-
private metadataFor;
|
|
59
|
-
private recordExposureFor;
|
|
60
|
-
recordExposure(exposure: Exposure): void;
|
|
61
|
-
recordSignal(signal: Signal): void;
|
|
62
|
-
registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
|
|
63
|
-
flush(options?: {
|
|
64
|
-
beacon?: boolean;
|
|
65
|
-
}): Promise<void>;
|
|
66
|
-
shutdown(): Promise<void>;
|
|
67
|
-
private changedKeys;
|
|
68
|
-
private lifecycleError;
|
|
69
|
-
private errorDecision;
|
|
70
|
-
}
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FireweaveWebRuntime = exports.DEFAULT_FLAGS_READY_TIMEOUT_MS = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* FireweaveWebRuntime — the async/sync boundary.
|
|
6
|
-
*
|
|
7
|
-
* `initialize()` and `setContext()` prefetch a decision cache asynchronously;
|
|
8
|
-
* `evaluateSync()` is a pure read of that cache. That split is what lets a
|
|
9
|
-
* synchronous OpenFeature web provider sit on top of an architecture that is
|
|
10
|
-
* otherwise async, without either side bending.
|
|
11
|
-
*
|
|
12
|
-
* ## Fail-open, not fail-silent
|
|
13
|
-
*
|
|
14
|
-
* A hung backend must not block app boot, so the prefetch races a ceiling. What
|
|
15
|
-
* happens when the ceiling wins is the design decision that matters: this
|
|
16
|
-
* runtime enters `STALE` — not `READY` — and serves defaults with reason
|
|
17
|
-
* `STALE`, so a timed-out boot is DISTINGUISHABLE from a successful one.
|
|
18
|
-
*
|
|
19
|
-
* The tempting alternative (resolve the race and carry on) makes a failed
|
|
20
|
-
* prefetch look exactly like a successful one where every control point
|
|
21
|
-
* happened to be off. Under a progressive rollout that is the difference
|
|
22
|
-
* between "nobody got the feature because the ramp is at 0%" and "nobody got
|
|
23
|
-
* the feature because the SDK never reached the server", and no operator can
|
|
24
|
-
* tell those apart after the fact.
|
|
25
|
-
*
|
|
26
|
-
* ## Validation order (spec/control-points.md "Validation, before any I/O")
|
|
27
|
-
*
|
|
28
|
-
* `evaluateSync` validates in the fixed order the spec names — key,
|
|
29
|
-
* default-vs-type, context, lifecycle — stopping at the first failure, and
|
|
30
|
-
* only THEN consults the prefetched cache. "Before any I/O" reads literally
|
|
31
|
-
* for node (the adapter call is the I/O); web has already done its I/O
|
|
32
|
-
* earlier (the prefetch), so here the same four-step order gates the cache
|
|
33
|
-
* READ instead, for the same reason: a malformed call must degrade to the
|
|
34
|
-
* caller's default without depending on what happens to be in the cache.
|
|
35
|
-
*/
|
|
36
|
-
const errors_js_1 = require("../domain/errors.js");
|
|
37
|
-
const context_js_1 = require("../domain/context.js");
|
|
38
|
-
const validation_js_1 = require("../domain/validation.js");
|
|
39
|
-
/** Ceiling on the initial prefetch so a hung backend cannot block boot. */
|
|
40
|
-
exports.DEFAULT_FLAGS_READY_TIMEOUT_MS = 5_000;
|
|
41
|
-
class FireweaveWebRuntime {
|
|
42
|
-
adapter;
|
|
43
|
-
config;
|
|
44
|
-
state = 'UNINITIALIZED';
|
|
45
|
-
cache = new Map();
|
|
46
|
-
globalContext;
|
|
47
|
-
contextPolicy;
|
|
48
|
-
releaseContext = {};
|
|
49
|
-
listeners = new Set();
|
|
50
|
-
constructor(adapter, config = {}) {
|
|
51
|
-
this.adapter = adapter;
|
|
52
|
-
this.config = config;
|
|
53
|
-
this.globalContext = { ...(config.globalContext ?? {}) };
|
|
54
|
-
this.contextPolicy = {
|
|
55
|
-
limits: { ...context_js_1.DEFAULT_CONTEXT_LIMITS, ...(config.limits ?? {}) },
|
|
56
|
-
reservedAttributeKeys: [...context_js_1.DEFAULT_RESERVED_ATTRIBUTE_KEYS, ...(config.reservedAttributeKeys ?? [])],
|
|
57
|
-
// Not required at the generic context-validation layer — web's
|
|
58
|
-
// targetingKey requirement is enforced at the remote adapter's
|
|
59
|
-
// prefetch/registerTarget boundary, not on every read.
|
|
60
|
-
requireTargetingKey: false,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
getState() {
|
|
64
|
-
return this.state;
|
|
65
|
-
}
|
|
66
|
-
onStateChange(listener) {
|
|
67
|
-
this.listeners.add(listener);
|
|
68
|
-
return () => this.listeners.delete(listener);
|
|
69
|
-
}
|
|
70
|
-
setState(next) {
|
|
71
|
-
this.state = next;
|
|
72
|
-
for (const l of this.listeners)
|
|
73
|
-
l(next);
|
|
74
|
-
}
|
|
75
|
-
setReleaseContext(ctx) {
|
|
76
|
-
this.releaseContext = { ...ctx };
|
|
77
|
-
}
|
|
78
|
-
getReleaseContext() {
|
|
79
|
-
return this.releaseContext;
|
|
80
|
-
}
|
|
81
|
-
/** Initialize the adapter and populate the cache. Never throws. */
|
|
82
|
-
async initialize(context) {
|
|
83
|
-
if (this.state === 'SHUTDOWN')
|
|
84
|
-
return;
|
|
85
|
-
this.setState('INITIALIZING');
|
|
86
|
-
if (context !== undefined)
|
|
87
|
-
this.globalContext = { ...this.globalContext, ...context };
|
|
88
|
-
try {
|
|
89
|
-
await this.adapter.initialize();
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
this.setState('ERROR');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
await this.refresh();
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Replace the global context and re-prefetch — the identity-change path.
|
|
99
|
-
*
|
|
100
|
-
* Call this after sign-in with the user's stable id, so percentage ramps
|
|
101
|
-
* bucket on that id rather than on an anonymous one. Returns the keys whose
|
|
102
|
-
* decisions changed, so a provider can emit a targeted ConfigurationChanged.
|
|
103
|
-
*/
|
|
104
|
-
async setContext(context) {
|
|
105
|
-
this.globalContext = { ...context };
|
|
106
|
-
const before = this.cache;
|
|
107
|
-
await this.refresh();
|
|
108
|
-
return this.changedKeys(before, this.cache);
|
|
109
|
-
}
|
|
110
|
-
/** Re-run the prefetch against the current global context. */
|
|
111
|
-
async refresh() {
|
|
112
|
-
if (this.state === 'SHUTDOWN')
|
|
113
|
-
return;
|
|
114
|
-
let canonical;
|
|
115
|
-
try {
|
|
116
|
-
canonical = (0, validation_js_1.canonicalizeContext)((0, context_js_1.mergeContexts)(this.globalContext), this.contextPolicy);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
this.setState('ERROR');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const timeoutMs = this.config.flagsReadyTimeoutMs ?? exports.DEFAULT_FLAGS_READY_TIMEOUT_MS;
|
|
123
|
-
let timer;
|
|
124
|
-
const TIMED_OUT = Symbol('timeout');
|
|
125
|
-
const prefetch = this.adapter.prefetch(canonical, this.config.controlPointKeys !== undefined ? { controlPointKeys: this.config.controlPointKeys } : undefined);
|
|
126
|
-
try {
|
|
127
|
-
const result = await Promise.race([
|
|
128
|
-
prefetch,
|
|
129
|
-
new Promise((resolve) => {
|
|
130
|
-
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
131
|
-
}),
|
|
132
|
-
]);
|
|
133
|
-
if (result === TIMED_OUT) {
|
|
134
|
-
// Fail OPEN (boot continues) but not SILENT: reads will carry STALE and
|
|
135
|
-
// the lifecycle says so. Swallow the late rejection so a slow failure
|
|
136
|
-
// does not surface as an unhandled rejection minutes later.
|
|
137
|
-
void prefetch.catch(() => undefined);
|
|
138
|
-
this.setState('STALE');
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.cache = result;
|
|
142
|
-
this.setState('READY');
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
this.setState('STALE');
|
|
146
|
-
}
|
|
147
|
-
finally {
|
|
148
|
-
if (timer !== undefined)
|
|
149
|
-
clearTimeout(timer);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Synchronous evaluation against the prefetched cache. Never throws —
|
|
154
|
-
* failures surface as ERROR decisions, exactly like the OpenFeature
|
|
155
|
-
* contract.
|
|
156
|
-
*
|
|
157
|
-
* Validates in the fixed order spec/control-points.md "Validation, before
|
|
158
|
-
* any I/O" names — key, default-vs-type, context, lifecycle — stopping at
|
|
159
|
-
* the first failure. Only once all four pass does this consult the cache.
|
|
160
|
-
*/
|
|
161
|
-
evaluateSync(controlPointKey, expectedType, defaultValue, invocationContext) {
|
|
162
|
-
const keyResult = (0, validation_js_1.validateControlPointKey)(controlPointKey);
|
|
163
|
-
if (!keyResult.ok) {
|
|
164
|
-
return this.errorDecision(controlPointKey, defaultValue, keyResult.error);
|
|
165
|
-
}
|
|
166
|
-
const defaultResult = (0, validation_js_1.validateDefaultValue)(expectedType, defaultValue);
|
|
167
|
-
if (!defaultResult.ok) {
|
|
168
|
-
return this.errorDecision(controlPointKey, defaultValue, defaultResult.error);
|
|
169
|
-
}
|
|
170
|
-
// The validated context here is not what SELECTS the cached decision —
|
|
171
|
-
// that was fixed by the last prefetch (ADR-0009: evaluation is a pure
|
|
172
|
-
// cache read). It still has to be validated: a caller must not be able to
|
|
173
|
-
// bypass context limits/reserved-key checks just by moving an oversized or
|
|
174
|
-
// malformed structure into the per-call argument instead of the
|
|
175
|
-
// constructor's globalContext.
|
|
176
|
-
const merged = (0, context_js_1.mergeContexts)(this.globalContext, invocationContext);
|
|
177
|
-
const contextResult = (0, validation_js_1.validateContext)(merged, this.contextPolicy);
|
|
178
|
-
if (!contextResult.ok) {
|
|
179
|
-
return this.errorDecision(controlPointKey, defaultValue, contextResult.error);
|
|
180
|
-
}
|
|
181
|
-
const lifecycleError = this.lifecycleError();
|
|
182
|
-
if (lifecycleError !== undefined) {
|
|
183
|
-
return this.errorDecision(controlPointKey, defaultValue, lifecycleError);
|
|
184
|
-
}
|
|
185
|
-
const resolution = this.cache.get(controlPointKey);
|
|
186
|
-
if (resolution === undefined || resolution.found === false) {
|
|
187
|
-
// spec/modes.md "Behaviour per mode": local's unknown-key row is
|
|
188
|
-
// `default`/reason `DEFAULT` — deliberately not an error. The local
|
|
189
|
-
// adapter signals this via `missReason: 'DEFAULT'` (adapter.ts); every
|
|
190
|
-
// other adapter leaves it undefined and keeps the path below.
|
|
191
|
-
if (this.adapter.missReason === 'DEFAULT') {
|
|
192
|
-
return { controlPointKey, value: defaultValue, reason: 'DEFAULT', metadata: {} };
|
|
193
|
-
}
|
|
194
|
-
// A cache miss while STALE is not a missing control point — it is an
|
|
195
|
-
// unanswered question. Reporting FLAG_NOT_FOUND there would send a
|
|
196
|
-
// caller hunting for a flag that may well exist.
|
|
197
|
-
if (this.state === 'STALE') {
|
|
198
|
-
return {
|
|
199
|
-
controlPointKey,
|
|
200
|
-
value: defaultValue,
|
|
201
|
-
reason: 'STALE',
|
|
202
|
-
variant: 'default',
|
|
203
|
-
metadata: { 'fireweave.stale': true },
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
return this.errorDecision(controlPointKey, defaultValue, new errors_js_1.FireweaveError('ControlPointNotFound'));
|
|
207
|
-
}
|
|
208
|
-
const value = resolution.value ?? null;
|
|
209
|
-
if (!(0, validation_js_1.matchesExpectedType)(value, expectedType)) {
|
|
210
|
-
return this.errorDecision(controlPointKey, defaultValue, new errors_js_1.FireweaveError('TypeMismatch'));
|
|
211
|
-
}
|
|
212
|
-
const metadata = this.metadataFor(resolution);
|
|
213
|
-
const decision = {
|
|
214
|
-
controlPointKey,
|
|
215
|
-
value,
|
|
216
|
-
reason: this.reasonFor(resolution),
|
|
217
|
-
...(resolution.variant !== undefined ? { variant: resolution.variant } : {}),
|
|
218
|
-
...(metadata !== undefined ? { metadata } : {}),
|
|
219
|
-
};
|
|
220
|
-
if (this.config.sendExposure === true) {
|
|
221
|
-
this.recordExposureFor(decision, invocationContext);
|
|
222
|
-
}
|
|
223
|
-
return decision;
|
|
224
|
-
}
|
|
225
|
-
reasonFor(resolution) {
|
|
226
|
-
// A STALE runtime is serving the last good decisions after a failed or
|
|
227
|
-
// timed-out re-fetch (spec/control-points.md): say so, whatever reason the
|
|
228
|
-
// backend gave when the decision was fresh.
|
|
229
|
-
if (this.state === 'STALE')
|
|
230
|
-
return 'STALE';
|
|
231
|
-
if (resolution.enabled === false)
|
|
232
|
-
return 'DISABLED';
|
|
233
|
-
if (resolution.reason !== undefined)
|
|
234
|
-
return resolution.reason;
|
|
235
|
-
return 'TARGETING_MATCH';
|
|
236
|
-
}
|
|
237
|
-
metadataFor(resolution) {
|
|
238
|
-
const metadata = {};
|
|
239
|
-
if (resolution.version !== undefined)
|
|
240
|
-
metadata['fireweave.controlPointVersion'] = resolution.version;
|
|
241
|
-
if (resolution.reasonCode !== undefined)
|
|
242
|
-
metadata['fireweave.reasonCode'] = resolution.reasonCode;
|
|
243
|
-
if (resolution.quotaLimited === true)
|
|
244
|
-
metadata['fireweave.quotaLimited'] = true;
|
|
245
|
-
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
246
|
-
}
|
|
247
|
-
recordExposureFor(decision, invocationContext) {
|
|
248
|
-
if (this.adapter.recordExposure === undefined)
|
|
249
|
-
return;
|
|
250
|
-
const merged = (0, context_js_1.mergeContexts)(this.globalContext, invocationContext);
|
|
251
|
-
const targetingKey = typeof merged.targetingKey === 'string' ? merged.targetingKey : '';
|
|
252
|
-
if (targetingKey === '')
|
|
253
|
-
return;
|
|
254
|
-
const rc = this.releaseContext;
|
|
255
|
-
const exposure = {
|
|
256
|
-
controlPointKey: decision.controlPointKey,
|
|
257
|
-
targetingKey,
|
|
258
|
-
value: decision.value,
|
|
259
|
-
...(decision.variant !== undefined ? { variant: decision.variant } : {}),
|
|
260
|
-
...(rc.rolloutId !== undefined ? { rolloutId: rc.rolloutId } : {}),
|
|
261
|
-
...(rc.changeId !== undefined ? { changeId: rc.changeId } : {}),
|
|
262
|
-
...(rc.stampId !== undefined ? { stampId: rc.stampId } : {}),
|
|
263
|
-
};
|
|
264
|
-
this.adapter.recordExposure(exposure);
|
|
265
|
-
}
|
|
266
|
-
recordExposure(exposure) {
|
|
267
|
-
this.adapter.recordExposure?.(exposure);
|
|
268
|
-
}
|
|
269
|
-
recordSignal(signal) {
|
|
270
|
-
this.adapter.recordSignal?.(signal);
|
|
271
|
-
}
|
|
272
|
-
async registerTarget(targetingKey, options = {}) {
|
|
273
|
-
if (this.state === 'SHUTDOWN') {
|
|
274
|
-
return { ok: false, error: new errors_js_1.FireweaveError('AlreadyClosed') };
|
|
275
|
-
}
|
|
276
|
-
if (this.adapter.registerTarget === undefined) {
|
|
277
|
-
return { ok: false, error: new errors_js_1.FireweaveError('UnsupportedCapability') };
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
return await this.adapter.registerTarget(targetingKey, options);
|
|
281
|
-
}
|
|
282
|
-
catch (err) {
|
|
283
|
-
return {
|
|
284
|
-
ok: false,
|
|
285
|
-
error: (0, errors_js_1.isFireweaveError)(err) ? err : new errors_js_1.FireweaveError('BackendUnavailable'),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
async flush(options = {}) {
|
|
290
|
-
await this.adapter.flush?.(options);
|
|
291
|
-
}
|
|
292
|
-
async shutdown() {
|
|
293
|
-
if (this.state === 'SHUTDOWN')
|
|
294
|
-
return;
|
|
295
|
-
try {
|
|
296
|
-
await this.adapter.flush?.();
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
// best-effort flush; never throw from shutdown
|
|
300
|
-
}
|
|
301
|
-
try {
|
|
302
|
-
await this.adapter.shutdown();
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
// never throw from shutdown
|
|
306
|
-
}
|
|
307
|
-
this.cache = new Map();
|
|
308
|
-
this.setState('SHUTDOWN');
|
|
309
|
-
}
|
|
310
|
-
changedKeys(before, after) {
|
|
311
|
-
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
312
|
-
const changed = [];
|
|
313
|
-
for (const key of keys) {
|
|
314
|
-
const a = before.get(key);
|
|
315
|
-
const b = after.get(key);
|
|
316
|
-
if (JSON.stringify(a ?? null) !== JSON.stringify(b ?? null))
|
|
317
|
-
changed.push(key);
|
|
318
|
-
}
|
|
319
|
-
return changed;
|
|
320
|
-
}
|
|
321
|
-
lifecycleError() {
|
|
322
|
-
switch (this.state) {
|
|
323
|
-
case 'UNINITIALIZED':
|
|
324
|
-
case 'INITIALIZING':
|
|
325
|
-
return new errors_js_1.FireweaveError('NotReady');
|
|
326
|
-
case 'ERROR':
|
|
327
|
-
return new errors_js_1.FireweaveError('BackendUnavailable');
|
|
328
|
-
case 'SHUTDOWN':
|
|
329
|
-
return new errors_js_1.FireweaveError('AlreadyClosed');
|
|
330
|
-
default:
|
|
331
|
-
// READY, STALE: no lifecycle error — a STALE miss is handled
|
|
332
|
-
// specially in the cache-lookup branch above.
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
errorDecision(controlPointKey, defaultValue, err) {
|
|
337
|
-
return {
|
|
338
|
-
controlPointKey,
|
|
339
|
-
value: defaultValue,
|
|
340
|
-
reason: 'ERROR',
|
|
341
|
-
errorCode: err.openFeatureErrorCode,
|
|
342
|
-
errorKind: err.kind,
|
|
343
|
-
errorMessage: err.message,
|
|
344
|
-
metadata: { 'fireweave.errorKind': err.kind, ...err.metadata },
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
exports.FireweaveWebRuntime = FireweaveWebRuntime;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { PolicyConfig } from '../src/start/policy.js';
|
|
2
|
-
import { type BuildOptions } from './shared.js';
|
|
3
|
-
export interface FireweaveDefineOptions extends BuildOptions {
|
|
4
|
-
/** The variable holding the browser key. Default FIREWEAVE_BROWSER_KEY. */
|
|
5
|
-
readonly keyVariable?: string;
|
|
6
|
-
/** Read these values instead of process.env (tests). */
|
|
7
|
-
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
8
|
-
/** Where the summary line goes. Default console.info. */
|
|
9
|
-
readonly log?: (line: string) => void;
|
|
10
|
-
}
|
|
11
|
-
/** Fail the build (throw) when the start profile would not start; returns the resolved config. */
|
|
12
|
-
export declare function assertFireweaveBuild(options?: FireweaveDefineOptions): PolicyConfig;
|
|
13
|
-
/** assertFireweaveBuild, plus the define map that hands the result to start(). */
|
|
14
|
-
export declare function fireweaveDefine(options?: FireweaveDefineOptions): Record<string, string>;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.assertFireweaveBuild = assertFireweaveBuild;
|
|
4
|
-
exports.fireweaveDefine = fireweaveDefine;
|
|
5
|
-
/**
|
|
6
|
-
* @fireweaveai/web-sdk/define — the start profile's build check for bundlers
|
|
7
|
-
* other than Vite (Next.js, webpack, esbuild, Rollup). No vite import.
|
|
8
|
-
*
|
|
9
|
-
* // next.config.mjs (or any build config)
|
|
10
|
-
* import { fireweaveDefine } from '@fireweaveai/web-sdk/define';
|
|
11
|
-
* const define = fireweaveDefine(); // throws if the build would ship without a usable key
|
|
12
|
-
* // webpack: new webpack.DefinePlugin(define) · esbuild: { define } · Rollup: replace({ values: define })
|
|
13
|
-
*
|
|
14
|
-
* Reads process.env only: FIREWEAVE_BROWSER_KEY (or `keyVariable`),
|
|
15
|
-
* FIREWEAVE_URL, and the environment name FIREWEAVE_ENV, then APP_ENV, then
|
|
16
|
-
* NODE_ENV. Same policy and messages as the Vite plugin.
|
|
17
|
-
*/
|
|
18
|
-
const names_js_1 = require("../src/start/names.js");
|
|
19
|
-
const policy_js_1 = require("../src/start/policy.js");
|
|
20
|
-
const shared_js_1 = require("./shared.js");
|
|
21
|
-
function resolveFromEnv(options) {
|
|
22
|
-
const env = options.env ?? process.env;
|
|
23
|
-
const lookup = (name) => (0, policy_js_1.sourced)(env[name], name);
|
|
24
|
-
const resolved = (0, shared_js_1.resolveBuild)(options, lookup, options.keyVariable ?? names_js_1.BUILD_ENV.key, [names_js_1.BUILD_ENV.environment, names_js_1.ENVIRONMENT_FALLBACK, 'NODE_ENV']);
|
|
25
|
-
const log = options.log ?? ((line) => console.info(line));
|
|
26
|
-
for (const w of resolved.warnings)
|
|
27
|
-
log(w);
|
|
28
|
-
log((0, shared_js_1.summaryLine)(resolved.config));
|
|
29
|
-
return resolved;
|
|
30
|
-
}
|
|
31
|
-
/** Fail the build (throw) when the start profile would not start; returns the resolved config. */
|
|
32
|
-
function assertFireweaveBuild(options = {}) {
|
|
33
|
-
return resolveFromEnv(options).config;
|
|
34
|
-
}
|
|
35
|
-
/** assertFireweaveBuild, plus the define map that hands the result to start(). */
|
|
36
|
-
function fireweaveDefine(options = {}) {
|
|
37
|
-
return { [names_js_1.INJECTED_CONFIG_NAME]: JSON.stringify(resolveFromEnv(options).injected) };
|
|
38
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { type InjectedConfig, type PolicyConfig, type Sourced } from '../src/start/policy.js';
|
|
2
|
-
/** A variable read at build time: its value and a source label for logs (never the value). */
|
|
3
|
-
export type EnvLookup = (name: string) => Sourced | undefined;
|
|
4
|
-
export interface BuildOptions {
|
|
5
|
-
/** Browser key. Prefer FIREWEAVE_BROWSER_KEY in the build environment. */
|
|
6
|
-
readonly key?: string;
|
|
7
|
-
/** fw-server URL or a same-origin proxy path ('/fw'). Default: FIREWEAVE_URL, else the SDK channel host. */
|
|
8
|
-
readonly url?: string;
|
|
9
|
-
/** Environment name for the mode rule. Default: FIREWEAVE_ENV, else APP_ENV. */
|
|
10
|
-
readonly environment?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface BuildResolution {
|
|
13
|
-
readonly config: PolicyConfig;
|
|
14
|
-
readonly injected: InjectedConfig;
|
|
15
|
-
readonly warnings: readonly string[];
|
|
16
|
-
}
|
|
17
|
-
export declare function resolveBuild(options: BuildOptions, lookup: EnvLookup, keyVariable: string, environmentNames: readonly string[], extraEnvironment?: Sourced): BuildResolution;
|
|
18
|
-
/** One summary line for build output. Names sources, never values. */
|
|
19
|
-
export declare function summaryLine(config: PolicyConfig): string;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveBuild = resolveBuild;
|
|
4
|
-
exports.summaryLine = summaryLine;
|
|
5
|
-
/**
|
|
6
|
-
* Build-time resolution shared by the Vite plugin and the define helper.
|
|
7
|
-
* Runs in Node at config time, never in a browser. It reuses the browser's
|
|
8
|
-
* own policy (src/start/policy.ts), so a build fails for exactly the reasons
|
|
9
|
-
* the page would.
|
|
10
|
-
*/
|
|
11
|
-
const build_info_js_1 = require("../src/start/build-info.js");
|
|
12
|
-
const names_js_1 = require("../src/start/names.js");
|
|
13
|
-
const policy_js_1 = require("../src/start/policy.js");
|
|
14
|
-
function resolveBuild(options, lookup, keyVariable, environmentNames, extraEnvironment) {
|
|
15
|
-
const key = (0, policy_js_1.firstOf)((0, policy_js_1.sourced)(options.key, 'fireweave({ key })'), lookup(keyVariable));
|
|
16
|
-
const url = (0, policy_js_1.firstOf)((0, policy_js_1.sourced)(options.url, 'fireweave({ url })'), lookup(names_js_1.BUILD_ENV.url));
|
|
17
|
-
const environment = (0, policy_js_1.firstOf)((0, policy_js_1.sourced)(options.environment, 'fireweave({ environment })'), ...environmentNames.map((name) => lookup(name)), extraEnvironment);
|
|
18
|
-
const checked = [`fireweave({ environment })`, ...environmentNames, ...(extraEnvironment !== undefined ? [extraEnvironment.source] : [])].join(', ');
|
|
19
|
-
const policy = (0, policy_js_1.resolvePolicy)({ key, url, environment, channel: build_info_js_1.SDK_CHANNEL, keyVariable, environmentChecked: checked });
|
|
20
|
-
if (!policy.ok)
|
|
21
|
-
throw new Error(policy.message);
|
|
22
|
-
const config = policy.config;
|
|
23
|
-
const injected = {
|
|
24
|
-
v: 1,
|
|
25
|
-
...(config.mode === 'remote' && key !== undefined ? { key: key.value, keySource: key.source } : {}),
|
|
26
|
-
...(url !== undefined ? { url: url.value, urlSource: url.source } : {}),
|
|
27
|
-
...(environment !== undefined ? { environment: environment.value, environmentSource: environment.source } : {}),
|
|
28
|
-
};
|
|
29
|
-
return { config, injected, warnings: policy.warnings };
|
|
30
|
-
}
|
|
31
|
-
/** One summary line for build output. Names sources, never values. */
|
|
32
|
-
function summaryLine(config) {
|
|
33
|
-
if (config.mode === 'local') {
|
|
34
|
-
const why = config.modeSource === 'environment' ? `environment '${config.environment ?? ''}' from ${config.environmentSource ?? ''}` : 'mode option';
|
|
35
|
-
return `[fireweave] local mode (${why}): values come from your control-points object; nothing is sent to fw-server.`;
|
|
36
|
-
}
|
|
37
|
-
const host = config.url !== undefined && !config.url.startsWith('/') ? new URL(config.url).host : config.url;
|
|
38
|
-
return `[fireweave] remote mode: browser key from ${config.keySource}, fw-server ${host} (${config.urlSource ?? ''}).`;
|
|
39
|
-
}
|