@cross-deck/node 1.6.0 → 1.7.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/CHANGELOG.md +35 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/{crossdeck-server-C1Ue0rv4.d.mts → crossdeck-server-DYawt4eT.d.mts} +11 -1
- package/dist/{crossdeck-server-C1Ue0rv4.d.ts → crossdeck-server-DYawt4eT.d.ts} +11 -1
- package/dist/index.cjs +78 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.mjs +78 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/contracts.json +0 -557
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to `@cross-deck/node` will be documented here. The
|
|
|
4
4
|
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.7.0] — 2026-06-11
|
|
8
|
+
|
|
9
|
+
**PARK on version-rejection — events are held, never dropped.** A third
|
|
10
|
+
event-queue outcome for the day the server stops accepting an outdated event
|
|
11
|
+
format. Purely additive; no public API change.
|
|
12
|
+
|
|
13
|
+
**Added:**
|
|
14
|
+
|
|
15
|
+
- **PARK (HTTP `426` / `sdk_version_unsupported`).** A version-rejection is now
|
|
16
|
+
recognised as its own outcome — distinct from retry (transient) and drop
|
|
17
|
+
(invalid): the data is good, only the wire dialect is stale. The queue
|
|
18
|
+
**holds** the events (folded to the buffer front, FIFO-capped at 1000),
|
|
19
|
+
**hushes** (stops flushing a known-too-old payload), **signals** once (one
|
|
20
|
+
`console.warn` + a typed `sdk.parked` debug event), and delivers on restart
|
|
21
|
+
after you upgrade. Node's queue is in-memory, so a process restart *before*
|
|
22
|
+
upgrade clears the held events — an opt-in disk-backed queue is on the
|
|
23
|
+
roadmap; the messaging says exactly this, never more.
|
|
24
|
+
- **`sdk_version_unsupported`** added to the error-codes catalogue with
|
|
25
|
+
remediation, and `version_error` to `CrossdeckErrorType`. `CrossdeckError`
|
|
26
|
+
carries `minVersion` / `surface` from the 426 body. New `onParked` callback.
|
|
27
|
+
|
|
28
|
+
**Fixed (no public API change):**
|
|
29
|
+
|
|
30
|
+
- The empty-input contract is now codified cross-SDK as
|
|
31
|
+
`invalid-input-rejected-natively`: `track("")` / `aliasIdentity` with a
|
|
32
|
+
missing `userId` reject at the call site by throwing a typed `CrossdeckError`
|
|
33
|
+
(`missing_event_name` / `missing_user_id`) and never reach the wire — the
|
|
34
|
+
Node/JS idiom of the invariant *"invalid input never crashes the app."* No
|
|
35
|
+
behaviour change; the guarantee is now documented and bundled.
|
|
36
|
+
- Standalone-build fix: the `contract-failed` schema-lock test now reads the
|
|
37
|
+
bundled contract (`_contracts-bundled.ts`) instead of the monorepo
|
|
38
|
+
`contracts/` path, so the published-mirror release build no longer fails.
|
|
39
|
+
|
|
40
|
+
See https://cross-deck.com/docs/sdk-event-durability/ for the durability contract.
|
|
41
|
+
|
|
7
42
|
## [1.6.0] — 2026-06-10
|
|
8
43
|
|
|
9
44
|
Event Envelope v1 conformance — server-enforced contract (spec
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
|
|
3
|
-
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
3
|
+
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "version_error" | "internal_error" | "network_error" | "configuration_error";
|
|
4
4
|
interface CrossdeckErrorPayload {
|
|
5
5
|
type: CrossdeckErrorType;
|
|
6
6
|
/**
|
|
@@ -19,6 +19,14 @@ interface CrossdeckErrorPayload {
|
|
|
19
19
|
requestId?: string;
|
|
20
20
|
status?: number;
|
|
21
21
|
retryAfterMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Required SDK version floor — populated only on a `426 Upgrade Required`
|
|
24
|
+
* / `sdk_version_unsupported` response, so the queue's PARK message names
|
|
25
|
+
* the exact version to update to.
|
|
26
|
+
*/
|
|
27
|
+
minVersion?: string;
|
|
28
|
+
/** SDK surface the rejection applies to (web/node/swift/…), on a 426. */
|
|
29
|
+
surface?: string;
|
|
22
30
|
}
|
|
23
31
|
declare class CrossdeckError extends Error {
|
|
24
32
|
readonly type: CrossdeckErrorType;
|
|
@@ -26,6 +34,8 @@ declare class CrossdeckError extends Error {
|
|
|
26
34
|
readonly requestId?: string;
|
|
27
35
|
readonly status?: number;
|
|
28
36
|
readonly retryAfterMs?: number;
|
|
37
|
+
readonly minVersion?: string;
|
|
38
|
+
readonly surface?: string;
|
|
29
39
|
constructor(payload: CrossdeckErrorPayload);
|
|
30
40
|
/**
|
|
31
41
|
* JSON representation suitable for structured loggers. Without this,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
|
|
3
|
-
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
3
|
+
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "version_error" | "internal_error" | "network_error" | "configuration_error";
|
|
4
4
|
interface CrossdeckErrorPayload {
|
|
5
5
|
type: CrossdeckErrorType;
|
|
6
6
|
/**
|
|
@@ -19,6 +19,14 @@ interface CrossdeckErrorPayload {
|
|
|
19
19
|
requestId?: string;
|
|
20
20
|
status?: number;
|
|
21
21
|
retryAfterMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Required SDK version floor — populated only on a `426 Upgrade Required`
|
|
24
|
+
* / `sdk_version_unsupported` response, so the queue's PARK message names
|
|
25
|
+
* the exact version to update to.
|
|
26
|
+
*/
|
|
27
|
+
minVersion?: string;
|
|
28
|
+
/** SDK surface the rejection applies to (web/node/swift/…), on a 426. */
|
|
29
|
+
surface?: string;
|
|
22
30
|
}
|
|
23
31
|
declare class CrossdeckError extends Error {
|
|
24
32
|
readonly type: CrossdeckErrorType;
|
|
@@ -26,6 +34,8 @@ declare class CrossdeckError extends Error {
|
|
|
26
34
|
readonly requestId?: string;
|
|
27
35
|
readonly status?: number;
|
|
28
36
|
readonly retryAfterMs?: number;
|
|
37
|
+
readonly minVersion?: string;
|
|
38
|
+
readonly surface?: string;
|
|
29
39
|
constructor(payload: CrossdeckErrorPayload);
|
|
30
40
|
/**
|
|
31
41
|
* JSON representation suitable for structured loggers. Without this,
|
package/dist/index.cjs
CHANGED
|
@@ -66,6 +66,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
66
66
|
requestId;
|
|
67
67
|
status;
|
|
68
68
|
retryAfterMs;
|
|
69
|
+
minVersion;
|
|
70
|
+
surface;
|
|
69
71
|
constructor(payload) {
|
|
70
72
|
super(payload.message);
|
|
71
73
|
this.name = "CrossdeckError";
|
|
@@ -74,6 +76,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
74
76
|
this.requestId = payload.requestId;
|
|
75
77
|
this.status = payload.status;
|
|
76
78
|
this.retryAfterMs = payload.retryAfterMs;
|
|
79
|
+
this.minVersion = payload.minVersion;
|
|
80
|
+
this.surface = payload.surface;
|
|
77
81
|
Object.setPrototypeOf(this, _CrossdeckError.prototype);
|
|
78
82
|
}
|
|
79
83
|
/**
|
|
@@ -95,6 +99,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
|
|
|
95
99
|
requestId: this.requestId,
|
|
96
100
|
status: this.status,
|
|
97
101
|
retryAfterMs: this.retryAfterMs,
|
|
102
|
+
minVersion: this.minVersion,
|
|
103
|
+
surface: this.surface,
|
|
98
104
|
stack: this.stack
|
|
99
105
|
};
|
|
100
106
|
}
|
|
@@ -185,7 +191,10 @@ async function crossdeckErrorFromResponse(res) {
|
|
|
185
191
|
message: envelope.message ?? `HTTP ${res.status}`,
|
|
186
192
|
requestId: envelope.request_id ?? requestId,
|
|
187
193
|
status: res.status,
|
|
188
|
-
retryAfterMs
|
|
194
|
+
retryAfterMs,
|
|
195
|
+
// PARK metadata, present only on a 426 / sdk_version_unsupported body.
|
|
196
|
+
minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
|
|
197
|
+
surface: typeof envelope.surface === "string" ? envelope.surface : void 0
|
|
189
198
|
});
|
|
190
199
|
}
|
|
191
200
|
return makeCrossdeckError({
|
|
@@ -378,7 +387,7 @@ function byteLength(s) {
|
|
|
378
387
|
var https = __toESM(require("https"));
|
|
379
388
|
|
|
380
389
|
// src/_version.ts
|
|
381
|
-
var SDK_VERSION = "1.
|
|
390
|
+
var SDK_VERSION = "1.7.0";
|
|
382
391
|
var SDK_NAME = "@cross-deck/node";
|
|
383
392
|
|
|
384
393
|
// src/_diagnostic-telemetry.ts
|
|
@@ -398,7 +407,12 @@ var DIAGNOSTIC_TELEMETRY_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
|
398
407
|
"run_id",
|
|
399
408
|
"test_file",
|
|
400
409
|
"test_name",
|
|
401
|
-
"device_class"
|
|
410
|
+
"device_class",
|
|
411
|
+
// verification_phase — categorical `boot` / `hot_path` bucket set by the
|
|
412
|
+
// runtime contract verifier layer. Declared in the shared diagnostics
|
|
413
|
+
// contract (contracts/diagnostics/contract-failed-payload-schema-lock.json)
|
|
414
|
+
// and carried by web/swift; node had drifted by omitting it.
|
|
415
|
+
"verification_phase"
|
|
402
416
|
]);
|
|
403
417
|
function filterDiagnosticPayload(payload) {
|
|
404
418
|
const filtered = {};
|
|
@@ -891,6 +905,16 @@ var EventQueue = class {
|
|
|
891
905
|
cancelTimer = null;
|
|
892
906
|
firstFlushFired = false;
|
|
893
907
|
nextRetryAt = null;
|
|
908
|
+
/**
|
|
909
|
+
* PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
|
|
910
|
+
* queue stops flushing — retrying a known-too-old payload only wastes
|
|
911
|
+
* bandwidth and drips pointless rejects into the server logs until the
|
|
912
|
+
* process restarts on an upgraded SDK. In-memory: a fresh process starts
|
|
913
|
+
* unparked, retries once, and either delivers (upgraded) or re-parks.
|
|
914
|
+
*/
|
|
915
|
+
parked = false;
|
|
916
|
+
/** One developer-facing console warning per process — never per-event spam. */
|
|
917
|
+
parkWarned = false;
|
|
894
918
|
retry;
|
|
895
919
|
/**
|
|
896
920
|
* Stable Idempotency-Key for the current in-flight batch. Minted
|
|
@@ -943,6 +967,7 @@ var EventQueue = class {
|
|
|
943
967
|
* this one settles. Strict ordering preserved.
|
|
944
968
|
*/
|
|
945
969
|
async flush() {
|
|
970
|
+
if (this.parked) return null;
|
|
946
971
|
let batch;
|
|
947
972
|
let batchId;
|
|
948
973
|
if (this.pendingBatch !== null && this.pendingBatchId !== null) {
|
|
@@ -990,6 +1015,28 @@ var EventQueue = class {
|
|
|
990
1015
|
} catch (err) {
|
|
991
1016
|
const message = err instanceof Error ? err.message : String(err);
|
|
992
1017
|
this.lastError = message;
|
|
1018
|
+
if (isVersionRejected(err)) {
|
|
1019
|
+
this.parked = true;
|
|
1020
|
+
this.buffer = [...batch, ...this.buffer];
|
|
1021
|
+
if (this.buffer.length > HARD_BUFFER_CAP) {
|
|
1022
|
+
const overflow = this.buffer.length - HARD_BUFFER_CAP;
|
|
1023
|
+
this.buffer.splice(0, overflow);
|
|
1024
|
+
this.dropped += overflow;
|
|
1025
|
+
}
|
|
1026
|
+
this.pendingBatch = null;
|
|
1027
|
+
this.pendingBatchId = null;
|
|
1028
|
+
this.inFlight -= batch.length;
|
|
1029
|
+
this.cfg.onBufferChange?.(this.buffer.length);
|
|
1030
|
+
const minVersion = versionRejectionFloor(err);
|
|
1031
|
+
if (!this.parkWarned) {
|
|
1032
|
+
this.parkWarned = true;
|
|
1033
|
+
console.warn(
|
|
1034
|
+
`[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Events are PARKED in memory (held, not lost across this process) and will deliver once you update @cross-deck/node${minVersion ? ` to >= ${minVersion}` : ""} and restart. A restart before upgrade clears them \u2014 configure a disk queue for cross-restart durability.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
993
1040
|
if (isPermanent4xx(err)) {
|
|
994
1041
|
const droppedCount = batch.length;
|
|
995
1042
|
this.pendingBatch = null;
|
|
@@ -1088,8 +1135,22 @@ function isPermanent4xx(err) {
|
|
|
1088
1135
|
if (typeof status !== "number" || !Number.isFinite(status)) return false;
|
|
1089
1136
|
if (status < 400 || status >= 500) return false;
|
|
1090
1137
|
if (status === 408 || status === 429) return false;
|
|
1138
|
+
if (status === 426) return false;
|
|
1091
1139
|
return true;
|
|
1092
1140
|
}
|
|
1141
|
+
function isVersionRejected(err) {
|
|
1142
|
+
if (!err || typeof err !== "object") return false;
|
|
1143
|
+
if (err.status === 426) return true;
|
|
1144
|
+
return err.code === "sdk_version_unsupported";
|
|
1145
|
+
}
|
|
1146
|
+
function versionRejectionFloor(err) {
|
|
1147
|
+
const v = err?.minVersion;
|
|
1148
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1149
|
+
}
|
|
1150
|
+
function versionRejectionSurface(err) {
|
|
1151
|
+
const v = err?.surface;
|
|
1152
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1153
|
+
}
|
|
1093
1154
|
function defaultScheduler(fn, ms) {
|
|
1094
1155
|
const id = setTimeout(fn, ms);
|
|
1095
1156
|
if (typeof id.unref === "function") {
|
|
@@ -2793,6 +2854,13 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
|
|
|
2793
2854
|
error: info.lastError
|
|
2794
2855
|
});
|
|
2795
2856
|
},
|
|
2857
|
+
onParked: (info) => {
|
|
2858
|
+
this.debug.emit(
|
|
2859
|
+
"sdk.parked",
|
|
2860
|
+
`[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held (paused, not lost); update @cross-deck/node${info.minVersion ? ` to >= ${info.minVersion}` : ""} and restart to resume.`,
|
|
2861
|
+
{ ...info }
|
|
2862
|
+
);
|
|
2863
|
+
},
|
|
2796
2864
|
onFirstFlushSuccess: () => {
|
|
2797
2865
|
this.debug.emit("sdk.first_event_sent", "First batch landed.");
|
|
2798
2866
|
}
|
|
@@ -4367,6 +4435,13 @@ var _CROSSDECK_ERROR_CODES = Object.freeze([
|
|
|
4367
4435
|
description: "A field is present but the value failed validation.",
|
|
4368
4436
|
resolution: "Read error.message for the field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
|
|
4369
4437
|
retryable: false
|
|
4438
|
+
},
|
|
4439
|
+
{
|
|
4440
|
+
code: "sdk_version_unsupported",
|
|
4441
|
+
type: "version_error",
|
|
4442
|
+
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 in memory and deliver once you upgrade and restart.",
|
|
4443
|
+
resolution: "Update @cross-deck/node to at least the version in error.minVersion and restart \u2014 the held queue backfills. See https://cross-deck.com/docs/sdk-event-durability/.",
|
|
4444
|
+
retryable: false
|
|
4370
4445
|
}
|
|
4371
4446
|
]);
|
|
4372
4447
|
function isCrossdeckErrorCode(code) {
|