@lucida-analytics/sdk-core 0.1.0 → 0.1.1
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/dist/index.d.ts +19 -2
- package/dist/index.js +67 -19
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Result of one transport attempt. status 0 means a network failure. */
|
|
2
2
|
interface TransportResult {
|
|
3
3
|
status: number;
|
|
4
|
+
/** The response's Retry-After in ms (429/503), when the transport can read it. The queue waits at least this long (capped at 60 s). */
|
|
5
|
+
retryAfterMs?: number;
|
|
4
6
|
}
|
|
5
7
|
interface SendOptions {
|
|
6
8
|
/** Fire-and-forget delivery that survives page unload (navigator.sendBeacon). */
|
|
@@ -187,6 +189,7 @@ declare class EventQueue {
|
|
|
187
189
|
private attempt;
|
|
188
190
|
private inflight;
|
|
189
191
|
private closed;
|
|
192
|
+
private warned404;
|
|
190
193
|
private readonly flushAt;
|
|
191
194
|
private readonly intervalMs;
|
|
192
195
|
private readonly maxSize;
|
|
@@ -686,6 +689,8 @@ type FilterOpV2 = "icontains" | "not_icontains" | "starts_with" | "not_starts_wi
|
|
|
686
689
|
/** Operator semantics: port of go/internal/flageval/ops.go (the reference). */
|
|
687
690
|
|
|
688
691
|
type FilterOp = "eq" | "neq" | "contains" | "ncontains" | "regex" | "nregex" | "gt" | "gte" | "lt" | "lte" | "is_set" | "is_not_set" | "in" | "not_in" | FilterOpV2 | FilterOpList;
|
|
692
|
+
/** Missing/null properties match only is_not_set; every other op (even negations) is false. */
|
|
693
|
+
declare function matchOp(op: FilterOp, prop: unknown, present: boolean, want: unknown, semver: boolean, nowMs?: number): boolean;
|
|
689
694
|
|
|
690
695
|
interface PropertyFilter {
|
|
691
696
|
/** `event_property` only inside an event filter's `properties`. */
|
|
@@ -889,7 +894,7 @@ declare class FlagFreshness {
|
|
|
889
894
|
/**
|
|
890
895
|
* Persistent sticky-bucketing store (LC-EXT-29): the value each unit got from a sticky flag, so it
|
|
891
896
|
* keeps it when rollout or variant weights change. Keyed by the flag's bucketing key.
|
|
892
|
-
*
|
|
897
|
+
* Synchronous (fits memory and localStorage); for an async backend use AsyncStickyStore + evaluateAllStickyAsync.
|
|
893
898
|
*/
|
|
894
899
|
interface StickyStore {
|
|
895
900
|
get(bucketingKey: string): Record<string, FlagValue> | undefined;
|
|
@@ -907,6 +912,18 @@ declare class MemoryStickyStore implements StickyStore {
|
|
|
907
912
|
declare const stickyValue: (f: FlagDefinition, r: EvalResult) => FlagValue | undefined;
|
|
908
913
|
/** evaluateAll with stored assignments loaded from `store` and fresh sticky matches written back. */
|
|
909
914
|
declare function evaluateAllSticky(flags: FlagDefinition[], c: EvalContext, store: StickyStore): Record<string, EvalResult>;
|
|
915
|
+
/** Sticky store whose reads and writes are asynchronous (Redis, a database, React Native storage). */
|
|
916
|
+
interface AsyncStickyStore {
|
|
917
|
+
get(bucketingKey: string): Promise<Record<string, FlagValue> | undefined>;
|
|
918
|
+
set(bucketingKey: string, flagKey: string, value: FlagValue): Promise<void>;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* evaluateAllSticky over an async store: the assignments of every bucketing key the sticky flags use are
|
|
922
|
+
* awaited first (so evaluation sees the stored values), evaluation runs on that snapshot, and the fresh
|
|
923
|
+
* matches are written back (queued, then awaited together). A failed read or write only means re-hashing
|
|
924
|
+
* next time; it never fails the evaluation.
|
|
925
|
+
*/
|
|
926
|
+
declare function evaluateAllStickyAsync(flags: FlagDefinition[], c: EvalContext, store: AsyncStickyStore): Promise<Record<string, EvalResult>>;
|
|
910
927
|
|
|
911
928
|
/**
|
|
912
929
|
* Migration flags (LC-FLG-24): a multivariate flag whose variants are the six stages of moving a data
|
|
@@ -1094,4 +1111,4 @@ declare class AgentConversation {
|
|
|
1094
1111
|
outcome(outcome: AgentOutcome, resolution?: string): boolean;
|
|
1095
1112
|
}
|
|
1096
1113
|
|
|
1097
|
-
export { AGENT_EVENTS, AGENT_NEG_PHRASES, AGENT_POS_PHRASES, AgentConversation, type AgentConversationOptions, type AgentFeedback, type AgentFeedbackKind, type AgentOutcome, type AgentRole, type Aggregation, type AiConfig, type AiConfigFallback, type AiMessage, type AiTool, type BackoffOptions, CONSENT_KEY, type CaptureExtras, type Clock, type ConditionGroup, Consent, type ConsentCategory, type ConsentDefault, type ConsentMode, type ConsentOptions, type ConsentState, type ConsentStatus, Core, type CoreChange, type CoreConfig, DISTINCT_ID_HEADER, EARLY_ACCESS_EVENT, type EarlyAccessFeature, type EarlyAccessService, type Enrichment, type EvalContext, type EvalReason, type EvalResult, EventQueue, type FilterOp, type FlagCacheStamp, type FlagDefinition, type FlagFallbacks, FlagFreshness, type FlagResult, FlagSnapshot, FlagStream, type FlagStreamOptions, type FlagSyncOptions, type FlagValue, GatedStorage, Groups, Identity, type Intervention, type InterventionHub, InterventionStream, type InterventionStreamOptions, MIGRATION_STAGES, MemoryStickyStore, type MigrationOps, type MigrationReport, type MigrationStage, type OfflineOptions, type OfflineTransport, type PropertyFilter, type Props, type QueueOptions, type RemoteSessionSettings, type RequestContext, type RuntimeBranch, RuntimeCounter, type RuntimeEvents, SESSION_ID_HEADER, type SendOptions, type RequestLike as ServerRequestLike, type SessionInfo, SessionManager, type SessionOptions, type SseMessage, type StickyStore, type Storage, SuperProps, type Transport, type TransportResult, type Variant, type WireEvent, agentCommonProps, agentFeedbackProps, agentMessageProps, agentOutcome, agentOutcomeProps, agentRedact, agentSentiment, agentTextHash, agentTraceId, aiConfig, backoffDelay, bucketingKey, cleanHeader, countKey, countedFilters, enrollmentCapture, enrollmentProp, evaluateAll, evaluateAllSticky, evaluateFlag, eventPropsMatch, fallbackValue, flagEventProps, flagHash, flagResult, interventionHub, isRetryable, mergePatch, migrateRead, migrateWrite, migrationStage, offlineTransport, requestContext, requiresServer, resolveRuntime, resolveRuntimeAll, routeTemplate, servable, sha256Hex, sseParser, startFlagSync, stickyValue, uuidv7, withFallback, withFlagProps };
|
|
1114
|
+
export { AGENT_EVENTS, AGENT_NEG_PHRASES, AGENT_POS_PHRASES, AgentConversation, type AgentConversationOptions, type AgentFeedback, type AgentFeedbackKind, type AgentOutcome, type AgentRole, type Aggregation, type AiConfig, type AiConfigFallback, type AiMessage, type AiTool, type AsyncStickyStore, type BackoffOptions, CONSENT_KEY, type CaptureExtras, type Clock, type ConditionGroup, Consent, type ConsentCategory, type ConsentDefault, type ConsentMode, type ConsentOptions, type ConsentState, type ConsentStatus, Core, type CoreChange, type CoreConfig, DISTINCT_ID_HEADER, EARLY_ACCESS_EVENT, type EarlyAccessFeature, type EarlyAccessService, type Enrichment, type EvalContext, type EvalReason, type EvalResult, EventQueue, type FilterOp, type FlagCacheStamp, type FlagDefinition, type FlagFallbacks, FlagFreshness, type FlagResult, FlagSnapshot, FlagStream, type FlagStreamOptions, type FlagSyncOptions, type FlagValue, GatedStorage, Groups, Identity, type Intervention, type InterventionHub, InterventionStream, type InterventionStreamOptions, MIGRATION_STAGES, MemoryStickyStore, type MigrationOps, type MigrationReport, type MigrationStage, type OfflineOptions, type OfflineTransport, type PropertyFilter, type Props, type QueueOptions, type RemoteSessionSettings, type RequestContext, type RuntimeBranch, RuntimeCounter, type RuntimeEvents, SESSION_ID_HEADER, type SendOptions, type RequestLike as ServerRequestLike, type SessionInfo, SessionManager, type SessionOptions, type SseMessage, type StickyStore, type Storage, SuperProps, type Transport, type TransportResult, type Variant, type WireEvent, agentCommonProps, agentFeedbackProps, agentMessageProps, agentOutcome, agentOutcomeProps, agentRedact, agentSentiment, agentTextHash, agentTraceId, aiConfig, backoffDelay, bucketingKey, cleanHeader, countKey, countedFilters, enrollmentCapture, enrollmentProp, evaluateAll, evaluateAllSticky, evaluateAllStickyAsync, evaluateFlag, eventPropsMatch, fallbackValue, flagEventProps, flagHash, flagResult, interventionHub, isRetryable, matchOp, mergePatch, migrateRead, migrateWrite, migrationStage, offlineTransport, requestContext, requiresServer, resolveRuntime, resolveRuntimeAll, routeTemplate, servable, sha256Hex, sseParser, startFlagSync, stickyValue, uuidv7, withFallback, withFlagProps };
|
package/dist/index.js
CHANGED
|
@@ -254,6 +254,7 @@ var EventQueue = class {
|
|
|
254
254
|
attempt = 0;
|
|
255
255
|
inflight;
|
|
256
256
|
closed = false;
|
|
257
|
+
warned404 = false;
|
|
257
258
|
flushAt;
|
|
258
259
|
intervalMs;
|
|
259
260
|
maxSize;
|
|
@@ -292,15 +293,15 @@ var EventQueue = class {
|
|
|
292
293
|
const body = JSON.stringify({ api_key: this.o.apiKey, batch, sent_at: new Date(sentAt).toISOString() });
|
|
293
294
|
this.o.log?.("send batch", batch.length);
|
|
294
295
|
const p = this.o.transport.send(this.o.url, body, opts.beacon ? { beacon: true } : {}).then(
|
|
295
|
-
(r) => r
|
|
296
|
-
() => 0
|
|
297
|
-
).then((
|
|
296
|
+
(r) => r,
|
|
297
|
+
() => ({ status: 0, retryAfterMs: void 0 })
|
|
298
|
+
).then((r) => this.settle(batch, r.status, !!opts.beacon, r.retryAfterMs)).finally(() => {
|
|
298
299
|
this.inflight = void 0;
|
|
299
300
|
});
|
|
300
301
|
this.inflight = p;
|
|
301
302
|
return p;
|
|
302
303
|
}
|
|
303
|
-
settle(batch, status, beacon) {
|
|
304
|
+
settle(batch, status, beacon, retryAfterMs) {
|
|
304
305
|
if (status >= 200 && status < 300) {
|
|
305
306
|
this.attempt = 0;
|
|
306
307
|
if (this.items.length >= this.flushAt) void this.flush();
|
|
@@ -308,12 +309,18 @@ var EventQueue = class {
|
|
|
308
309
|
return;
|
|
309
310
|
}
|
|
310
311
|
if (!isRetryable(status) || beacon || this.attempt >= this.maxRetries) {
|
|
312
|
+
if (status === 404 && !this.warned404) {
|
|
313
|
+
this.warned404 = true;
|
|
314
|
+
console.warn(
|
|
315
|
+
`[lucida] ${this.o.url} returned 404, so events are being dropped. apiHost is probably wrong: use https://capture.lucidace.com (or your own capture host), not the app host.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
311
318
|
this.o.onDrop?.(batch, `status_${status}`);
|
|
312
319
|
this.attempt = 0;
|
|
313
320
|
return;
|
|
314
321
|
}
|
|
315
322
|
this.items.unshift(...batch);
|
|
316
|
-
const delay = backoffDelay(this.attempt++, this.o.backoff);
|
|
323
|
+
const delay = Math.max(backoffDelay(this.attempt++, this.o.backoff), Math.min(retryAfterMs ?? 0, 6e4));
|
|
317
324
|
this.o.log?.("retry in", delay, "ms");
|
|
318
325
|
this.arm(delay);
|
|
319
326
|
}
|
|
@@ -335,14 +342,24 @@ var KEY2 = "lc_session";
|
|
|
335
342
|
function minutes(v, lo, hi) {
|
|
336
343
|
return typeof v === "number" && Number.isInteger(v) && v >= lo && v <= hi ? v * 6e4 : void 0;
|
|
337
344
|
}
|
|
345
|
+
var MIN_IDLE = 6e4;
|
|
346
|
+
var MAX_IDLE = 10 * 36e5;
|
|
347
|
+
var DAY = 24 * 36e5;
|
|
348
|
+
var UUID7 = /^([0-9a-f]{8})-?([0-9a-f]{4})-7[0-9a-f]{3}-/i;
|
|
349
|
+
function bootstrapOk(id, now) {
|
|
350
|
+
const m = UUID7.exec(id);
|
|
351
|
+
if (!m) return true;
|
|
352
|
+
const ts = parseInt(m[1] + m[2], 16);
|
|
353
|
+
return ts - now <= 6e4 && now - ts <= DAY;
|
|
354
|
+
}
|
|
338
355
|
var SessionManager = class {
|
|
339
356
|
constructor(storage, o = {}) {
|
|
340
357
|
this.storage = storage;
|
|
341
358
|
this.o = o;
|
|
342
|
-
this.idleMs = o.idleMs ?? 30 * 6e4;
|
|
359
|
+
this.idleMs = Math.min(MAX_IDLE, Math.max(MIN_IDLE, o.idleMs ?? 30 * 6e4));
|
|
343
360
|
this.maxMs = o.maxMs ?? 24 * 36e5;
|
|
344
361
|
this.newOn = o.newOnEvents ?? [];
|
|
345
|
-
this.clock = o.clock ?? Date;
|
|
362
|
+
this.clock = o.clock ?? { now: () => Date.now() };
|
|
346
363
|
this.bootstrapId = o.bootstrapId;
|
|
347
364
|
}
|
|
348
365
|
storage;
|
|
@@ -358,7 +375,7 @@ var SessionManager = class {
|
|
|
358
375
|
* out-of-range remote values leave the current setting.
|
|
359
376
|
*/
|
|
360
377
|
configure(r) {
|
|
361
|
-
if (this.o.idleMs === void 0) this.idleMs = minutes(r.session_idle_minutes, 1,
|
|
378
|
+
if (this.o.idleMs === void 0) this.idleMs = minutes(r.session_idle_minutes, 1, 600) ?? this.idleMs;
|
|
362
379
|
if (this.o.maxMs === void 0) this.maxMs = minutes(r.session_max_minutes, 5, 10080) ?? this.maxMs;
|
|
363
380
|
if (this.o.newOnEvents === void 0 && Array.isArray(r.session_new_on_events)) {
|
|
364
381
|
this.newOn = r.session_new_on_events.filter((e) => typeof e === "string" && e !== "");
|
|
@@ -378,8 +395,8 @@ var SessionManager = class {
|
|
|
378
395
|
let s = this.read();
|
|
379
396
|
const campaignChanged = !!(s?.campaign && campaignKey && s.campaign !== campaignKey);
|
|
380
397
|
const ruleEvent = event !== void 0 && this.newOn.includes(event);
|
|
381
|
-
const isNew = !s || now - s.last > this.idleMs || now - s.start > this.maxMs || campaignChanged || ruleEvent;
|
|
382
|
-
if (isNew || !s) s = { id: !s && this.bootstrapId
|
|
398
|
+
const isNew = !s || Math.abs(now - s.last) > this.idleMs || Math.abs(now - s.start) > this.maxMs || campaignChanged || ruleEvent;
|
|
399
|
+
if (isNew || !s) s = { id: !s && this.bootstrapId && bootstrapOk(this.bootstrapId, now) ? this.bootstrapId : uuidv7(now), start: now, last: now };
|
|
383
400
|
this.bootstrapId = void 0;
|
|
384
401
|
s.last = now;
|
|
385
402
|
if (campaignKey) s.campaign = campaignKey;
|
|
@@ -469,7 +486,7 @@ function validTime(t, now) {
|
|
|
469
486
|
var Core = class {
|
|
470
487
|
constructor(c) {
|
|
471
488
|
this.c = c;
|
|
472
|
-
this.clock = c.clock ?? Date;
|
|
489
|
+
this.clock = c.clock ?? { now: () => Date.now() };
|
|
473
490
|
this.consent = new Consent({ ...c.consent, storage: c.consentStorage ?? c.storage });
|
|
474
491
|
this.gated = new GatedStorage(c.storage, () => this.consent.has("analytics"));
|
|
475
492
|
this.storage = this.gated;
|
|
@@ -1800,23 +1817,27 @@ function evaluateAll(flags, c) {
|
|
|
1800
1817
|
for (const f of flags) visit(f.key);
|
|
1801
1818
|
return out;
|
|
1802
1819
|
}
|
|
1803
|
-
var MAX_BRANCHED_DEPS = 4;
|
|
1804
1820
|
function evaluateDependent(f, c, resolved) {
|
|
1805
|
-
|
|
1806
|
-
for (const d of deps(f)) if (!pending.includes(d) && resolved[d]?.runtime?.length && pending.length < MAX_BRANCHED_DEPS) pending.push(d);
|
|
1807
|
-
return evalOver(f, c, resolved, pending);
|
|
1821
|
+
return evalOver(f, c, resolved, [], /* @__PURE__ */ new Set());
|
|
1808
1822
|
}
|
|
1809
|
-
function evalOver(f, c, resolved, pending) {
|
|
1823
|
+
function evalOver(f, c, resolved, pending, done) {
|
|
1810
1824
|
const [dep, ...rest] = pending;
|
|
1811
|
-
if (dep === void 0)
|
|
1812
|
-
|
|
1825
|
+
if (dep === void 0) {
|
|
1826
|
+
const reads = /* @__PURE__ */ new Set();
|
|
1827
|
+
const tracked = new Proxy(resolved, { get: (t, k) => (reads.add(String(k)), t[k]) });
|
|
1828
|
+
const r = evaluateFlag(f, c, tracked);
|
|
1829
|
+
const more = [];
|
|
1830
|
+
for (const d of deps(f)) if (reads.has(d) && !done.has(d) && !more.includes(d) && resolved[d]?.runtime?.length) more.push(d);
|
|
1831
|
+
return more.length ? evalOver(f, c, resolved, more, /* @__PURE__ */ new Set([...done, ...more])) : r;
|
|
1832
|
+
}
|
|
1833
|
+
const base = evalOver(f, c, resolved, rest, done);
|
|
1813
1834
|
const cur = resolved[dep];
|
|
1814
1835
|
const tried = /* @__PURE__ */ new Set([cur.value]);
|
|
1815
1836
|
const branches = [];
|
|
1816
1837
|
for (const alt of outcomes(cur)) {
|
|
1817
1838
|
if (tried.has(alt.value)) continue;
|
|
1818
1839
|
tried.add(alt.value);
|
|
1819
|
-
const r = evalOver(f, c, { ...resolved, [dep]: alt }, rest);
|
|
1840
|
+
const r = evalOver(f, c, { ...resolved, [dep]: alt }, rest, done);
|
|
1820
1841
|
if (same(r, base)) continue;
|
|
1821
1842
|
branches.push({ filters: [{ type: "flag", key: dep, op: "eq", value: alt.value }], result: r });
|
|
1822
1843
|
}
|
|
@@ -1964,6 +1985,31 @@ function evaluateAllSticky(flags, c, store) {
|
|
|
1964
1985
|
}
|
|
1965
1986
|
return out;
|
|
1966
1987
|
}
|
|
1988
|
+
async function evaluateAllStickyAsync(flags, c, store) {
|
|
1989
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const f of flags) {
|
|
1991
|
+
const k = f.sticky ? bucketingKey(f, c) : void 0;
|
|
1992
|
+
if (k) keys.add(k);
|
|
1993
|
+
}
|
|
1994
|
+
const snapshot = new MemoryStickyStore();
|
|
1995
|
+
const writes = [];
|
|
1996
|
+
await Promise.all(
|
|
1997
|
+
[...keys].map(async (k) => {
|
|
1998
|
+
for (const [flag, v] of Object.entries(await store.get(k).catch(() => void 0) ?? {})) snapshot.set(k, flag, v);
|
|
1999
|
+
})
|
|
2000
|
+
);
|
|
2001
|
+
const queued = {
|
|
2002
|
+
get: (k) => snapshot.get(k),
|
|
2003
|
+
set(k, flag, v) {
|
|
2004
|
+
if (snapshot.get(k)?.[flag] === v) return;
|
|
2005
|
+
snapshot.set(k, flag, v);
|
|
2006
|
+
writes.push(store.set(k, flag, v).catch(() => void 0));
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
const out = evaluateAllSticky(flags, c, queued);
|
|
2010
|
+
await Promise.all(writes);
|
|
2011
|
+
return out;
|
|
2012
|
+
}
|
|
1967
2013
|
|
|
1968
2014
|
// src/migration.ts
|
|
1969
2015
|
var MIGRATION_STAGES = ["off", "dualwrite", "shadow", "live", "rampdown", "complete"];
|
|
@@ -2379,6 +2425,7 @@ export {
|
|
|
2379
2425
|
enrollmentProp,
|
|
2380
2426
|
evaluateAll,
|
|
2381
2427
|
evaluateAllSticky,
|
|
2428
|
+
evaluateAllStickyAsync,
|
|
2382
2429
|
evaluateFlag,
|
|
2383
2430
|
eventPropsMatch,
|
|
2384
2431
|
fallbackValue,
|
|
@@ -2387,6 +2434,7 @@ export {
|
|
|
2387
2434
|
flagResult,
|
|
2388
2435
|
interventionHub,
|
|
2389
2436
|
isRetryable,
|
|
2437
|
+
matchOp,
|
|
2390
2438
|
mergePatch,
|
|
2391
2439
|
migrateRead,
|
|
2392
2440
|
migrateWrite,
|