node-avo-inspector 2.0.0-beta.1 → 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.
- package/README.md +2 -0
- package/dist/AvoBatchQueue.d.ts +73 -0
- package/dist/AvoBatchQueue.js +239 -0
- package/dist/AvoDeduplicator.d.ts +15 -6
- package/dist/AvoDeduplicator.js +46 -24
- package/dist/AvoInspector.d.ts +136 -23
- package/dist/AvoInspector.js +777 -109
- package/dist/AvoInspectorVersion.d.ts +3 -0
- package/dist/AvoInspectorVersion.js +10 -0
- package/dist/AvoLog.d.ts +52 -0
- package/dist/AvoLog.js +246 -0
- package/dist/AvoNetworkCallsHandler.d.ts +55 -8
- package/dist/AvoNetworkCallsHandler.js +266 -92
- package/dist/AvoSchemaParser.d.ts +6 -0
- package/dist/AvoSchemaParser.js +138 -33
- package/dist/AvoStreamId.d.ts +2 -1
- package/dist/AvoStreamId.js +21 -2
- package/dist/eventSpec/AvoEventSpecCache.d.ts +28 -5
- package/dist/eventSpec/AvoEventSpecCache.js +66 -11
- package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +50 -18
- package/dist/eventSpec/AvoEventSpecFetcher.d.ts +29 -6
- package/dist/eventSpec/AvoEventSpecFetcher.js +279 -69
- package/dist/eventSpec/EventValidator.d.ts +16 -10
- package/dist/eventSpec/EventValidator.js +351 -38
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/utils.d.ts +29 -1
- package/dist/utils.js +86 -10
- package/package.json +2 -1
package/dist/AvoSchemaParser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
45
|
-
var primitives = { boolean: {}, number: {}, string: {} };
|
|
46
|
-
var objects = [];
|
|
131
|
+
const seen = new Set();
|
|
47
132
|
return array.filter((item) => {
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
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 === "
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|
package/dist/AvoStreamId.d.ts
CHANGED
package/dist/AvoStreamId.js
CHANGED
|
@@ -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
|
|
10
|
+
this._streamId = AvoStreamId.normalize(streamId);
|
|
7
11
|
if (this._streamId.includes(":")) {
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
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 >
|
|
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
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
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,
|
|
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 {};
|