@fireweaveai/web-sdk-cjs 0.0.0-reserve.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/application/client.d.ts +104 -0
- package/dist/application/client.js +147 -0
- package/dist/application/mode.d.ts +99 -0
- package/dist/application/mode.js +101 -0
- package/dist/application/ports.d.ts +101 -0
- package/dist/application/ports.js +2 -0
- package/dist/application/runtime.d.ts +70 -0
- package/dist/application/runtime.js +348 -0
- package/dist/build/node/define.d.ts +14 -0
- package/dist/build/node/define.js +38 -0
- package/dist/build/node/shared.d.ts +19 -0
- package/dist/build/node/shared.js +39 -0
- package/dist/build/node/vite.d.ts +53 -0
- package/dist/build/node/vite.js +106 -0
- package/dist/build/src/start/build-info.d.ts +2 -0
- package/dist/build/src/start/build-info.js +9 -0
- package/dist/build/src/start/names.d.ts +38 -0
- package/dist/build/src/start/names.js +41 -0
- package/dist/build/src/start/policy.d.ts +70 -0
- package/dist/build/src/start/policy.js +128 -0
- package/dist/domain/context.d.ts +51 -0
- package/dist/domain/context.js +72 -0
- package/dist/domain/errors.d.ts +36 -0
- package/dist/domain/errors.js +97 -0
- package/dist/domain/target.d.ts +2 -0
- package/dist/domain/target.js +2 -0
- package/dist/domain/types.d.ts +89 -0
- package/dist/domain/types.js +10 -0
- package/dist/domain/validation.d.ts +128 -0
- package/dist/domain/validation.js +358 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +66 -0
- package/dist/infrastructure/adapters/inmemory.d.ts +50 -0
- package/dist/infrastructure/adapters/inmemory.js +94 -0
- package/dist/infrastructure/adapters/local.d.ts +79 -0
- package/dist/infrastructure/adapters/local.js +98 -0
- package/dist/infrastructure/adapters/remote.d.ts +44 -0
- package/dist/infrastructure/adapters/remote.js +329 -0
- package/dist/infrastructure/hosts.d.ts +10 -0
- package/dist/infrastructure/hosts.js +84 -0
- package/dist/start/build-info.d.ts +2 -0
- package/dist/start/build-info.js +9 -0
- package/dist/start/control-points.d.ts +24 -0
- package/dist/start/control-points.js +61 -0
- package/dist/start/fw.d.ts +42 -0
- package/dist/start/fw.js +137 -0
- package/dist/start/identity.d.ts +19 -0
- package/dist/start/identity.js +93 -0
- package/dist/start/index.d.ts +34 -0
- package/dist/start/index.js +38 -0
- package/dist/start/names.d.ts +38 -0
- package/dist/start/names.js +41 -0
- package/dist/start/policy.d.ts +70 -0
- package/dist/start/policy.js +128 -0
- package/dist/start/state.d.ts +103 -0
- package/dist/start/state.js +375 -0
- package/package.json +68 -0
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
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;
|
|
@@ -0,0 +1,14 @@
|
|
|
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>;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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;
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type BuildOptions } from './shared.js';
|
|
2
|
+
export type FireweavePluginOptions = BuildOptions;
|
|
3
|
+
/** The subset of Vite's types the plugin touches, so the package needs no vite types to build. */
|
|
4
|
+
interface UserConfigLike {
|
|
5
|
+
readonly root?: string;
|
|
6
|
+
readonly envDir?: string | false;
|
|
7
|
+
readonly build?: {
|
|
8
|
+
readonly lib?: unknown;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
interface ConfigEnvLike {
|
|
12
|
+
readonly command: 'serve' | 'build';
|
|
13
|
+
readonly mode: string;
|
|
14
|
+
readonly isPreview?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface ResolvedConfigLike {
|
|
17
|
+
readonly envPrefix?: string | readonly string[];
|
|
18
|
+
readonly build?: {
|
|
19
|
+
readonly ssr?: unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
type OutputFileLike = {
|
|
23
|
+
readonly type: 'chunk';
|
|
24
|
+
readonly code: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: 'asset';
|
|
27
|
+
readonly source: string | Uint8Array;
|
|
28
|
+
};
|
|
29
|
+
interface PluginContextLike {
|
|
30
|
+
readonly environment?: {
|
|
31
|
+
readonly config?: {
|
|
32
|
+
readonly consumer?: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export type LoadEnv = (mode: string, envDir: string, prefixes: string[]) => Record<string, string> | Promise<Record<string, string>>;
|
|
37
|
+
export interface FireweaveVitePlugin {
|
|
38
|
+
readonly name: 'fireweave';
|
|
39
|
+
readonly enforce: 'pre';
|
|
40
|
+
config(userConfig: UserConfigLike, env: ConfigEnvLike): Promise<Record<string, unknown> | undefined>;
|
|
41
|
+
configResolved(config: ResolvedConfigLike): void;
|
|
42
|
+
generateBundle(this: PluginContextLike, options: unknown, bundle: Readonly<Record<string, OutputFileLike>>): void;
|
|
43
|
+
}
|
|
44
|
+
/** Test seam: the env-file loader and the process environment. */
|
|
45
|
+
export interface PluginDeps {
|
|
46
|
+
readonly loadEnv?: LoadEnv;
|
|
47
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
48
|
+
readonly log?: (line: string) => void;
|
|
49
|
+
}
|
|
50
|
+
export declare function createFireweavePlugin(options?: FireweavePluginOptions, deps?: PluginDeps): FireweaveVitePlugin;
|
|
51
|
+
/** The fireweave() Vite plugin. Add it to `plugins` in vite.config.ts (or a framework's `vite.plugins`). */
|
|
52
|
+
export declare function fireweave(options?: FireweavePluginOptions): FireweaveVitePlugin;
|
|
53
|
+
export {};
|