node-avo-inspector 1.2.0 → 2.0.0-beta.2

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +270 -4
  3. package/dist/AvoBatchQueue.d.ts +73 -0
  4. package/dist/AvoBatchQueue.js +239 -0
  5. package/dist/AvoDeduplicator.d.ts +15 -6
  6. package/dist/AvoDeduplicator.js +46 -24
  7. package/dist/AvoInspector.d.ts +136 -23
  8. package/dist/AvoInspector.js +777 -109
  9. package/dist/AvoInspectorVersion.d.ts +3 -0
  10. package/dist/AvoInspectorVersion.js +10 -0
  11. package/dist/AvoLog.d.ts +52 -0
  12. package/dist/AvoLog.js +246 -0
  13. package/dist/AvoNetworkCallsHandler.d.ts +55 -8
  14. package/dist/AvoNetworkCallsHandler.js +266 -92
  15. package/dist/AvoSchemaParser.d.ts +6 -0
  16. package/dist/AvoSchemaParser.js +138 -33
  17. package/dist/AvoStreamId.d.ts +2 -1
  18. package/dist/AvoStreamId.js +21 -2
  19. package/dist/eventSpec/AvoEventSpecCache.d.ts +28 -5
  20. package/dist/eventSpec/AvoEventSpecCache.js +66 -11
  21. package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +50 -18
  22. package/dist/eventSpec/AvoEventSpecFetcher.d.ts +29 -6
  23. package/dist/eventSpec/AvoEventSpecFetcher.js +279 -69
  24. package/dist/eventSpec/EventValidator.d.ts +16 -10
  25. package/dist/eventSpec/EventValidator.js +351 -38
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +4 -1
  28. package/dist/utils.d.ts +29 -1
  29. package/dist/utils.js +86 -10
  30. package/package.json +18 -1
  31. package/.claude/scheduled_tasks.lock +0 -1
  32. package/.claude/settings.local.json +0 -11
  33. package/.claude/worktrees/release-1.1.1/LICENSE +0 -21
  34. package/.claude/worktrees/release-1.1.1/README.md +0 -69
  35. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/index.js +0 -30
  36. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/package.json +0 -15
  37. package/.claude/worktrees/release-1.1.1/examples/node-inspector-js/yarn.lock +0 -6
  38. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/dist/index.js +0 -34
  39. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/index.ts +0 -30
  40. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/package.json +0 -18
  41. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/tsconfig.json +0 -15
  42. package/.claude/worktrees/release-1.1.1/examples/node-inspector-ts/yarn.lock +0 -11
  43. package/.claude/worktrees/release-1.1.1/package-lock.json +0 -4039
  44. package/.claude/worktrees/release-1.1.1/package.json +0 -48
  45. package/.claude/worktrees/release-1.1.1/reason/AvoInspector.rei +0 -11
  46. package/.claude/worktrees/release-1.1.1/reason/NodeAvoInspector.re +0 -27
  47. package/.claude/worktrees/release-1.1.1/src/AvoDeduplicator.ts +0 -146
  48. package/.claude/worktrees/release-1.1.1/src/AvoEncryption.ts +0 -90
  49. package/.claude/worktrees/release-1.1.1/src/AvoGuid.ts +0 -9
  50. package/.claude/worktrees/release-1.1.1/src/AvoInspector.ts +0 -383
  51. package/.claude/worktrees/release-1.1.1/src/AvoInspectorEnv.ts +0 -8
  52. package/.claude/worktrees/release-1.1.1/src/AvoNetworkCallsHandler.ts +0 -267
  53. package/.claude/worktrees/release-1.1.1/src/AvoSchemaParser.ts +0 -115
  54. package/.claude/worktrees/release-1.1.1/src/AvoStreamId.ts +0 -16
  55. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEncryption.ts +0 -515
  56. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecCache.ts +0 -242
  57. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoEventSpecFetcher.ts +0 -241
  58. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoInspector.ts +0 -360
  59. package/.claude/worktrees/release-1.1.1/src/__tests__/TestAvoStreamId.ts +0 -72
  60. package/.claude/worktrees/release-1.1.1/src/__tests__/TestDeduplicator.ts +0 -191
  61. package/.claude/worktrees/release-1.1.1/src/__tests__/TestEventValidator.ts +0 -231
  62. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandler.ts +0 -179
  63. package/.claude/worktrees/release-1.1.1/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
  64. package/.claude/worktrees/release-1.1.1/src/__tests__/TestParsing.ts +0 -153
  65. package/.claude/worktrees/release-1.1.1/src/__tests__/constants.ts +0 -63
  66. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecCache.ts +0 -109
  67. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
  68. package/.claude/worktrees/release-1.1.1/src/eventSpec/AvoEventSpecFetcher.ts +0 -106
  69. package/.claude/worktrees/release-1.1.1/src/eventSpec/EventValidator.ts +0 -74
  70. package/.claude/worktrees/release-1.1.1/src/index.ts +0 -2
  71. package/.claude/worktrees/release-1.1.1/src/utils.ts +0 -49
  72. package/.claude/worktrees/release-1.1.1/tsconfig.json +0 -13
  73. package/.claude/worktrees/release-1.1.1/yarn.lock +0 -2310
  74. package/.github/workflows/claude.yml +0 -55
  75. package/dist/__tests__/TestAvoEncryption.d.ts +0 -1
  76. package/dist/__tests__/TestAvoEncryption.js +0 -288
  77. package/dist/__tests__/TestAvoEventSpecCache.d.ts +0 -1
  78. package/dist/__tests__/TestAvoEventSpecCache.js +0 -194
  79. package/dist/__tests__/TestAvoEventSpecFetcher.d.ts +0 -1
  80. package/dist/__tests__/TestAvoEventSpecFetcher.js +0 -219
  81. package/dist/__tests__/TestAvoInspector.d.ts +0 -1
  82. package/dist/__tests__/TestAvoInspector.js +0 -291
  83. package/dist/__tests__/TestAvoStreamId.d.ts +0 -1
  84. package/dist/__tests__/TestAvoStreamId.js +0 -55
  85. package/dist/__tests__/TestDeduplicator.d.ts +0 -1
  86. package/dist/__tests__/TestDeduplicator.js +0 -102
  87. package/dist/__tests__/TestEventValidator.d.ts +0 -1
  88. package/dist/__tests__/TestEventValidator.js +0 -196
  89. package/dist/__tests__/TestNetworkCallsHandler.d.ts +0 -1
  90. package/dist/__tests__/TestNetworkCallsHandler.js +0 -86
  91. package/dist/__tests__/TestNetworkCallsHandlerPublic.d.ts +0 -1
  92. package/dist/__tests__/TestNetworkCallsHandlerPublic.js +0 -160
  93. package/dist/__tests__/TestParsing.d.ts +0 -1
  94. package/dist/__tests__/TestParsing.js +0 -128
  95. package/dist/__tests__/constants.d.ts +0 -42
  96. package/dist/__tests__/constants.js +0 -53
  97. package/examples/node-inspector-js/index.js +0 -30
  98. package/examples/node-inspector-js/package.json +0 -15
  99. package/examples/node-inspector-js/yarn.lock +0 -6
  100. package/examples/node-inspector-ts/dist/index.js +0 -34
  101. package/examples/node-inspector-ts/index.ts +0 -30
  102. package/examples/node-inspector-ts/package.json +0 -18
  103. package/examples/node-inspector-ts/tsconfig.json +0 -15
  104. package/examples/node-inspector-ts/yarn.lock +0 -11
  105. package/reason/AvoInspector.rei +0 -11
  106. package/reason/NodeAvoInspector.re +0 -27
  107. package/src/AvoDeduplicator.ts +0 -150
  108. package/src/AvoEncryption.ts +0 -90
  109. package/src/AvoGuid.ts +0 -9
  110. package/src/AvoInspector.ts +0 -498
  111. package/src/AvoInspectorEnv.ts +0 -8
  112. package/src/AvoNetworkCallsHandler.ts +0 -340
  113. package/src/AvoSchemaParser.ts +0 -115
  114. package/src/AvoStreamId.ts +0 -16
  115. package/src/__tests__/TestAvoEncryption.ts +0 -515
  116. package/src/__tests__/TestAvoEventSpecCache.ts +0 -242
  117. package/src/__tests__/TestAvoEventSpecFetcher.ts +0 -279
  118. package/src/__tests__/TestAvoInspector.ts +0 -376
  119. package/src/__tests__/TestAvoStreamId.ts +0 -72
  120. package/src/__tests__/TestDeduplicator.ts +0 -191
  121. package/src/__tests__/TestEventValidator.ts +0 -231
  122. package/src/__tests__/TestNetworkCallsHandler.ts +0 -181
  123. package/src/__tests__/TestNetworkCallsHandlerPublic.ts +0 -208
  124. package/src/__tests__/TestParsing.ts +0 -153
  125. package/src/__tests__/constants.ts +0 -63
  126. package/src/eventSpec/AvoEventSpecCache.ts +0 -119
  127. package/src/eventSpec/AvoEventSpecFetchTypes.ts +0 -47
  128. package/src/eventSpec/AvoEventSpecFetcher.ts +0 -186
  129. package/src/eventSpec/EventValidator.ts +0 -74
  130. package/src/index.ts +0 -2
  131. package/src/utils.ts +0 -49
  132. package/tsconfig.json +0 -13
@@ -4,32 +4,115 @@ exports.AvoSchemaParser = void 0;
4
4
  let isArray = (obj) => {
5
5
  return Object.prototype.toString.call(obj) === "[object Array]";
6
6
  };
7
+ let isComplex = (value) => {
8
+ return typeof value === "object" && value != null;
9
+ };
10
+ // Binary data is never enumerated (a Buffer has one indexed key per byte): an ArrayBuffer view
11
+ // is typed by its element type, as Java and Go type primitive arrays. A float view
12
+ // (Float16Array, Float32Array, Float64Array) is list(float) with children ["float"], any other
13
+ // (Buffer, integer typed array, DataView) list(int) with ["int"]; an ArrayBuffer or
14
+ // SharedArrayBuffer is an object with no properties. It is still one complex value: the leaf
15
+ // rules apply to it first, and mapping it costs one expansion.
16
+ const FLOAT_VIEW_TAGS = ["[object Float16Array]", "[object Float32Array]", "[object Float64Array]"];
17
+ const binaryElementType = (value) => {
18
+ if (!ArrayBuffer.isView(value)) {
19
+ return null;
20
+ }
21
+ return FLOAT_VIEW_TAGS.indexOf(Object.prototype.toString.call(value)) >= 0 ? "float" : "int";
22
+ };
23
+ const isArrayBufferLike = (value) => {
24
+ const tag = Object.prototype.toString.call(value);
25
+ return tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]";
26
+ };
27
+ // A view's children, whatever its length: its elements are never visited.
28
+ const binaryChildren = (_value, elementType) => [elementType];
29
+ // Deeper complex values are reported as "object" instead of being descended into.
30
+ const MAX_DEPTH = 10;
31
+ // Complex values expanded per extractSchema call. Shared references that are not cycles
32
+ // can otherwise expand exponentially; the rest are reported as "object" like the depth cap.
33
+ const MAX_EXPANSIONS = 10000;
34
+ // Properties emitted per extractSchema call, at every depth; past it the rest are omitted
35
+ // (in iteration order). Independent of MAX_EXPANSIONS: one object with a million keys is a
36
+ // single expansion.
37
+ const MAX_PROPERTIES = 10000;
7
38
  class AvoSchemaParser {
39
+ /**
40
+ * Maps each property to its name, type and (for objects and lists) child schema.
41
+ * Values nested deeper than MAX_DEPTH, cyclic references, and complex values past the
42
+ * MAX_EXPANSIONS budget are reported as "object".
43
+ */
8
44
  static extractSchema(eventProperties) {
9
- if (eventProperties === null || eventProperties === undefined) {
45
+ // Only an object has named properties. JavaScript callers can pass anything, and
46
+ // mapping a primitive, an array or binary data as the root would return a bare type or
47
+ // an element list instead of properties (the wire's eventProperties is a list of them).
48
+ if (!isComplex(eventProperties) ||
49
+ isArray(eventProperties) ||
50
+ binaryElementType(eventProperties) !== null ||
51
+ isArrayBufferLike(eventProperties)) {
10
52
  return [];
11
53
  }
12
- let mapping = (object) => {
54
+ // Objects and arrays on the path from the root to the value being mapped. A value that
55
+ // is its own ancestor (a cycle) is reported like one past the depth cap, so an object
56
+ // holding itself under several keys cannot expand exponentially.
57
+ const ancestors = new Set();
58
+ let expansions = 0;
59
+ let properties = 0;
60
+ const isLeaf = (value, depth) => isComplex(value) &&
61
+ (depth >= MAX_DEPTH || ancestors.has(value) || expansions >= MAX_EXPANSIONS);
62
+ let mapping = (object, depth) => {
63
+ if (isComplex(object)) {
64
+ ancestors.add(object);
65
+ expansions += 1;
66
+ }
67
+ try {
68
+ return mapValue(object, depth);
69
+ }
70
+ finally {
71
+ ancestors.delete(object);
72
+ }
73
+ };
74
+ let mapValue = (object, depth) => {
75
+ const elementType = binaryElementType(object);
76
+ if (elementType !== null) {
77
+ return binaryChildren(object, elementType);
78
+ }
79
+ if (isArrayBufferLike(object)) {
80
+ return [];
81
+ }
13
82
  if (isArray(object)) {
14
83
  let list = object.map((x) => {
15
- return mapping(x);
84
+ return isLeaf(x, depth) ? "object" : mapping(x, depth + 1);
16
85
  });
17
86
  return this.removeDuplicates(list);
18
87
  }
19
- else if (typeof object === "object") {
88
+ else if (isComplex(object)) {
89
+ // isComplex, not typeof: a null list element is the type "null" (spec §9.2), not
90
+ // an object to enumerate.
20
91
  let mappedResult = [];
21
- for (var key in object) {
22
- if (object.hasOwnProperty(key)) {
23
- let val = object[key];
24
- let mappedEntry = {
25
- propertyName: key,
26
- propertyType: this.getPropValueType(val),
27
- };
28
- if (typeof val === "object" && val != null) {
29
- mappedEntry["children"] = mapping(val);
92
+ // Object.keys, not object.hasOwnProperty: a null-prototype object has no such method,
93
+ // and a property named "hasOwnProperty" would shadow it.
94
+ for (const key of Object.keys(object)) {
95
+ if (properties >= MAX_PROPERTIES) {
96
+ break;
97
+ }
98
+ properties += 1;
99
+ let val = object[key];
100
+ let mappedEntry = {
101
+ propertyName: key,
102
+ propertyType: this.getPropValueType(val),
103
+ };
104
+ // Binary data takes this path too, as in Java and Go: past the limits it is "object",
105
+ // otherwise mapping types it with one expansion, without walking its bytes.
106
+ if (isComplex(val)) {
107
+ if (isLeaf(val, depth)) {
108
+ mappedEntry.propertyType = "object";
109
+ mappedEntry["children"] = [];
110
+ }
111
+ else {
112
+ mappedEntry["children"] = mapping(val, depth + 1);
30
113
  }
31
- mappedResult.push(mappedEntry);
32
114
  }
115
+ mappedResult.push(mappedEntry);
33
116
  }
34
117
  return mappedResult;
35
118
  }
@@ -37,25 +120,39 @@ class AvoSchemaParser {
37
120
  return this.getPropValueType(object);
38
121
  }
39
122
  };
40
- var mappedEventProps = mapping(eventProperties);
123
+ var mappedEventProps = mapping(eventProperties, 0);
41
124
  return mappedEventProps;
42
125
  }
126
+ // A list's children: each distinct child schema once, the first occurrence, in order. Children
127
+ // are compared by value (childKey), so equal elements, such as one Buffer repeated, leave one
128
+ // child. Only the output is deduplicated: every element was already mapped and counted
129
+ // toward the limits.
43
130
  static removeDuplicates(array) {
44
- // XXX TODO fix any types
45
- var primitives = { boolean: {}, number: {}, string: {} };
46
- var objects = [];
131
+ const seen = new Set();
47
132
  return array.filter((item) => {
48
- var type = typeof item;
49
- if (type in primitives) {
50
- return primitives[type].hasOwnProperty(item)
51
- ? false
52
- : (primitives[type][item] = true);
53
- }
54
- else {
55
- return objects.indexOf(item) >= 0 ? false : objects.push(item);
133
+ const key = this.childKey(item);
134
+ if (seen.has(key)) {
135
+ return false;
56
136
  }
137
+ seen.add(key);
138
+ return true;
57
139
  });
58
140
  }
141
+ // A canonical key for a child schema: a type string; an object's properties, sorted so that
142
+ // property order does not matter; or a nested list's children, in order.
143
+ static childKey(child) {
144
+ if (typeof child === "string") {
145
+ return JSON.stringify(child);
146
+ }
147
+ if (child.length > 0 && !isArray(child[0]) && typeof child[0] === "object") {
148
+ const properties = child.map((entry) => JSON.stringify(entry.propertyName) +
149
+ ":" +
150
+ JSON.stringify(entry.propertyType) +
151
+ (entry.children !== undefined ? ":" + this.childKey(entry.children) : ""));
152
+ return "{" + properties.sort().join(",") + "}";
153
+ }
154
+ return "[" + child.map((element) => this.childKey(element)).join(",") + "]";
155
+ }
59
156
  static getBasicPropType(propValue) {
60
157
  let propType = typeof propValue;
61
158
  if (propValue == null) {
@@ -64,13 +161,13 @@ class AvoSchemaParser {
64
161
  else if (propType === "string") {
65
162
  return "string";
66
163
  }
67
- else if (propType === "number" || propType === "bigint") {
68
- if ((propValue + "").indexOf(".") >= 0) {
69
- return "float";
70
- }
71
- else {
72
- return "int";
73
- }
164
+ else if (propType === "bigint") {
165
+ return "int";
166
+ }
167
+ else if (propType === "number") {
168
+ // Whole numbers (including 0.0, which JS cannot tell from 0) are "int"; everything
169
+ // else, including exponent forms like 1e-7, NaN and ±Infinity, is "float".
170
+ return Number.isInteger(propValue) ? "int" : "float";
74
171
  }
75
172
  else if (propType === "boolean") {
76
173
  return "boolean";
@@ -83,6 +180,10 @@ class AvoSchemaParser {
83
180
  }
84
181
  }
85
182
  static getPropValueType(propValue) {
183
+ const elementType = binaryElementType(propValue);
184
+ if (elementType !== null) {
185
+ return `list(${elementType})`;
186
+ }
86
187
  if (isArray(propValue)) {
87
188
  //we now know that propValue is an array. get first element in propValue array
88
189
  let propElement = propValue[0];
@@ -91,6 +192,10 @@ class AvoSchemaParser {
91
192
  }
92
193
  else {
93
194
  let propElementType = this.getBasicPropType(propElement);
195
+ // "list(unknown)" is not a wire type; elements with no JSON type count as objects.
196
+ if (propElementType === "unknown") {
197
+ propElementType = "object";
198
+ }
94
199
  return `list(${propElementType})`;
95
200
  }
96
201
  }
@@ -1,5 +1,6 @@
1
1
  export declare class AvoStreamId {
2
2
  private _streamId;
3
- constructor(streamId?: string);
3
+ constructor(streamId?: unknown);
4
+ private static normalize;
4
5
  get streamId(): string;
5
6
  }
@@ -1,13 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AvoStreamId = void 0;
4
+ const AvoInspector_1 = require("./AvoInspector");
5
+ const AvoLog_1 = require("./AvoLog");
4
6
  class AvoStreamId {
7
+ // Accepts whatever a JavaScript caller passes. Numbers, bigints and booleans (a numeric
8
+ // user id, say) are sent as their string form; any other non-string is ignored.
5
9
  constructor(streamId) {
6
- this._streamId = streamId !== null && streamId !== void 0 ? streamId : "";
10
+ this._streamId = AvoStreamId.normalize(streamId);
7
11
  if (this._streamId.includes(":")) {
8
- console.warn("[Avo Inspector] Warning: streamId contains ':' which is not supported");
12
+ AvoLog_1.AvoLog.streamIdColon();
9
13
  }
10
14
  }
15
+ static normalize(streamId) {
16
+ if (streamId === undefined || streamId === null) {
17
+ return "";
18
+ }
19
+ if (typeof streamId === "string") {
20
+ return streamId;
21
+ }
22
+ if (typeof streamId === "number" || typeof streamId === "bigint" || typeof streamId === "boolean") {
23
+ return String(streamId);
24
+ }
25
+ if (AvoInspector_1.AvoInspector.shouldLog) {
26
+ console.warn("[Avo Inspector] Warning: streamId must be a string; ignoring a value of type " + typeof streamId);
27
+ }
28
+ return "";
29
+ }
11
30
  get streamId() {
12
31
  return this._streamId;
13
32
  }
@@ -1,18 +1,41 @@
1
- import { EventSpecResponse } from "./AvoEventSpecFetchTypes";
1
+ import { EventSpecCoordinates, EventSpecResponse } from "./AvoEventSpecFetchTypes";
2
+ /**
3
+ * The key of a fetch with gateway coordinates, or undefined without any: "\0" and a JSON
4
+ * array of `parts` then both normalized coordinates (null for an absent one). JSON encodes
5
+ * every part on its own, so no event name can make it equal another key; and as the
6
+ * apiKey never contains NUL, no key without coordinates starts with "\0".
7
+ * Shared by the spec cache key and the fetcher's in-flight dedupe key.
8
+ */
9
+ export declare const coordinatesKey: (parts: string[], coordinates?: EventSpecCoordinates) => string | undefined;
10
+ /** Trims each coordinate and drops a blank or non-string one. */
11
+ export declare const normalizeCoordinates: (coordinates?: EventSpecCoordinates) => EventSpecCoordinates;
12
+ /** TTL of a fetch that failed other than with a 404, so a blip skips validation only briefly. */
13
+ export declare const TRANSIENT_FAILURE_TTL_MS = 5000;
2
14
  export declare class AvoEventSpecCache {
3
15
  private cache;
4
16
  private globalEventCount;
5
- static makeKey(apiKey: string, streamId: string, eventName: string): string;
17
+ /**
18
+ * The key of one spec. Gateway coordinates are part of it, so a gateway-input spec and an
19
+ * output spec for the same event name never share an entry; without coordinates the key
20
+ * is the plain `apiKey\0streamId\0eventName`.
21
+ */
22
+ static makeKey(apiKey: string, streamId: string, eventName: string, coordinates?: EventSpecCoordinates): string;
6
23
  /**
7
24
  * Returns the cached EventSpecResponse, or undefined on cache miss.
8
- * A cached response with eventSpec: null means "no spec exists for this event".
25
+ * A cached response with eventSpec: null means "no spec exists for this event";
26
+ * a cached null means the fetch failed (such as a 404), so the event is not validated.
9
27
  */
10
- get(key: string): EventSpecResponse | undefined;
28
+ get(key: string): EventSpecResponse | null | undefined;
11
29
  /**
12
30
  * Returns true if the key exists in the cache and has not expired.
13
31
  */
14
32
  contains(key: string): boolean;
15
- set(key: string, value: EventSpecResponse): void;
33
+ /**
34
+ * Stores a fetched spec, or null for a failed fetch. A spec is kept until its TTL or
35
+ * MAX_EVENT_COUNT uses; a null until its TTL only. `ttlMs` defaults to 60 s; a transient
36
+ * failure passes TRANSIENT_FAILURE_TTL_MS.
37
+ */
38
+ set(key: string, value: EventSpecResponse | null, ttlMs?: number): void;
16
39
  flush(): void;
17
40
  private purgeExpired;
18
41
  private evictLRU;
@@ -1,19 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AvoEventSpecCache = void 0;
3
+ exports.AvoEventSpecCache = exports.TRANSIENT_FAILURE_TTL_MS = exports.normalizeCoordinates = exports.coordinatesKey = void 0;
4
+ const utils_1 = require("../utils");
4
5
  const MAX_EVENT_COUNT = 50;
6
+ /**
7
+ * The key of a fetch with gateway coordinates, or undefined without any: "\0" and a JSON
8
+ * array of `parts` then both normalized coordinates (null for an absent one). JSON encodes
9
+ * every part on its own, so no event name can make it equal another key; and as the
10
+ * apiKey never contains NUL, no key without coordinates starts with "\0".
11
+ * Shared by the spec cache key and the fetcher's in-flight dedupe key.
12
+ */
13
+ const coordinatesKey = (parts, coordinates) => {
14
+ const { originHint, outputReference } = (0, exports.normalizeCoordinates)(coordinates);
15
+ if (originHint === undefined && outputReference === undefined) {
16
+ return undefined;
17
+ }
18
+ return "\0" + JSON.stringify([...parts, originHint !== null && originHint !== void 0 ? originHint : null, outputReference !== null && outputReference !== void 0 ? outputReference : null]);
19
+ };
20
+ exports.coordinatesKey = coordinatesKey;
21
+ /** Trims each coordinate and drops a blank or non-string one. */
22
+ const normalizeCoordinates = (coordinates) => {
23
+ const result = {};
24
+ if (coordinates === null || typeof coordinates !== "object") {
25
+ return result;
26
+ }
27
+ const originHint = (0, utils_1.normalizeOption)(coordinates.originHint);
28
+ const outputReference = (0, utils_1.normalizeOption)(coordinates.outputReference);
29
+ if (originHint !== undefined) {
30
+ result.originHint = originHint;
31
+ }
32
+ if (outputReference !== undefined) {
33
+ result.outputReference = outputReference;
34
+ }
35
+ return result;
36
+ };
37
+ exports.normalizeCoordinates = normalizeCoordinates;
5
38
  const TTL_MS = 60000;
39
+ /** TTL of a fetch that failed other than with a 404, so a blip skips validation only briefly. */
40
+ exports.TRANSIENT_FAILURE_TTL_MS = 5000;
6
41
  class AvoEventSpecCache {
7
42
  constructor() {
8
43
  this.cache = new Map();
9
44
  this.globalEventCount = 0;
10
45
  }
11
- static makeKey(apiKey, streamId, eventName) {
12
- return `${apiKey}\0${streamId}\0${eventName}`;
46
+ /**
47
+ * The key of one spec. Gateway coordinates are part of it, so a gateway-input spec and an
48
+ * output spec for the same event name never share an entry; without coordinates the key
49
+ * is the plain `apiKey\0streamId\0eventName`.
50
+ */
51
+ static makeKey(apiKey, streamId, eventName, coordinates) {
52
+ var _a;
53
+ return (_a = (0, exports.coordinatesKey)([apiKey, streamId, eventName], coordinates)) !== null && _a !== void 0 ? _a : `${apiKey}\0${streamId}\0${eventName}`;
13
54
  }
14
55
  /**
15
56
  * Returns the cached EventSpecResponse, or undefined on cache miss.
16
- * A cached response with eventSpec: null means "no spec exists for this event".
57
+ * A cached response with eventSpec: null means "no spec exists for this event";
58
+ * a cached null means the fetch failed (such as a 404), so the event is not validated.
17
59
  */
18
60
  get(key) {
19
61
  const entry = this.cache.get(key);
@@ -21,13 +63,14 @@ class AvoEventSpecCache {
21
63
  return undefined;
22
64
  }
23
65
  // TTL check
24
- if (Date.now() - entry.timestamp > TTL_MS) {
66
+ if (Date.now() - entry.timestamp > entry.ttlMs) {
25
67
  this.cache.delete(key);
26
68
  return undefined;
27
69
  }
28
- // Per-entry access count eviction
70
+ // Per-entry access count eviction, which refreshes a spec after MAX_EVENT_COUNT uses.
71
+ // A cached failure has nothing to refresh: it stays until its TTL.
29
72
  entry.eventCount++;
30
- if (entry.eventCount >= MAX_EVENT_COUNT) {
73
+ if (entry.value !== null && entry.eventCount >= MAX_EVENT_COUNT) {
31
74
  this.cache.delete(key);
32
75
  return undefined;
33
76
  }
@@ -42,17 +85,28 @@ class AvoEventSpecCache {
42
85
  if (entry === undefined) {
43
86
  return false;
44
87
  }
45
- if (Date.now() - entry.timestamp > TTL_MS) {
88
+ if (Date.now() - entry.timestamp > entry.ttlMs) {
46
89
  this.cache.delete(key);
47
90
  return false;
48
91
  }
49
- if (entry.eventCount >= MAX_EVENT_COUNT) {
92
+ if (entry.value !== null && entry.eventCount >= MAX_EVENT_COUNT) {
50
93
  this.cache.delete(key);
51
94
  return false;
52
95
  }
53
96
  return true;
54
97
  }
55
- set(key, value) {
98
+ /**
99
+ * Stores a fetched spec, or null for a failed fetch. A spec is kept until its TTL or
100
+ * MAX_EVENT_COUNT uses; a null until its TTL only. `ttlMs` defaults to 60 s; a transient
101
+ * failure passes TRANSIENT_FAILURE_TTL_MS.
102
+ */
103
+ set(key, value, ttlMs = TTL_MS) {
104
+ // Every track sharing one fetch stores its response: only the first store counts, or a
105
+ // burst of such tracks would rotate unrelated entries out of the cache.
106
+ const existing = this.cache.get(key);
107
+ if (existing !== undefined && existing.value === value) {
108
+ return;
109
+ }
56
110
  this.globalEventCount++;
57
111
  // Global rotation: evict LRU entry every MAX_EVENT_COUNT operations
58
112
  if (this.globalEventCount >= MAX_EVENT_COUNT) {
@@ -63,6 +117,7 @@ class AvoEventSpecCache {
63
117
  this.cache.set(key, {
64
118
  value,
65
119
  timestamp: Date.now(),
120
+ ttlMs,
66
121
  eventCount: 0,
67
122
  lastAccessed: Date.now(),
68
123
  });
@@ -78,7 +133,7 @@ class AvoEventSpecCache {
78
133
  purgeExpired() {
79
134
  const now = Date.now();
80
135
  for (const [key, entry] of this.cache.entries()) {
81
- if (now - entry.timestamp > TTL_MS) {
136
+ if (now - entry.timestamp > entry.ttlMs) {
82
137
  this.cache.delete(key);
83
138
  }
84
139
  }
@@ -1,18 +1,42 @@
1
1
  /**
2
2
  * Type definitions for Event Spec validation responses.
3
3
  */
4
- /** A single property constraint from the spec. */
5
- export interface PropertyConstraint {
6
- propertyName: string;
7
- /** Expected type, e.g. "string", "int", "float", "boolean", "object", "list(string)" */
8
- propertyType: string;
9
- /** Optional regex pattern that the value must match */
10
- regex?: string;
4
+ /**
5
+ * The value constraints the spec sets on one property. Each constraint maps a key to the
6
+ * tracking-plan event IDs (base event or variant) that require it, so one property can carry
7
+ * different rules for different variants.
8
+ */
9
+ export interface PropertyConstraints {
10
+ /** Tracking-plan type name, e.g. "string", "int", "float", "bool", "object" (not validated) */
11
+ type: string;
12
+ /** Whether the property is required (a missing value fails only when it is) */
13
+ required: boolean;
14
+ /** True when the property is a list of `type` */
15
+ isList?: boolean;
16
+ /** Pinned values: exact value -> event IDs that require it */
17
+ pinnedValues?: Record<string, string[]>;
18
+ /** Allowed values: JSON array of strings -> event IDs that accept only those values */
19
+ allowedValues?: Record<string, string[]>;
20
+ /** Regex patterns: pattern -> event IDs that require a match */
21
+ regexPatterns?: Record<string, string[]>;
22
+ /** Ranges: "min,max" (either bound may be empty) -> event IDs that require it */
23
+ minMaxRanges?: Record<string, string[]>;
24
+ /** Constraints on the properties of an object, or of each object in a list */
25
+ children?: Record<string, PropertyConstraints>;
11
26
  }
12
- /** The spec for a single event, as returned by the API. */
27
+ /** One tracking-plan event matching the requested name: its base event and variants. */
28
+ export interface EventSpecEntry {
29
+ branchId: string;
30
+ baseEventId: string;
31
+ variantIds: string[];
32
+ /** Only the properties that carry value constraints */
33
+ props: Record<string, PropertyConstraints>;
34
+ }
35
+ /** The spec for an event name, as returned by the API. */
13
36
  export interface EventSpec {
14
37
  eventName: string;
15
- properties: PropertyConstraint[];
38
+ /** Several entries when name mapping matches more than one event */
39
+ events: EventSpecEntry[];
16
40
  }
17
41
  /** Metadata identifying the spec source. */
18
42
  export interface EventSpecMetadata {
@@ -26,16 +50,24 @@ export interface EventSpecResponse {
26
50
  eventSpec: EventSpec | null;
27
51
  metadata: EventSpecMetadata;
28
52
  }
53
+ /**
54
+ * The outcome for one property: the tracking-plan event IDs whose constraints the value
55
+ * breaks, and the outcome for the object's properties when it has any.
56
+ */
57
+ export interface PropertyValidationOutcome {
58
+ failedEventIds: string[];
59
+ children?: Record<string, PropertyValidationOutcome>;
60
+ }
29
61
  /** Per-property validation result. */
30
- export interface PropertyValidationResult {
62
+ export interface PropertyValidationResult extends PropertyValidationOutcome {
31
63
  propertyName: string;
32
- failedEventIds: string[];
33
- passedEventIds: string[];
34
64
  }
35
- /** Payload sent to reportValidatedEvent. */
36
- export interface ValidatedEventPayload {
37
- streamId: string;
38
- eventName: string;
39
- eventSpecMetadata: EventSpecMetadata;
40
- propertyResults: PropertyValidationResult[];
65
+ /**
66
+ * The gateway coordinates of a spec fetch, as the event carries them on the track body:
67
+ * trimmed, blank omitted. With `originHint` a gateway key answers the gateway-input
68
+ * checkpoint's spec, with both that output's spec; a source key ignores them.
69
+ */
70
+ export interface EventSpecCoordinates {
71
+ originHint?: string;
72
+ outputReference?: string;
41
73
  }
@@ -1,25 +1,48 @@
1
- import { EventSpecResponse } from "./AvoEventSpecFetchTypes";
2
- type FetchCallback = (result: EventSpecResponse | null) => void;
1
+ import { EventSpecResponse, EventSpecCoordinates } from "./AvoEventSpecFetchTypes";
2
+ /**
3
+ * `notFound` is true when a null result is the endpoint's 404 (no spec for these keys),
4
+ * false for any other failure (status, timeout, network error, unparseable body).
5
+ */
6
+ type FetchCallback = (result: EventSpecResponse | null, notFound?: boolean) => void;
3
7
  export declare class AvoEventSpecFetcher {
4
8
  private apiKey;
9
+ private protocol;
10
+ private hostname;
11
+ private port;
12
+ private origin;
5
13
  private inFlight;
6
14
  private agent;
15
+ private requests;
7
16
  private static specEndpoint;
8
- constructor(apiKey: string);
9
- fetch(eventName: string, streamId: string, callback: FetchCallback): void;
17
+ private static fetchTimeoutMs;
18
+ private static socketWaitTimeoutMs;
19
+ /** `mockEndpoint`: a valid override URL (AvoNetworkCallsHandler.mockEndpoint), or null. */
20
+ constructor(apiKey: string, mockEndpoint?: string | null);
21
+ /**
22
+ * `coordinates`: the event's gateway coordinates. Each non-blank one is sent as a query
23
+ * parameter and is part of the dedupe key; without any, the request and key are those of
24
+ * a plain source fetch.
25
+ */
26
+ fetch(eventName: string, streamId: string, callback: FetchCallback, coordinates?: EventSpecCoordinates): void;
27
+ private send;
10
28
  private resolveCallbacks;
11
29
  /**
12
30
  * Parse the wire format from /trackingPlan/eventSpec into our internal types.
13
31
  *
14
32
  * Wire format:
15
- * { events: [{ b, id, vids, p: { "PropName": { t: "string", r: true, v: {...}, rx: {...} } } }],
33
+ * { events: [{ b, id, vids, p: { "PropName": { t, r, l?, p?, v?, rx?, minmax?, children? } } }],
16
34
  * metadata: { schemaId, branchId, latestActionId, sourceId } }
35
+ * where p, v, rx and minmax each map a constraint key to the event IDs that require it.
17
36
  *
18
37
  * Internal format:
19
- * { eventSpec: { eventName, properties: [{ propertyName, propertyType, regex? }] } | null,
38
+ * { eventSpec: { eventName, events: [{ branchId, baseEventId, variantIds, props }] } | null,
20
39
  * metadata: { schemaId, branchId, latestActionId, sourceId } }
21
40
  */
22
41
  static parseWireResponse(wire: any, eventName: string): EventSpecResponse;
42
+ private static parseEventEntry;
43
+ private static parseConstraintsByName;
44
+ private static parseConstraints;
45
+ /** Aborts this instance's in-flight requests; the shared agent stays usable. */
23
46
  destroy(): void;
24
47
  }
25
48
  export {};