@cross-deck/web 1.7.0 → 1.9.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/dist/vue.cjs CHANGED
@@ -36,6 +36,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
36
36
  this.requestId = payload.requestId;
37
37
  this.status = payload.status;
38
38
  this.retryAfterMs = payload.retryAfterMs;
39
+ this.minVersion = payload.minVersion;
40
+ this.surface = payload.surface;
39
41
  Object.setPrototypeOf(this, _CrossdeckError.prototype);
40
42
  }
41
43
  };
@@ -56,7 +58,10 @@ async function crossdeckErrorFromResponse(res) {
56
58
  message: envelope.message ?? `HTTP ${res.status}`,
57
59
  requestId: envelope.request_id ?? requestId,
58
60
  status: res.status,
59
- retryAfterMs
61
+ retryAfterMs,
62
+ // PARK metadata, present only on a 426 / sdk_version_unsupported body.
63
+ minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
64
+ surface: typeof envelope.surface === "string" ? envelope.surface : void 0
60
65
  });
61
66
  }
62
67
  return new CrossdeckError({
@@ -92,7 +97,7 @@ function typeMapForStatus(status) {
92
97
  }
93
98
 
94
99
  // src/_version.ts
95
- var SDK_VERSION = "1.7.0";
100
+ var SDK_VERSION = "1.9.0";
96
101
  var SDK_NAME = "@cross-deck/web";
97
102
 
98
103
  // src/http.ts
@@ -932,6 +937,18 @@ var EventQueue = class {
932
937
  this.cancelTimer = null;
933
938
  this.firstFlushFired = false;
934
939
  this.nextRetryAt = null;
940
+ /**
941
+ * PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
942
+ * queue stops flushing — the server has told us our wire dialect is too
943
+ * old, and retrying the same payload only burns the user's battery and
944
+ * bandwidth and drips pointless rejects into the server logs until the
945
+ * app ships an upgraded SDK. The held events stay durable (persisted)
946
+ * and are delivered by the next boot's rehydrate, post-upgrade. Parked
947
+ * is per-instance: a fresh boot (the upgraded build) starts unparked.
948
+ */
949
+ this.parked = false;
950
+ /** One developer-facing console warning per instance — never per-event spam. */
951
+ this.parkWarned = false;
935
952
  this.retry = new RetryPolicy(cfg.retry ?? {});
936
953
  this.persistent = cfg.persistentStore ?? null;
937
954
  if (this.persistent) {
@@ -987,6 +1004,7 @@ var EventQueue = class {
987
1004
  * flushes (pagehide / visibilitychange→hidden / beforeunload).
988
1005
  */
989
1006
  async flush(options = {}) {
1007
+ if (this.parked) return null;
990
1008
  let batch;
991
1009
  let batchId;
992
1010
  if (this.pendingBatch !== null && this.pendingBatchId !== null) {
@@ -1038,6 +1056,29 @@ var EventQueue = class {
1038
1056
  } catch (err) {
1039
1057
  const message = err instanceof Error ? err.message : String(err);
1040
1058
  this.lastError = message;
1059
+ if (isVersionRejected(err)) {
1060
+ this.parked = true;
1061
+ this.buffer = [...batch, ...this.buffer];
1062
+ if (this.buffer.length > HARD_BUFFER_CAP) {
1063
+ const overflow = this.buffer.length - HARD_BUFFER_CAP;
1064
+ this.buffer.splice(0, overflow);
1065
+ this.dropped += overflow;
1066
+ }
1067
+ this.pendingBatch = null;
1068
+ this.pendingBatchId = null;
1069
+ this.inFlight -= batch.length;
1070
+ this.persistAll();
1071
+ this.cfg.onBufferChange?.(this.buffer.length);
1072
+ const minVersion = versionRejectionFloor(err);
1073
+ if (!this.parkWarned) {
1074
+ this.parkWarned = true;
1075
+ console.warn(
1076
+ `[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Your events are PARKED on-device (held, not lost) and will deliver automatically once you update @cross-deck/web${minVersion ? ` to >= ${minVersion}` : ""} and redeploy.`
1077
+ );
1078
+ }
1079
+ this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
1080
+ return null;
1081
+ }
1041
1082
  if (isPermanent4xx(err)) {
1042
1083
  const droppedCount = batch.length;
1043
1084
  this.pendingBatch = null;
@@ -1156,8 +1197,22 @@ function isPermanent4xx(err) {
1156
1197
  if (typeof status !== "number" || !Number.isFinite(status)) return false;
1157
1198
  if (status < 400 || status >= 500) return false;
1158
1199
  if (status === 408 || status === 429) return false;
1200
+ if (status === 426) return false;
1159
1201
  return true;
1160
1202
  }
1203
+ function isVersionRejected(err) {
1204
+ if (!err || typeof err !== "object") return false;
1205
+ if (err.status === 426) return true;
1206
+ return err.code === "sdk_version_unsupported";
1207
+ }
1208
+ function versionRejectionFloor(err) {
1209
+ const v = err?.minVersion;
1210
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1211
+ }
1212
+ function versionRejectionSurface(err) {
1213
+ const v = err?.surface;
1214
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1215
+ }
1161
1216
  function defaultScheduler(fn, ms) {
1162
1217
  const id = setTimeout(fn, ms);
1163
1218
  if (typeof id.unref === "function") {
@@ -1343,6 +1398,16 @@ function hasDocument() {
1343
1398
  return typeof globalThis.document !== "undefined";
1344
1399
  }
1345
1400
 
1401
+ // src/read-cost-bridge.ts
1402
+ var BUCKETS_BRIDGE_KEY = "__crossdeckBucketsBridge__";
1403
+ function bridgeReadCost(ctx) {
1404
+ try {
1405
+ const setter = globalThis[BUCKETS_BRIDGE_KEY];
1406
+ if (typeof setter === "function") setter(ctx);
1407
+ } catch {
1408
+ }
1409
+ }
1410
+
1346
1411
  // src/device-info.ts
1347
1412
  function isBrowser() {
1348
1413
  return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined" && typeof globalThis.navigator !== "undefined";
@@ -2898,6 +2963,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
2898
2963
  description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
2899
2964
  resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
2900
2965
  retryable: false
2966
+ },
2967
+ {
2968
+ code: "sdk_version_unsupported",
2969
+ type: "version_error",
2970
+ description: "HTTP 426 \u2014 your installed SDK sends an event format the server no longer accepts. The data is good; only the wire dialect is too old. The SDK PARKS automatically: events are held on-device (paused, not lost) and deliver on the next launch after you upgrade.",
2971
+ resolution: "Update @cross-deck/web to at least the version in error.minVersion and redeploy \u2014 the parked queue backfills automatically. See https://cross-deck.com/docs/sdk-event-durability/.",
2972
+ retryable: false
2901
2973
  }
2902
2974
  ]);
2903
2975
  function getErrorCode(code) {
@@ -3443,7 +3515,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
3443
3515
  "google_not_supported",
3444
3516
  "stripe_not_supported",
3445
3517
  "missing_required_param",
3446
- "invalid_param_value"
3518
+ "invalid_param_value",
3519
+ "sdk_version_unsupported"
3447
3520
  ]);
3448
3521
  var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3449
3522
  contractId: "sdk-error-codes-catalogue",
@@ -4600,6 +4673,13 @@ var CrossdeckClient = class {
4600
4673
  headline,
4601
4674
  { ...info }
4602
4675
  );
4676
+ },
4677
+ onParked: (info) => {
4678
+ debug.emit(
4679
+ "sdk.parked",
4680
+ `[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held on-device (paused, not lost); update @cross-deck/web${info.minVersion ? ` to >= ${info.minVersion}` : ""} to resume.`,
4681
+ { ...info }
4682
+ );
4603
4683
  }
4604
4684
  });
4605
4685
  const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
@@ -4817,6 +4897,7 @@ var CrossdeckClient = class {
4817
4897
  message: "identify(userId) requires a non-empty userId."
4818
4898
  });
4819
4899
  }
4900
+ bridgeReadCost({ actor: userId });
4820
4901
  if (!s.consent.analytics) {
4821
4902
  s.debug.emit(
4822
4903
  "sdk.consent_denied",
@@ -5469,6 +5550,7 @@ var CrossdeckClient = class {
5469
5550
  }
5470
5551
  this.state.autoTracker?.uninstall();
5471
5552
  this.state.identity.reset();
5553
+ bridgeReadCost({ actor: void 0 });
5472
5554
  this.state.entitlements.clearAll();
5473
5555
  this.state.events.reset();
5474
5556
  this.state.superProps.clear();
@@ -5559,6 +5641,23 @@ var CrossdeckClient = class {
5559
5641
  const s = this.requireStarted();
5560
5642
  return s.identity.crossdeckCustomerId ?? s.developerUserId ?? s.identity.anonymousId;
5561
5643
  }
5644
+ /**
5645
+ * The device-scoped anonymous id the SDK minted on first boot and persists
5646
+ * across launches (stable until reset()). Public accessor so a server-to-
5647
+ * server flow or a block/suspension gate can pass the device identity to
5648
+ * POST /v1/resolve without reaching into private storage.
5649
+ *
5650
+ * Returns `null` BEFORE init() — there is no anon id yet, and a gate that
5651
+ * fires during early app boot should get a clean falsy, not a throw. (This is
5652
+ * deliberately softer than getCheckoutReference(), which requires init.)
5653
+ *
5654
+ * Note: /v1/resolve also accepts a VERIFIED identity (userId + idToken)
5655
+ * without an anonymousId, and that path is higher-trust — prefer it where the
5656
+ * user is authenticated.
5657
+ */
5658
+ getAnonymousId() {
5659
+ return this.state ? this.state.identity.anonymousId : null;
5660
+ }
5562
5661
  // ---------- private helpers ----------
5563
5662
  requireStarted() {
5564
5663
  if (!this.state) {