@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 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
- * shortcut: synchronous port (fits memory and localStorage); preload async stores before evaluating.
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.status,
296
- () => 0
297
- ).then((status) => this.settle(batch, status, !!opts.beacon)).finally(() => {
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, 1440) ?? this.idleMs;
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 || uuidv7(now), start: now, last: now };
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
- const pending = [];
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) return evaluateFlag(f, c, resolved);
1812
- const base = evalOver(f, c, resolved, rest);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucida-analytics/sdk-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",