@shipeasy/sdk 6.0.0 → 6.2.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.
@@ -25,6 +25,177 @@ __export(openfeature_server_exports, {
25
25
  module.exports = __toCommonJS(openfeature_server_exports);
26
26
  var import_server_sdk = require("@openfeature/server-sdk");
27
27
 
28
+ // src/server/index.ts
29
+ var import_node_async_hooks = require("async_hooks");
30
+
31
+ // src/see/core.ts
32
+ var SEE_MAX_SUBJECT = 200;
33
+ var SEE_MAX_EXTRA_VALUE = 200;
34
+ var SEE_MAX_EXTRA_KEYS = 20;
35
+ function causesThe(subject) {
36
+ return {
37
+ to(outcome) {
38
+ return {
39
+ __seConsequence: true,
40
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
41
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
42
+ };
43
+ }
44
+ };
45
+ }
46
+ function violation(name) {
47
+ return { __seViolation: true, violationName: String(name) };
48
+ }
49
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
50
+ function readExpectedMark(err) {
51
+ if (typeof err !== "object" || err === null) return void 0;
52
+ const v = err[EXPECTED_SYM];
53
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
54
+ }
55
+ function markExpected(err, because, extras) {
56
+ if (typeof err !== "object" || err === null) return;
57
+ const prev = readExpectedMark(err);
58
+ const clean = sanitizeExtras(extras);
59
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
60
+ const mark = {
61
+ because: String(because),
62
+ ...merged ? { extras: merged } : {}
63
+ };
64
+ try {
65
+ Object.defineProperty(err, EXPECTED_SYM, {
66
+ value: mark,
67
+ enumerable: false,
68
+ configurable: true
69
+ });
70
+ } catch {
71
+ }
72
+ }
73
+ function truncate(s, max) {
74
+ return s.length > max ? s.slice(0, max) : s;
75
+ }
76
+ function sanitizeExtras(extras) {
77
+ if (!extras || typeof extras !== "object") return void 0;
78
+ const out = {};
79
+ let n = 0;
80
+ for (const [k, v] of Object.entries(extras)) {
81
+ if (v === null || v === void 0) continue;
82
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
83
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
84
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
85
+ else if (typeof v === "boolean") out[k] = v;
86
+ else continue;
87
+ n += 1;
88
+ }
89
+ return n > 0 ? out : void 0;
90
+ }
91
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
92
+ void Promise.resolve().then(cb);
93
+ };
94
+ function startSeeChain(getProblem, dispatch) {
95
+ let subject;
96
+ let outcome;
97
+ let collected;
98
+ let flushed = false;
99
+ scheduleMicrotask(() => {
100
+ if (flushed) return;
101
+ flushed = true;
102
+ dispatch(
103
+ getProblem(),
104
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
105
+ // leading article would double up ("causes the the app").
106
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
107
+ collected
108
+ );
109
+ });
110
+ const tail = {
111
+ extras(x) {
112
+ if (x && typeof x === "object") collected = { ...collected, ...x };
113
+ return tail;
114
+ }
115
+ };
116
+ const step = {
117
+ to(o) {
118
+ outcome = String(o);
119
+ return tail;
120
+ }
121
+ };
122
+ const start = (s) => {
123
+ subject = String(s);
124
+ return step;
125
+ };
126
+ return { causes_the: start, causesThe: start };
127
+ }
128
+ function startSeeViolationChain(name, dispatch) {
129
+ return startSeeChain(() => violation(name), dispatch);
130
+ }
131
+ function startControlFlowChain(err) {
132
+ return {
133
+ because(reason) {
134
+ markExpected(err, reason);
135
+ const tail = {
136
+ extras(x) {
137
+ markExpected(err, reason, x);
138
+ return tail;
139
+ }
140
+ };
141
+ return tail;
142
+ }
143
+ };
144
+ }
145
+
146
+ // src/server/index.ts
147
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
148
+ var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
149
+ var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
150
+ var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
151
+ var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
152
+ globalThis[_I18N_SSR_SYM] = () => {
153
+ const fromALS = _i18nALS.getStore();
154
+ if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
155
+ for (const v of _i18nCache.values()) {
156
+ if (Object.keys(v.strings).length > 0) return v;
157
+ }
158
+ return fromALS ?? null;
159
+ };
160
+ var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
161
+ var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
162
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
163
+ if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
164
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
165
+ }
166
+ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
167
+ get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
168
+ set: (v) => {
169
+ const b = Boolean(v);
170
+ try {
171
+ _editModeALS.enterWith(b);
172
+ } catch {
173
+ }
174
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
175
+ },
176
+ configurable: true
177
+ });
178
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
179
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
180
+ var _server = null;
181
+ function getShipeasyServerClient() {
182
+ return _server;
183
+ }
184
+ function dispatchSee(problem, consequence, extras, kind) {
185
+ if (!_server) {
186
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
187
+ return;
188
+ }
189
+ _server.reportError(problem, consequence, extras, kind);
190
+ }
191
+ var see = Object.assign(
192
+ (problem) => startSeeChain(() => problem, dispatchSee),
193
+ {
194
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
195
+ ControlFlowException: (err) => startControlFlowChain(err)
196
+ }
197
+ );
198
+
28
199
  // src/openfeature/shared.ts
29
200
  var REASON_MAP = {
30
201
  RULE_MATCH: { reason: "TARGETING_MATCH" },
@@ -78,12 +249,31 @@ function resolveConfig(raw, defaultValue, type) {
78
249
  return { value: r.value, reason: "TARGETING_MATCH" };
79
250
  }
80
251
  var ShipeasyProvider = class {
81
- constructor(client) {
82
- this.client = client;
83
- }
84
- client;
85
252
  metadata = { name: "shipeasy" };
86
253
  runsOn = "server";
254
+ client;
255
+ /**
256
+ * Construct the provider. The **global form** (no argument) resolves the
257
+ * engine built by `configure({ apiKey })`, so the docs build it after
258
+ * configuration without ever naming the `Engine`:
259
+ *
260
+ * ```ts
261
+ * configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
262
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider());
263
+ * ```
264
+ *
265
+ * Passing an explicit `Engine` stays supported for advanced/back-compat use.
266
+ * Throws if no engine is passed and `configure()` has not run.
267
+ */
268
+ constructor(client) {
269
+ const resolved = client ?? getShipeasyServerClient();
270
+ if (!resolved) {
271
+ throw new Error(
272
+ "[shipeasy] new ShipeasyProvider() resolves the configured global engine \u2014 call configure({ apiKey }) first, or pass an Engine explicitly."
273
+ );
274
+ }
275
+ this.client = resolved;
276
+ }
87
277
  /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
88
278
  async initialize() {
89
279
  await this.client.initOnce();
@@ -1,6 +1,177 @@
1
1
  // src/openfeature-server/index.ts
2
2
  import { ErrorCode } from "@openfeature/server-sdk";
3
3
 
4
+ // src/server/index.ts
5
+ import { AsyncLocalStorage } from "async_hooks";
6
+
7
+ // src/see/core.ts
8
+ var SEE_MAX_SUBJECT = 200;
9
+ var SEE_MAX_EXTRA_VALUE = 200;
10
+ var SEE_MAX_EXTRA_KEYS = 20;
11
+ function causesThe(subject) {
12
+ return {
13
+ to(outcome) {
14
+ return {
15
+ __seConsequence: true,
16
+ subject: truncate(String(subject), SEE_MAX_SUBJECT),
17
+ outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
18
+ };
19
+ }
20
+ };
21
+ }
22
+ function violation(name) {
23
+ return { __seViolation: true, violationName: String(name) };
24
+ }
25
+ var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
26
+ function readExpectedMark(err) {
27
+ if (typeof err !== "object" || err === null) return void 0;
28
+ const v = err[EXPECTED_SYM];
29
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
30
+ }
31
+ function markExpected(err, because, extras) {
32
+ if (typeof err !== "object" || err === null) return;
33
+ const prev = readExpectedMark(err);
34
+ const clean = sanitizeExtras(extras);
35
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
36
+ const mark = {
37
+ because: String(because),
38
+ ...merged ? { extras: merged } : {}
39
+ };
40
+ try {
41
+ Object.defineProperty(err, EXPECTED_SYM, {
42
+ value: mark,
43
+ enumerable: false,
44
+ configurable: true
45
+ });
46
+ } catch {
47
+ }
48
+ }
49
+ function truncate(s, max) {
50
+ return s.length > max ? s.slice(0, max) : s;
51
+ }
52
+ function sanitizeExtras(extras) {
53
+ if (!extras || typeof extras !== "object") return void 0;
54
+ const out = {};
55
+ let n = 0;
56
+ for (const [k, v] of Object.entries(extras)) {
57
+ if (v === null || v === void 0) continue;
58
+ if (n >= SEE_MAX_EXTRA_KEYS) break;
59
+ if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
60
+ else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
61
+ else if (typeof v === "boolean") out[k] = v;
62
+ else continue;
63
+ n += 1;
64
+ }
65
+ return n > 0 ? out : void 0;
66
+ }
67
+ var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
68
+ void Promise.resolve().then(cb);
69
+ };
70
+ function startSeeChain(getProblem, dispatch) {
71
+ let subject;
72
+ let outcome;
73
+ let collected;
74
+ let flushed = false;
75
+ scheduleMicrotask(() => {
76
+ if (flushed) return;
77
+ flushed = true;
78
+ dispatch(
79
+ getProblem(),
80
+ // Bare noun phrase — titles render as "… causes the {subject} …", so a
81
+ // leading article would double up ("causes the the app").
82
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
83
+ collected
84
+ );
85
+ });
86
+ const tail = {
87
+ extras(x) {
88
+ if (x && typeof x === "object") collected = { ...collected, ...x };
89
+ return tail;
90
+ }
91
+ };
92
+ const step = {
93
+ to(o) {
94
+ outcome = String(o);
95
+ return tail;
96
+ }
97
+ };
98
+ const start = (s) => {
99
+ subject = String(s);
100
+ return step;
101
+ };
102
+ return { causes_the: start, causesThe: start };
103
+ }
104
+ function startSeeViolationChain(name, dispatch) {
105
+ return startSeeChain(() => violation(name), dispatch);
106
+ }
107
+ function startControlFlowChain(err) {
108
+ return {
109
+ because(reason) {
110
+ markExpected(err, reason);
111
+ const tail = {
112
+ extras(x) {
113
+ markExpected(err, reason, x);
114
+ return tail;
115
+ }
116
+ };
117
+ return tail;
118
+ }
119
+ };
120
+ }
121
+
122
+ // src/server/index.ts
123
+ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
124
+ var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
125
+ var _i18nALS = new AsyncLocalStorage();
126
+ var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
127
+ var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
128
+ globalThis[_I18N_SSR_SYM] = () => {
129
+ const fromALS = _i18nALS.getStore();
130
+ if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
131
+ for (const v of _i18nCache.values()) {
132
+ if (Object.keys(v.strings).length > 0) return v;
133
+ }
134
+ return fromALS ?? null;
135
+ };
136
+ var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
137
+ var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new AsyncLocalStorage());
138
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
139
+ if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
140
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
141
+ }
142
+ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
143
+ get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
144
+ set: (v) => {
145
+ const b = Boolean(v);
146
+ try {
147
+ _editModeALS.enterWith(b);
148
+ } catch {
149
+ }
150
+ globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
151
+ },
152
+ configurable: true
153
+ });
154
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
155
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new AsyncLocalStorage());
156
+ var _server = null;
157
+ function getShipeasyServerClient() {
158
+ return _server;
159
+ }
160
+ function dispatchSee(problem, consequence, extras, kind) {
161
+ if (!_server) {
162
+ console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
163
+ return;
164
+ }
165
+ _server.reportError(problem, consequence, extras, kind);
166
+ }
167
+ var see = Object.assign(
168
+ (problem) => startSeeChain(() => problem, dispatchSee),
169
+ {
170
+ Violation: (name) => startSeeViolationChain(name, dispatchSee),
171
+ ControlFlowException: (err) => startControlFlowChain(err)
172
+ }
173
+ );
174
+
4
175
  // src/openfeature/shared.ts
5
176
  var REASON_MAP = {
6
177
  RULE_MATCH: { reason: "TARGETING_MATCH" },
@@ -54,12 +225,31 @@ function resolveConfig(raw, defaultValue, type) {
54
225
  return { value: r.value, reason: "TARGETING_MATCH" };
55
226
  }
56
227
  var ShipeasyProvider = class {
57
- constructor(client) {
58
- this.client = client;
59
- }
60
- client;
61
228
  metadata = { name: "shipeasy" };
62
229
  runsOn = "server";
230
+ client;
231
+ /**
232
+ * Construct the provider. The **global form** (no argument) resolves the
233
+ * engine built by `configure({ apiKey })`, so the docs build it after
234
+ * configuration without ever naming the `Engine`:
235
+ *
236
+ * ```ts
237
+ * configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
238
+ * await OpenFeature.setProviderAndWait(new ShipeasyProvider());
239
+ * ```
240
+ *
241
+ * Passing an explicit `Engine` stays supported for advanced/back-compat use.
242
+ * Throws if no engine is passed and `configure()` has not run.
243
+ */
244
+ constructor(client) {
245
+ const resolved = client ?? getShipeasyServerClient();
246
+ if (!resolved) {
247
+ throw new Error(
248
+ "[shipeasy] new ShipeasyProvider() resolves the configured global engine \u2014 call configure({ apiKey }) first, or pass an Engine explicitly."
249
+ );
250
+ }
251
+ this.client = resolved;
252
+ }
63
253
  /** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
64
254
  async initialize() {
65
255
  await this.client.initOnce();
@@ -591,6 +591,20 @@ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
591
591
  * verbatim, so it should carry `user_id`/`anonymous_id` + any targeting attrs).
592
592
  */
593
593
  attributes?: AttributesFn<U>;
594
+ /**
595
+ * Long-running server: start the **background poll** internally (initial fetch
596
+ * + periodic refresh) so flags stay fresh without a redeploy. Default `false`.
597
+ * With `poll: false` (default) a one-shot fetch is kicked off fire-and-forget
598
+ * (serverless-friendly). You never need to call `engine.init()` yourself —
599
+ * configuration owns the lifecycle.
600
+ */
601
+ poll?: boolean;
602
+ /**
603
+ * One-shot fetch on configure (fire-and-forget). Default `true`. Ignored when
604
+ * `poll: true` (the poll does the initial fetch). Set `false` only if you want
605
+ * to control the first fetch yourself.
606
+ */
607
+ init?: boolean;
594
608
  }
595
609
  /**
596
610
  * Configure the SDK once at app boot, then evaluate per user with
@@ -613,6 +627,77 @@ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
613
627
  declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
614
628
  /** Test seam: reset the registered attribute transform. */
615
629
  declare function _resetConfigureForTests(): void;
630
+ /** Seed shapes shared by {@link configureForTesting} / {@link configureForOffline}. */
631
+ interface ConfigureTestOptions<U = unknown> {
632
+ /** Same transform as {@link configure} (default identity). */
633
+ attributes?: AttributesFn<U>;
634
+ /** `{ name: bool }` — forced `getFlag` results. */
635
+ flags?: Record<string, boolean>;
636
+ /** `{ name: value }` — forced `getConfig` results. */
637
+ configs?: Record<string, unknown>;
638
+ /** `{ name: [group, params] }` — forced enrolments. */
639
+ experiments?: Record<string, [string, Record<string, unknown>]>;
640
+ }
641
+ /**
642
+ * Configure the SDK in **test mode** — a drop-in sibling of {@link configure}
643
+ * with no network, ever (no api key needed). Seed what your code under test
644
+ * should see, then read through the ordinary `new Client(user)`:
645
+ *
646
+ * ```ts
647
+ * configureForTesting({ flags: { new_checkout: true } });
648
+ * const flags = new Client({ user_id: "u_1" });
649
+ * flags.getFlag("new_checkout"); // true
650
+ * ```
651
+ *
652
+ * Replaces any previously-configured engine, so tests can reconfigure freely.
653
+ */
654
+ declare function configureForTesting<U = unknown>(opts?: ConfigureTestOptions<U>): Engine;
655
+ /** Options for {@link configureForOffline} — exactly one of `snapshot` / `path`. */
656
+ interface ConfigureOfflineOptions<U = unknown> extends ConfigureTestOptions<U> {
657
+ /** In-memory `{ flags: <body of /sdk/flags>, experiments: <body of /sdk/experiments> }`. */
658
+ snapshot?: {
659
+ flags: FlagsBlob;
660
+ experiments: ExpsBlob;
661
+ };
662
+ /** Path to a JSON file `{ flags, experiments }`. */
663
+ path?: string;
664
+ }
665
+ /**
666
+ * Configure the SDK **offline** — evaluate the REAL rules from an in-memory
667
+ * snapshot or a JSON file, with no network. A drop-in sibling of
668
+ * {@link configure} (no api key needed). Optional `flags`/`configs`/`experiments`
669
+ * overrides are layered on top (same shapes as {@link configureForTesting}).
670
+ * Replaces any previously-configured engine.
671
+ */
672
+ declare function configureForOffline<U = unknown>(opts: ConfigureOfflineOptions<U>): Engine;
673
+ /**
674
+ * Force `getFlag(name)` → `value` on the spot, for the current config. A quick
675
+ * in-test override layered on top of whatever {@link configureForTesting} /
676
+ * {@link configureForOffline} (or {@link configure}) set up — wins over the blob
677
+ * until {@link clearOverrides}.
678
+ */
679
+ declare function overrideFlag(name: string, value: boolean): void;
680
+ /** Force `getConfig(name)` → `value` on the spot (see {@link overrideFlag}). */
681
+ declare function overrideConfig(name: string, value: unknown): void;
682
+ /**
683
+ * Force `getExperiment(name)` to report enrolment in `group` with `params` on
684
+ * the spot (see {@link overrideFlag}).
685
+ */
686
+ declare function overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
687
+ /**
688
+ * Drop every on-the-spot flag/config/experiment override — INCLUDING the seed
689
+ * from {@link configureForTesting} (test mode has no blob beneath, so everything
690
+ * reverts to the empty-blob defaults). Under {@link configureForOffline} the
691
+ * snapshot remains and evaluations revert to it.
692
+ */
693
+ declare function clearOverrides(): void;
694
+ /**
695
+ * Register a listener fired after a background poll fetches NEW data (a 200, not
696
+ * a 304). Returns an unsubscribe callable. Requires `configure({ poll: true })`
697
+ * (no poll thread runs otherwise). Configuration owns the engine; you never
698
+ * touch it.
699
+ */
700
+ declare function onChange(listener: () => void): () => void;
616
701
  /**
617
702
  * A user-bound evaluation handle. Construct one per user/request — it's cheap
618
703
  * (it delegates to the {@link Engine} built by {@link configure}); it does NOT
@@ -637,6 +722,21 @@ declare class Client<U = unknown> {
637
722
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
638
723
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
639
724
  getKillswitch(name: string, switchKey?: string): boolean;
725
+ /**
726
+ * Record a conversion/metric event for the bound user. Derives the unit from
727
+ * the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
728
+ * {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
729
+ * drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
730
+ * mode.
731
+ */
732
+ track(eventName: string, props?: Record<string, unknown>): void;
733
+ /**
734
+ * Emit an exposure event for `name` at this server-side decision point for the
735
+ * bound user. Delegates to {@link Engine.logExposure} with the resolved
736
+ * attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
737
+ * in test mode).
738
+ */
739
+ logExposure(name: string): void;
640
740
  }
641
741
  interface SeeApi {
642
742
  /**
@@ -689,4 +789,4 @@ interface SeeApi {
689
789
  */
690
790
  declare const see: SeeApi;
691
791
 
692
- export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, configure, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
792
+ export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };
@@ -591,6 +591,20 @@ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
591
591
  * verbatim, so it should carry `user_id`/`anonymous_id` + any targeting attrs).
592
592
  */
593
593
  attributes?: AttributesFn<U>;
594
+ /**
595
+ * Long-running server: start the **background poll** internally (initial fetch
596
+ * + periodic refresh) so flags stay fresh without a redeploy. Default `false`.
597
+ * With `poll: false` (default) a one-shot fetch is kicked off fire-and-forget
598
+ * (serverless-friendly). You never need to call `engine.init()` yourself —
599
+ * configuration owns the lifecycle.
600
+ */
601
+ poll?: boolean;
602
+ /**
603
+ * One-shot fetch on configure (fire-and-forget). Default `true`. Ignored when
604
+ * `poll: true` (the poll does the initial fetch). Set `false` only if you want
605
+ * to control the first fetch yourself.
606
+ */
607
+ init?: boolean;
594
608
  }
595
609
  /**
596
610
  * Configure the SDK once at app boot, then evaluate per user with
@@ -613,6 +627,77 @@ interface ConfigureOptions<U = unknown> extends Omit<EngineOptions, "apiKey"> {
613
627
  declare function configure<U = unknown>(opts: ConfigureOptions<U>): Engine;
614
628
  /** Test seam: reset the registered attribute transform. */
615
629
  declare function _resetConfigureForTests(): void;
630
+ /** Seed shapes shared by {@link configureForTesting} / {@link configureForOffline}. */
631
+ interface ConfigureTestOptions<U = unknown> {
632
+ /** Same transform as {@link configure} (default identity). */
633
+ attributes?: AttributesFn<U>;
634
+ /** `{ name: bool }` — forced `getFlag` results. */
635
+ flags?: Record<string, boolean>;
636
+ /** `{ name: value }` — forced `getConfig` results. */
637
+ configs?: Record<string, unknown>;
638
+ /** `{ name: [group, params] }` — forced enrolments. */
639
+ experiments?: Record<string, [string, Record<string, unknown>]>;
640
+ }
641
+ /**
642
+ * Configure the SDK in **test mode** — a drop-in sibling of {@link configure}
643
+ * with no network, ever (no api key needed). Seed what your code under test
644
+ * should see, then read through the ordinary `new Client(user)`:
645
+ *
646
+ * ```ts
647
+ * configureForTesting({ flags: { new_checkout: true } });
648
+ * const flags = new Client({ user_id: "u_1" });
649
+ * flags.getFlag("new_checkout"); // true
650
+ * ```
651
+ *
652
+ * Replaces any previously-configured engine, so tests can reconfigure freely.
653
+ */
654
+ declare function configureForTesting<U = unknown>(opts?: ConfigureTestOptions<U>): Engine;
655
+ /** Options for {@link configureForOffline} — exactly one of `snapshot` / `path`. */
656
+ interface ConfigureOfflineOptions<U = unknown> extends ConfigureTestOptions<U> {
657
+ /** In-memory `{ flags: <body of /sdk/flags>, experiments: <body of /sdk/experiments> }`. */
658
+ snapshot?: {
659
+ flags: FlagsBlob;
660
+ experiments: ExpsBlob;
661
+ };
662
+ /** Path to a JSON file `{ flags, experiments }`. */
663
+ path?: string;
664
+ }
665
+ /**
666
+ * Configure the SDK **offline** — evaluate the REAL rules from an in-memory
667
+ * snapshot or a JSON file, with no network. A drop-in sibling of
668
+ * {@link configure} (no api key needed). Optional `flags`/`configs`/`experiments`
669
+ * overrides are layered on top (same shapes as {@link configureForTesting}).
670
+ * Replaces any previously-configured engine.
671
+ */
672
+ declare function configureForOffline<U = unknown>(opts: ConfigureOfflineOptions<U>): Engine;
673
+ /**
674
+ * Force `getFlag(name)` → `value` on the spot, for the current config. A quick
675
+ * in-test override layered on top of whatever {@link configureForTesting} /
676
+ * {@link configureForOffline} (or {@link configure}) set up — wins over the blob
677
+ * until {@link clearOverrides}.
678
+ */
679
+ declare function overrideFlag(name: string, value: boolean): void;
680
+ /** Force `getConfig(name)` → `value` on the spot (see {@link overrideFlag}). */
681
+ declare function overrideConfig(name: string, value: unknown): void;
682
+ /**
683
+ * Force `getExperiment(name)` to report enrolment in `group` with `params` on
684
+ * the spot (see {@link overrideFlag}).
685
+ */
686
+ declare function overrideExperiment(name: string, group: string, params: Record<string, unknown>): void;
687
+ /**
688
+ * Drop every on-the-spot flag/config/experiment override — INCLUDING the seed
689
+ * from {@link configureForTesting} (test mode has no blob beneath, so everything
690
+ * reverts to the empty-blob defaults). Under {@link configureForOffline} the
691
+ * snapshot remains and evaluations revert to it.
692
+ */
693
+ declare function clearOverrides(): void;
694
+ /**
695
+ * Register a listener fired after a background poll fetches NEW data (a 200, not
696
+ * a 304). Returns an unsubscribe callable. Requires `configure({ poll: true })`
697
+ * (no poll thread runs otherwise). Configuration owns the engine; you never
698
+ * touch it.
699
+ */
700
+ declare function onChange(listener: () => void): () => void;
616
701
  /**
617
702
  * A user-bound evaluation handle. Construct one per user/request — it's cheap
618
703
  * (it delegates to the {@link Engine} built by {@link configure}); it does NOT
@@ -637,6 +722,21 @@ declare class Client<U = unknown> {
637
722
  getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
638
723
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
639
724
  getKillswitch(name: string, switchKey?: string): boolean;
725
+ /**
726
+ * Record a conversion/metric event for the bound user. Derives the unit from
727
+ * the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
728
+ * {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
729
+ * drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
730
+ * mode.
731
+ */
732
+ track(eventName: string, props?: Record<string, unknown>): void;
733
+ /**
734
+ * Emit an exposure event for `name` at this server-side decision point for the
735
+ * bound user. Delegates to {@link Engine.logExposure} with the resolved
736
+ * attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
737
+ * in test mode).
738
+ */
739
+ logExposure(name: string): void;
640
740
  }
641
741
  interface SeeApi {
642
742
  /**
@@ -689,4 +789,4 @@ interface SeeApi {
689
789
  */
690
790
  declare const see: SeeApi;
691
791
 
692
- export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, configure, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
792
+ export { ANON_ID_COOKIE, type AttributesFn, type BootstrapData, type BootstrapEmitOptions, type BootstrapPayload, Client, type ConfigureOfflineOptions, type ConfigureOptions, type ConfigureTestOptions, type Consequence, Engine, type EngineEnv, type EngineOptions, type ExperimentResult, type ExpsBlob, FLAG_REASONS, type FetchLabelsOptions, type FlagDetail, type FlagReason, type FlagsBlob, type GetConfigOptions, type I18nForRequest, type LabelFile, type ScriptTagSpec, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type StickyBucketStore, type StickyEntry, type User, type Violation, _murmur3ForTests, _resetConfigureForTests, _resetShipeasyServerForTests, clearOverrides, configure, configureForOffline, configureForTesting, configureShipeasyServer, createInMemoryStickyStore, fetchLabelsForSSR, flags, getBootstrapData, getBootstrapTags, getShipeasyServerClient, i18n, isExpected, onChange, overrideConfig, overrideExperiment, overrideFlag, see, seeContext, shipeasy, version };