@shipeasy/sdk 6.3.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -4
- package/dist/client/index.d.mts +166 -27
- package/dist/client/index.d.ts +166 -27
- package/dist/client/index.js +268 -132
- package/dist/client/index.mjs +268 -132
- package/dist/openfeature-server/index.d.mts +144 -14
- package/dist/openfeature-server/index.d.ts +144 -14
- package/dist/openfeature-server/index.js +65 -33
- package/dist/openfeature-server/index.mjs +65 -33
- package/dist/openfeature-web/index.d.mts +110 -30
- package/dist/openfeature-web/index.d.ts +110 -30
- package/dist/server/index.d.mts +181 -28
- package/dist/server/index.d.ts +181 -28
- package/dist/server/index.js +337 -159
- package/dist/server/index.mjs +337 -159
- package/docs/skill/SKILL.md +20 -10
- package/package.json +3 -1
package/dist/server/index.d.mts
CHANGED
|
@@ -125,6 +125,10 @@ interface ExperimentResult<P> {
|
|
|
125
125
|
inExperiment: boolean;
|
|
126
126
|
group: string;
|
|
127
127
|
params: P;
|
|
128
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
129
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
130
|
+
* finding the enrolled experiment in a universe. */
|
|
131
|
+
universe?: string;
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
134
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -170,13 +174,46 @@ interface ExperimentGroup {
|
|
|
170
174
|
interface Experiment {
|
|
171
175
|
universe: string;
|
|
172
176
|
targetingGate?: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
179
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
180
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
181
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
182
|
+
*/
|
|
183
|
+
holdoutGate?: string | null;
|
|
173
184
|
allocationPct: number;
|
|
185
|
+
/**
|
|
186
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
187
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
188
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
189
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
190
|
+
* legacy independent-salt `allocationPct` gate.
|
|
191
|
+
*/
|
|
192
|
+
poolOffsetBp?: number | null;
|
|
193
|
+
poolSizeBp?: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
196
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
197
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
198
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
199
|
+
*/
|
|
200
|
+
reservedHeadroomBp?: number | null;
|
|
174
201
|
salt: string;
|
|
175
202
|
groups: ExperimentGroup[];
|
|
176
203
|
status: "draft" | "running" | "stopped" | "archived";
|
|
204
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
205
|
+
hashVersion?: number;
|
|
177
206
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
178
207
|
bucketBy?: string | null;
|
|
179
208
|
}
|
|
209
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
210
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
211
|
+
interface UniverseParam {
|
|
212
|
+
name: string;
|
|
213
|
+
type: "bool" | "int" | "number" | "string";
|
|
214
|
+
default: unknown;
|
|
215
|
+
}
|
|
216
|
+
type UniverseParamSchema = UniverseParam[];
|
|
180
217
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
181
218
|
interface StickyEntry {
|
|
182
219
|
g: string;
|
|
@@ -197,6 +234,13 @@ interface StickyBucketStore {
|
|
|
197
234
|
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
198
235
|
interface Universe {
|
|
199
236
|
holdout_range: [number, number] | null;
|
|
237
|
+
/**
|
|
238
|
+
* The universe's config schema — the single source of truth for param names,
|
|
239
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
240
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
241
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
242
|
+
*/
|
|
243
|
+
param_schema?: UniverseParamSchema | null;
|
|
200
244
|
}
|
|
201
245
|
interface Killswitch {
|
|
202
246
|
killed: 0 | 1 | boolean;
|
|
@@ -223,8 +267,36 @@ interface BootstrapPayload {
|
|
|
223
267
|
configs: Record<string, unknown>;
|
|
224
268
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
225
269
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
270
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
271
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
272
|
+
* not enrolled in any experiment there. Only universes with running
|
|
273
|
+
* experiments are included. */
|
|
274
|
+
universes?: Record<string, {
|
|
275
|
+
defaults: Record<string, unknown>;
|
|
276
|
+
}>;
|
|
226
277
|
}
|
|
227
278
|
declare const ANON_ID_COOKIE = "__se_anon_id";
|
|
279
|
+
/**
|
|
280
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
281
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
282
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
283
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
284
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
285
|
+
*/
|
|
286
|
+
interface Assignment {
|
|
287
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
288
|
+
readonly name: string | null;
|
|
289
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
290
|
+
readonly group: string | null;
|
|
291
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
292
|
+
readonly enrolled: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
295
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
296
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
297
|
+
*/
|
|
298
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
299
|
+
}
|
|
228
300
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
229
301
|
interface EngineOptions {
|
|
230
302
|
apiKey: string;
|
|
@@ -237,12 +309,31 @@ interface EngineOptions {
|
|
|
237
309
|
*/
|
|
238
310
|
initialBlob?: FlagsBlob;
|
|
239
311
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
312
|
+
* Master network switch — when `false`, the SDK makes NO outbound requests at
|
|
313
|
+
* all: init()/initOnce() never fetch (getters read code defaults / overrides),
|
|
314
|
+
* track() and exposure logging are no-ops, usage telemetry is off, and
|
|
315
|
+
* SDK-internal error self-monitoring is off. Think of it as a production-safe
|
|
316
|
+
* offline mode.
|
|
317
|
+
*
|
|
318
|
+
* DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
|
|
319
|
+
* production, OFF everywhere else — so a local/dev/CI run never phones home
|
|
320
|
+
* unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
|
|
321
|
+
* (when neither is set, e.g. on Cloudflare Workers) from the `env` option
|
|
322
|
+
* above, which defaults to `"prod"`. Pass an explicit value to override.
|
|
323
|
+
*/
|
|
324
|
+
isNetworkEnabled?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Per-evaluation usage telemetry ("tracking"/outside logging). Each
|
|
327
|
+
* getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
|
|
328
|
+
* loop) fires one fire-and-forget beacon counted by Cloudflare's native
|
|
329
|
+
* per-path analytics.
|
|
330
|
+
*
|
|
331
|
+
* DEFAULT is environment-derived: ON in production, OFF everywhere else (same
|
|
332
|
+
* inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
|
|
333
|
+
* to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
|
|
334
|
+
* subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
|
|
335
|
+
* request paths that evaluate many flags per request. Forced off whenever
|
|
336
|
+
* `isNetworkEnabled` is false.
|
|
246
337
|
*/
|
|
247
338
|
disableTelemetry?: boolean;
|
|
248
339
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
@@ -257,6 +348,14 @@ interface EngineOptions {
|
|
|
257
348
|
* SDK entirely. See {@link LogLevel}.
|
|
258
349
|
*/
|
|
259
350
|
logLevel?: LogLevel;
|
|
351
|
+
/**
|
|
352
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
353
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
354
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
355
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
356
|
+
* `true` to disable.
|
|
357
|
+
*/
|
|
358
|
+
disableInternalErrorReporting?: boolean;
|
|
260
359
|
/**
|
|
261
360
|
* Attribute names usable for targeting but never persisted in analytics
|
|
262
361
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -286,6 +385,8 @@ declare class Engine {
|
|
|
286
385
|
private readonly env;
|
|
287
386
|
private readonly privateAttributes;
|
|
288
387
|
private readonly stickyStore;
|
|
388
|
+
private readonly networkEnabled;
|
|
389
|
+
private readonly offline;
|
|
289
390
|
private readonly telemetry;
|
|
290
391
|
private readonly seeLimiter;
|
|
291
392
|
private flagsBlob;
|
|
@@ -299,6 +400,7 @@ declare class Engine {
|
|
|
299
400
|
private readonly flagOverrides;
|
|
300
401
|
private readonly configOverrides;
|
|
301
402
|
private readonly experimentOverrides;
|
|
403
|
+
private readonly exposureSeen;
|
|
302
404
|
private readonly changeListeners;
|
|
303
405
|
constructor(opts: EngineOptions);
|
|
304
406
|
/**
|
|
@@ -378,19 +480,47 @@ declare class Engine {
|
|
|
378
480
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
379
481
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
380
482
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
381
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
485
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
486
|
+
*/
|
|
487
|
+
private bindSticky;
|
|
488
|
+
/**
|
|
489
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
490
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
491
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
492
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
493
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
494
|
+
*/
|
|
495
|
+
private evalExperiment;
|
|
496
|
+
/**
|
|
497
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
498
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
499
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
500
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
501
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
502
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
503
|
+
*/
|
|
504
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
505
|
+
/**
|
|
506
|
+
* The universe-first experiment read entry point:
|
|
507
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
508
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
509
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
510
|
+
*/
|
|
511
|
+
universe(name: string): {
|
|
512
|
+
assign(user: User): Assignment;
|
|
513
|
+
};
|
|
382
514
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
383
515
|
private stripPrivate;
|
|
384
516
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
385
517
|
/**
|
|
386
|
-
*
|
|
387
|
-
* (
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
391
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
518
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
519
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
520
|
+
* spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
|
|
521
|
+
* is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
392
522
|
*/
|
|
393
|
-
|
|
523
|
+
private postExposure;
|
|
394
524
|
/**
|
|
395
525
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
396
526
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -482,11 +612,26 @@ interface ShipeasyServerConfig {
|
|
|
482
612
|
*/
|
|
483
613
|
logLevel?: LogLevel;
|
|
484
614
|
/**
|
|
485
|
-
*
|
|
615
|
+
* Master network switch — `false` puts the SDK fully offline (no fetch, no
|
|
616
|
+
* track, no telemetry, no error self-monitoring). Defaults to
|
|
617
|
+
* environment-derived: ON in production, OFF everywhere else. See
|
|
618
|
+
* {@link EngineOptions.isNetworkEnabled}.
|
|
619
|
+
*/
|
|
620
|
+
isNetworkEnabled?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Disable per-evaluation usage telemetry ("tracking"). Defaults to
|
|
623
|
+
* environment-derived: ON in production, OFF everywhere else. On Cloudflare
|
|
486
624
|
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
487
625
|
* that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
|
|
488
626
|
*/
|
|
489
627
|
disableTelemetry?: boolean;
|
|
628
|
+
/**
|
|
629
|
+
* Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
|
|
630
|
+
* our end") are reported to Shipeasy's own project (never yours) so we can
|
|
631
|
+
* track SDK bugs. ON by default. See
|
|
632
|
+
* {@link EngineOptions.disableInternalErrorReporting}.
|
|
633
|
+
*/
|
|
634
|
+
disableInternalErrorReporting?: boolean;
|
|
490
635
|
/**
|
|
491
636
|
* Attribute names usable for targeting but never persisted in analytics
|
|
492
637
|
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
@@ -582,7 +727,17 @@ declare const flags: {
|
|
|
582
727
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
583
728
|
getDetail(name: string, user: User): FlagDetail;
|
|
584
729
|
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
585
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Assign `user` within a universe: `flags.universe("checkout").assign(user)`.
|
|
732
|
+
* A universe is a mutual-exclusion pool, so the unit lands in ≤1 experiment;
|
|
733
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
734
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on any
|
|
735
|
+
* error) it returns a safe not-enrolled handle. This replaces the removed
|
|
736
|
+
* `getExperiment` — read experiments by universe, never by name.
|
|
737
|
+
*/
|
|
738
|
+
universe(name: string): {
|
|
739
|
+
assign(user: User): Assignment;
|
|
740
|
+
};
|
|
586
741
|
/**
|
|
587
742
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
588
743
|
* killswitch is killed. With `switchKey`, returns true when that specific
|
|
@@ -590,9 +745,6 @@ declare const flags: {
|
|
|
590
745
|
*/
|
|
591
746
|
ks(name: string, switchKey?: string): boolean;
|
|
592
747
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
593
|
-
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
594
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
595
|
-
logExposure(user: string | User, name: string): void;
|
|
596
748
|
/**
|
|
597
749
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
598
750
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -740,7 +892,15 @@ declare class Client<U = unknown> {
|
|
|
740
892
|
getFlagDetail(name: string): FlagDetail;
|
|
741
893
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
742
894
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
743
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Assign the bound user within a universe: `client.universe("checkout").assign()`.
|
|
897
|
+
* The user is already bound at construction, so `assign()` takes no arg. Returns
|
|
898
|
+
* an {@link Assignment} (`.group` / `.get(field, fallback)`) and auto-logs one
|
|
899
|
+
* exposure when enrolled. Replaces the removed `getExperiment`.
|
|
900
|
+
*/
|
|
901
|
+
universe(name: string): {
|
|
902
|
+
assign(): Assignment;
|
|
903
|
+
};
|
|
744
904
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
745
905
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
746
906
|
/**
|
|
@@ -751,13 +911,6 @@ declare class Client<U = unknown> {
|
|
|
751
911
|
* mode.
|
|
752
912
|
*/
|
|
753
913
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
754
|
-
/**
|
|
755
|
-
* Emit an exposure event for `name` at this server-side decision point for the
|
|
756
|
-
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
757
|
-
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
758
|
-
* in test mode).
|
|
759
|
-
*/
|
|
760
|
-
logExposure(name: string): void;
|
|
761
914
|
}
|
|
762
915
|
interface SeeApi {
|
|
763
916
|
/**
|
|
@@ -810,4 +963,4 @@ interface SeeApi {
|
|
|
810
963
|
*/
|
|
811
964
|
declare const see: SeeApi;
|
|
812
965
|
|
|
813
|
-
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, LOG_LEVELS, type LabelFile, type LogLevel, 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 };
|
|
966
|
+
export { ANON_ID_COOKIE, type Assignment, 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, LOG_LEVELS, type LabelFile, type LogLevel, 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 UniverseParam, type UniverseParamSchema, 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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -125,6 +125,10 @@ interface ExperimentResult<P> {
|
|
|
125
125
|
inExperiment: boolean;
|
|
126
126
|
group: string;
|
|
127
127
|
params: P;
|
|
128
|
+
/** The universe this experiment belongs to. Carried in the SSR bootstrap +
|
|
129
|
+
* `/sdk/evaluate` so the client can resolve `universe(name).assign()` by
|
|
130
|
+
* finding the enrolled experiment in a universe. */
|
|
131
|
+
universe?: string;
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
134
|
* Why a flag evaluated the way it did (LaunchDarkly variationDetail parity).
|
|
@@ -170,13 +174,46 @@ interface ExperimentGroup {
|
|
|
170
174
|
interface Experiment {
|
|
171
175
|
universe: string;
|
|
172
176
|
targetingGate?: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* Per-experiment holdout flag (a `type='holdout'` gate). When set and the flag
|
|
179
|
+
* passes for a unit, that unit is *held out* of THIS experiment — never
|
|
180
|
+
* assigned, sees the universe defaults. Checked after the universe holdout,
|
|
181
|
+
* before allocation. Mirrors @shipeasy/core §B3.
|
|
182
|
+
*/
|
|
183
|
+
holdoutGate?: string | null;
|
|
173
184
|
allocationPct: number;
|
|
185
|
+
/**
|
|
186
|
+
* Contiguous half-open slice `[poolOffsetBp, poolOffsetBp + poolSizeBp)` of the
|
|
187
|
+
* *universe* hash space this experiment claims. Used only when `hashVersion >= 2`
|
|
188
|
+
* (pooled assignment / real mutual exclusion, §B4): the unit's universe segment
|
|
189
|
+
* must fall in this range to be in the experiment. Null ⇒ fall back to the
|
|
190
|
+
* legacy independent-salt `allocationPct` gate.
|
|
191
|
+
*/
|
|
192
|
+
poolOffsetBp?: number | null;
|
|
193
|
+
poolSizeBp?: number | null;
|
|
194
|
+
/**
|
|
195
|
+
* A tail of the group split kept empty (basis points) so a new variant can be
|
|
196
|
+
* appended into it while running without reshuffling enrolled units. Group
|
|
197
|
+
* weights sum to `10000 − reservedHeadroomBp`; a unit hashing into the reserved
|
|
198
|
+
* tail is treated as not-assigned (sees universe defaults). §B5.
|
|
199
|
+
*/
|
|
200
|
+
reservedHeadroomBp?: number | null;
|
|
174
201
|
salt: string;
|
|
175
202
|
groups: ExperimentGroup[];
|
|
176
203
|
status: "draft" | "running" | "stopped" | "archived";
|
|
204
|
+
/** Bucketing scheme version. `>= 2` unlocks pooled (mutually-exclusive) assignment. */
|
|
205
|
+
hashVersion?: number;
|
|
177
206
|
/** Attribute to bucket on (e.g. company_id); defaults to user_id/anonymous_id. */
|
|
178
207
|
bucketBy?: string | null;
|
|
179
208
|
}
|
|
209
|
+
/** One param the universe owns: a name, a type, and the default a variant
|
|
210
|
+
* inherits when it doesn't override the value (§A/§B2). */
|
|
211
|
+
interface UniverseParam {
|
|
212
|
+
name: string;
|
|
213
|
+
type: "bool" | "int" | "number" | "string";
|
|
214
|
+
default: unknown;
|
|
215
|
+
}
|
|
216
|
+
type UniverseParamSchema = UniverseParam[];
|
|
180
217
|
/** One persisted sticky assignment: group + 8-char salt prefix (reshuffle key). */
|
|
181
218
|
interface StickyEntry {
|
|
182
219
|
g: string;
|
|
@@ -197,6 +234,13 @@ interface StickyBucketStore {
|
|
|
197
234
|
declare function createInMemoryStickyStore(seed?: Record<string, Record<string, StickyEntry>>): StickyBucketStore;
|
|
198
235
|
interface Universe {
|
|
199
236
|
holdout_range: [number, number] | null;
|
|
237
|
+
/**
|
|
238
|
+
* The universe's config schema — the single source of truth for param names,
|
|
239
|
+
* types, and defaults (§A). The `assign()` merge layers these defaults *under*
|
|
240
|
+
* an assigned variant's overrides so an unset param still returns the universe
|
|
241
|
+
* default. Null/absent ⇒ no defaults (legacy: variant params returned verbatim).
|
|
242
|
+
*/
|
|
243
|
+
param_schema?: UniverseParamSchema | null;
|
|
200
244
|
}
|
|
201
245
|
interface Killswitch {
|
|
202
246
|
killed: 0 | 1 | boolean;
|
|
@@ -223,8 +267,36 @@ interface BootstrapPayload {
|
|
|
223
267
|
configs: Record<string, unknown>;
|
|
224
268
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
225
269
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
270
|
+
/** Per-universe param defaults (name → default map) so the client can resolve
|
|
271
|
+
* `universe(name).get(field)` to the universe default even when the unit is
|
|
272
|
+
* not enrolled in any experiment there. Only universes with running
|
|
273
|
+
* experiments are included. */
|
|
274
|
+
universes?: Record<string, {
|
|
275
|
+
defaults: Record<string, unknown>;
|
|
276
|
+
}>;
|
|
226
277
|
}
|
|
227
278
|
declare const ANON_ID_COOKIE = "__se_anon_id";
|
|
279
|
+
/**
|
|
280
|
+
* The result of `universe(name).assign(user)` — a user's standing in a universe.
|
|
281
|
+
* A universe is a mutual-exclusion pool, so a unit lands in **at most one**
|
|
282
|
+
* experiment. Never throws: an un-enrolled unit still resolves `get()` to the
|
|
283
|
+
* universe defaults (or your fallback). Reading is side-effect free — the single
|
|
284
|
+
* exposure is logged once by `assign()` when the unit is enrolled.
|
|
285
|
+
*/
|
|
286
|
+
interface Assignment {
|
|
287
|
+
/** The experiment the unit landed in, or `null` when not enrolled. */
|
|
288
|
+
readonly name: string | null;
|
|
289
|
+
/** The assigned variant/group name, or `null` when not enrolled. */
|
|
290
|
+
readonly group: string | null;
|
|
291
|
+
/** True iff the unit is enrolled in an experiment in this universe. */
|
|
292
|
+
readonly enrolled: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* Read a resolved param: the assigned variant's override, else the universe
|
|
295
|
+
* default, else `fallback`. Works even when not enrolled (variant layer is
|
|
296
|
+
* absent, so you get `universeDefault ?? fallback`).
|
|
297
|
+
*/
|
|
298
|
+
get<T = unknown>(field: string, fallback?: T): T | undefined;
|
|
299
|
+
}
|
|
228
300
|
type EngineEnv = "dev" | "staging" | "prod";
|
|
229
301
|
interface EngineOptions {
|
|
230
302
|
apiKey: string;
|
|
@@ -237,12 +309,31 @@ interface EngineOptions {
|
|
|
237
309
|
*/
|
|
238
310
|
initialBlob?: FlagsBlob;
|
|
239
311
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
312
|
+
* Master network switch — when `false`, the SDK makes NO outbound requests at
|
|
313
|
+
* all: init()/initOnce() never fetch (getters read code defaults / overrides),
|
|
314
|
+
* track() and exposure logging are no-ops, usage telemetry is off, and
|
|
315
|
+
* SDK-internal error self-monitoring is off. Think of it as a production-safe
|
|
316
|
+
* offline mode.
|
|
317
|
+
*
|
|
318
|
+
* DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
|
|
319
|
+
* production, OFF everywhere else — so a local/dev/CI run never phones home
|
|
320
|
+
* unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
|
|
321
|
+
* (when neither is set, e.g. on Cloudflare Workers) from the `env` option
|
|
322
|
+
* above, which defaults to `"prod"`. Pass an explicit value to override.
|
|
323
|
+
*/
|
|
324
|
+
isNetworkEnabled?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Per-evaluation usage telemetry ("tracking"/outside logging). Each
|
|
327
|
+
* getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
|
|
328
|
+
* loop) fires one fire-and-forget beacon counted by Cloudflare's native
|
|
329
|
+
* per-path analytics.
|
|
330
|
+
*
|
|
331
|
+
* DEFAULT is environment-derived: ON in production, OFF everywhere else (same
|
|
332
|
+
* inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
|
|
333
|
+
* to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
|
|
334
|
+
* subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
|
|
335
|
+
* request paths that evaluate many flags per request. Forced off whenever
|
|
336
|
+
* `isNetworkEnabled` is false.
|
|
246
337
|
*/
|
|
247
338
|
disableTelemetry?: boolean;
|
|
248
339
|
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
@@ -257,6 +348,14 @@ interface EngineOptions {
|
|
|
257
348
|
* SDK entirely. See {@link LogLevel}.
|
|
258
349
|
*/
|
|
259
350
|
logLevel?: LogLevel;
|
|
351
|
+
/**
|
|
352
|
+
* Opt out of SDK-internal error self-monitoring. When one of the SDK's own
|
|
353
|
+
* last-resort guards catches an internal failure (an "on our end" bug), the
|
|
354
|
+
* SDK reports it to Shipeasy's own project so we can track SDK bugs across
|
|
355
|
+
* apps — never to your project. ON by default; forced off in test mode. Pass
|
|
356
|
+
* `true` to disable.
|
|
357
|
+
*/
|
|
358
|
+
disableInternalErrorReporting?: boolean;
|
|
260
359
|
/**
|
|
261
360
|
* Attribute names usable for targeting but never persisted in analytics
|
|
262
361
|
* (LD/Statsig `privateAttributes`). The server evaluates locally so private
|
|
@@ -286,6 +385,8 @@ declare class Engine {
|
|
|
286
385
|
private readonly env;
|
|
287
386
|
private readonly privateAttributes;
|
|
288
387
|
private readonly stickyStore;
|
|
388
|
+
private readonly networkEnabled;
|
|
389
|
+
private readonly offline;
|
|
289
390
|
private readonly telemetry;
|
|
290
391
|
private readonly seeLimiter;
|
|
291
392
|
private flagsBlob;
|
|
@@ -299,6 +400,7 @@ declare class Engine {
|
|
|
299
400
|
private readonly flagOverrides;
|
|
300
401
|
private readonly configOverrides;
|
|
301
402
|
private readonly experimentOverrides;
|
|
403
|
+
private readonly exposureSeen;
|
|
302
404
|
private readonly changeListeners;
|
|
303
405
|
constructor(opts: EngineOptions);
|
|
304
406
|
/**
|
|
@@ -378,19 +480,47 @@ declare class Engine {
|
|
|
378
480
|
getFlag(name: string, user: User, defaultValue?: boolean): boolean;
|
|
379
481
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
380
482
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
381
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Bind the per-experiment sticky seam over the configured {@link StickyBucketStore}
|
|
485
|
+
* for one (unit, experiment). Absent store ⇒ deterministic (no sticky).
|
|
486
|
+
*/
|
|
487
|
+
private bindSticky;
|
|
488
|
+
/**
|
|
489
|
+
* Evaluate one experiment by name for `user` — override → full classify
|
|
490
|
+
* pipeline (targeting → universe holdout → holdout gate → sticky → allocation
|
|
491
|
+
* → group), merging the universe defaults under the assigned variant (§B2).
|
|
492
|
+
* Internal: the public surface is `universe(name).assign(user)`. Reused by the
|
|
493
|
+
* SSR `evaluate()` bootstrap (keyed by experiment name) and by `assignUniverse`.
|
|
494
|
+
*/
|
|
495
|
+
private evalExperiment;
|
|
496
|
+
/**
|
|
497
|
+
* Assign `user` within `universeName`. A universe is a mutual-exclusion pool,
|
|
498
|
+
* so a unit lands in **at most one** experiment; the returned {@link Assignment}
|
|
499
|
+
* exposes the variant + resolved params and auto-logs a single exposure when
|
|
500
|
+
* enrolled. An un-enrolled unit still resolves `get()` to the universe defaults.
|
|
501
|
+
* Never throws. This is the sole experiment read path (there is no
|
|
502
|
+
* `getExperiment` — a caller asks a universe, not an experiment).
|
|
503
|
+
*/
|
|
504
|
+
assignUniverse(universeName: string, user: User): Assignment;
|
|
505
|
+
/**
|
|
506
|
+
* The universe-first experiment read entry point:
|
|
507
|
+
* `engine.universe("checkout").assign(user)`. Returns a reusable handle bound
|
|
508
|
+
* to one universe; `assign(user)` picks the ≤1 experiment the unit is pooled
|
|
509
|
+
* into and auto-logs a single exposure. See {@link assignUniverse}.
|
|
510
|
+
*/
|
|
511
|
+
universe(name: string): {
|
|
512
|
+
assign(user: User): Assignment;
|
|
513
|
+
};
|
|
382
514
|
/** Drop caller-marked private attributes from an outbound props bag. */
|
|
383
515
|
private stripPrivate;
|
|
384
516
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
385
517
|
/**
|
|
386
|
-
*
|
|
387
|
-
* (
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* as `{ user_id }`); if the user is enrolled, POSTs a single exposure to
|
|
391
|
-
* `/collect`. No-op in test mode or when the user isn't enrolled.
|
|
518
|
+
* POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
|
|
519
|
+
* per process (bounded set) so repeated `assign()` calls in one server don't
|
|
520
|
+
* spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
|
|
521
|
+
* is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
|
|
392
522
|
*/
|
|
393
|
-
|
|
523
|
+
private postExposure;
|
|
394
524
|
/**
|
|
395
525
|
* Report a structured error into the errors primitive. Fire-and-forget —
|
|
396
526
|
* never blocks or throws into the request path. Spam-guarded by a 30s
|
|
@@ -482,11 +612,26 @@ interface ShipeasyServerConfig {
|
|
|
482
612
|
*/
|
|
483
613
|
logLevel?: LogLevel;
|
|
484
614
|
/**
|
|
485
|
-
*
|
|
615
|
+
* Master network switch — `false` puts the SDK fully offline (no fetch, no
|
|
616
|
+
* track, no telemetry, no error self-monitoring). Defaults to
|
|
617
|
+
* environment-derived: ON in production, OFF everywhere else. See
|
|
618
|
+
* {@link EngineOptions.isNetworkEnabled}.
|
|
619
|
+
*/
|
|
620
|
+
isNetworkEnabled?: boolean;
|
|
621
|
+
/**
|
|
622
|
+
* Disable per-evaluation usage telemetry ("tracking"). Defaults to
|
|
623
|
+
* environment-derived: ON in production, OFF everywhere else. On Cloudflare
|
|
486
624
|
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
487
625
|
* that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
|
|
488
626
|
*/
|
|
489
627
|
disableTelemetry?: boolean;
|
|
628
|
+
/**
|
|
629
|
+
* Opt out of SDK-internal error self-monitoring. Internal SDK failures ("on
|
|
630
|
+
* our end") are reported to Shipeasy's own project (never yours) so we can
|
|
631
|
+
* track SDK bugs. ON by default. See
|
|
632
|
+
* {@link EngineOptions.disableInternalErrorReporting}.
|
|
633
|
+
*/
|
|
634
|
+
disableInternalErrorReporting?: boolean;
|
|
490
635
|
/**
|
|
491
636
|
* Attribute names usable for targeting but never persisted in analytics
|
|
492
637
|
* (LD/Statsig `privateAttributes`). Stripped from every outbound `track()`
|
|
@@ -582,7 +727,17 @@ declare const flags: {
|
|
|
582
727
|
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
583
728
|
getDetail(name: string, user: User): FlagDetail;
|
|
584
729
|
getConfig<T = unknown>(name: string, decodeOrOpts?: ((raw: unknown) => T) | GetConfigOptions<T>): T | undefined;
|
|
585
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Assign `user` within a universe: `flags.universe("checkout").assign(user)`.
|
|
732
|
+
* A universe is a mutual-exclusion pool, so the unit lands in ≤1 experiment;
|
|
733
|
+
* the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
|
|
734
|
+
* and auto-logs one exposure when enrolled. Before configure() (or on any
|
|
735
|
+
* error) it returns a safe not-enrolled handle. This replaces the removed
|
|
736
|
+
* `getExperiment` — read experiments by universe, never by name.
|
|
737
|
+
*/
|
|
738
|
+
universe(name: string): {
|
|
739
|
+
assign(user: User): Assignment;
|
|
740
|
+
};
|
|
586
741
|
/**
|
|
587
742
|
* Read a killswitch. Without `switchKey`, returns true when the whole
|
|
588
743
|
* killswitch is killed. With `switchKey`, returns true when that specific
|
|
@@ -590,9 +745,6 @@ declare const flags: {
|
|
|
590
745
|
*/
|
|
591
746
|
ks(name: string, switchKey?: string): boolean;
|
|
592
747
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
593
|
-
/** Emit an exposure for an enrolled experiment at the decision point. See
|
|
594
|
-
* {@link Engine.logExposure}. No-op before configure(). */
|
|
595
|
-
logExposure(user: string | User, name: string): void;
|
|
596
748
|
/**
|
|
597
749
|
* Evaluate all flags / configs / experiments for a user against the locally
|
|
598
750
|
* cached blob. Pass the request URL to apply ?se_ks_* / ?se_cf_* / ?se_exp_*
|
|
@@ -740,7 +892,15 @@ declare class Client<U = unknown> {
|
|
|
740
892
|
getFlagDetail(name: string): FlagDetail;
|
|
741
893
|
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
742
894
|
getConfig<T = unknown>(name: string, opts: GetConfigOptions<T>): T;
|
|
743
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Assign the bound user within a universe: `client.universe("checkout").assign()`.
|
|
897
|
+
* The user is already bound at construction, so `assign()` takes no arg. Returns
|
|
898
|
+
* an {@link Assignment} (`.group` / `.get(field, fallback)`) and auto-logs one
|
|
899
|
+
* exposure when enrolled. Replaces the removed `getExperiment`.
|
|
900
|
+
*/
|
|
901
|
+
universe(name: string): {
|
|
902
|
+
assign(): Assignment;
|
|
903
|
+
};
|
|
744
904
|
/** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
|
|
745
905
|
getKillswitch(name: string, switchKey?: string): boolean;
|
|
746
906
|
/**
|
|
@@ -751,13 +911,6 @@ declare class Client<U = unknown> {
|
|
|
751
911
|
* mode.
|
|
752
912
|
*/
|
|
753
913
|
track(eventName: string, props?: Record<string, unknown>): void;
|
|
754
|
-
/**
|
|
755
|
-
* Emit an exposure event for `name` at this server-side decision point for the
|
|
756
|
-
* bound user. Delegates to {@link Engine.logExposure} with the resolved
|
|
757
|
-
* attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
|
|
758
|
-
* in test mode).
|
|
759
|
-
*/
|
|
760
|
-
logExposure(name: string): void;
|
|
761
914
|
}
|
|
762
915
|
interface SeeApi {
|
|
763
916
|
/**
|
|
@@ -810,4 +963,4 @@ interface SeeApi {
|
|
|
810
963
|
*/
|
|
811
964
|
declare const see: SeeApi;
|
|
812
965
|
|
|
813
|
-
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, LOG_LEVELS, type LabelFile, type LogLevel, 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 };
|
|
966
|
+
export { ANON_ID_COOKIE, type Assignment, 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, LOG_LEVELS, type LabelFile, type LogLevel, 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 UniverseParam, type UniverseParamSchema, 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 };
|