@databricks/appkit 0.45.0 → 0.47.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/CLAUDE.md +1 -0
- package/dist/agents/databricks.d.ts +25 -1
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +20 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/app/index.d.ts +49 -2
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +87 -10
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cli/commands/generate-types.js +9 -4
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +52 -0
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/types.d.ts +11 -0
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +2 -0
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js +183 -0
- package/dist/core/lifecycle-manager.js.map +1 -0
- package/dist/core/plugin-context.d.ts +19 -1
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +9 -3
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +2 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +3 -3
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +16 -0
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +162 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/metric.js +7 -0
- package/dist/plugins/analytics/mv/cache.js +51 -0
- package/dist/plugins/analytics/mv/cache.js.map +1 -0
- package/dist/plugins/analytics/mv/constants.js +72 -0
- package/dist/plugins/analytics/mv/constants.js.map +1 -0
- package/dist/plugins/analytics/mv/formatters.js +151 -0
- package/dist/plugins/analytics/mv/formatters.js.map +1 -0
- package/dist/plugins/analytics/mv/index.js +6 -0
- package/dist/plugins/analytics/mv/registry.js +55 -0
- package/dist/plugins/analytics/mv/registry.js.map +1 -0
- package/dist/plugins/analytics/mv/schemas.js +178 -0
- package/dist/plugins/analytics/mv/schemas.js.map +1 -0
- package/dist/plugins/analytics/types.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +10 -2
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +18 -7
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/index.d.ts +51 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +110 -23
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/registry/resource-registry.d.ts +9 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js +22 -5
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/schemas/metric-fqn.js +14 -0
- package/dist/schemas/metric-fqn.js.map +1 -0
- package/dist/shared/src/execute.d.ts +1 -3
- package/dist/shared/src/execute.d.ts.map +1 -1
- package/dist/shared/src/plugin.d.ts +16 -0
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js +78 -46
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js +90 -0
- package/dist/shared/src/schemas/metric-source.js.map +1 -0
- package/dist/stream/buffers.js +1 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/defaults.js +0 -2
- package/dist/stream/defaults.js.map +1 -1
- package/dist/stream/stream-manager.d.ts +2 -2
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +26 -24
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/stream-registry.js +30 -23
- package/dist/stream/stream-registry.js.map +1 -1
- package/dist/stream/timers.js +17 -0
- package/dist/stream/timers.js.map +1 -0
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +19 -13
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/type-generator/index.js +14 -7
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js +13 -31
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/mv-registry/describe.js +1 -31
- package/dist/type-generator/mv-registry/describe.js.map +1 -1
- package/dist/type-generator/mv-registry/sync.js +1 -1
- package/dist/type-generator/mv-registry/sync.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts +5 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +21 -4
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/utils/safe-handler.js +28 -0
- package/dist/utils/safe-handler.js.map +1 -0
- package/docs/api/appkit/Class.Plugin.md +2 -0
- package/docs/api/appkit/Class.ResourceRegistry.md +1 -1
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.GenerationParams.md +58 -0
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit.md +1 -0
- package/docs/development/type-generation.md +3 -3
- package/docs/plugins/analytics.md +172 -23
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
//#region ../shared/src/schemas/metric-fqn.ts
|
|
2
2
|
/**
|
|
3
|
-
* Unity Catalog object-name grammar
|
|
4
|
-
* view FQN
|
|
3
|
+
* Unity Catalog object-name grammar
|
|
4
|
+
* the single source of truth for metric view FQN (Fully Qualified Name).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* validated in two places that must agree:
|
|
6
|
+
* A metric view's `source` FQN is validated in two places that must agree:
|
|
8
7
|
*
|
|
9
8
|
* 1. The canonical Zod schema (`./metric-source.ts`), which composes the
|
|
10
9
|
* three-part FQN regex from {@link UC_FQN_PATTERN} for IDE/CI and the
|
|
@@ -13,18 +12,8 @@
|
|
|
13
12
|
* which imports {@link UC_FQN_PATTERN} as a plain value to validate each
|
|
14
13
|
* dot-split segment.
|
|
15
14
|
*
|
|
16
|
-
* The type-generator's runtime path must NOT pull the shared Zod schema package
|
|
17
|
-
* in (locked dependency-graph ruling - see the comment in
|
|
18
|
-
* `packages/appkit/src/type-generator/cache.ts`). Keeping the pattern in this
|
|
19
|
-
* zod-free module lets the runtime import the regex without dragging zod into
|
|
20
|
-
* its bundle, while still single-sourcing the grammar.
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
* The metric view FQN is always backtick-quoted before interpolation into SQL
|
|
24
|
-
* (see `quoteFqnForSql` in the type-generator), so the **delimited identifier**
|
|
25
|
-
* grammar is the one that applies - not the narrower unquoted-identifier rule.
|
|
26
|
-
*
|
|
27
|
-
* Per the Databricks SQL names reference, a Unity Catalog object name:
|
|
16
|
+
* A Unity Catalog object name:
|
|
28
17
|
* - cannot exceed 255 characters ({@link MAX_UC_OBJECT_NAME_LENGTH}); and
|
|
29
18
|
* - cannot contain any of these characters:
|
|
30
19
|
* - period (`.`)
|
|
@@ -35,41 +24,21 @@
|
|
|
35
24
|
*
|
|
36
25
|
* Every other character is permitted in a quoted name, including non-ASCII
|
|
37
26
|
* letters (the docs demonstrate Chinese/Russian/Portuguese names) and hyphens.
|
|
38
|
-
* This is intentionally broader than the old hand-rolled allowlist
|
|
39
|
-
* (`[a-zA-Z0-9_-]`), which was flagged in PR #433 review (pkosiec: "more
|
|
40
|
-
* restrictive than UC"): the goal is to accept what UC accepts as a quoted
|
|
41
|
-
* name and reject only what UC rejects.
|
|
42
|
-
*
|
|
43
|
-
* Verified against the Databricks docs on 2026-06-19:
|
|
44
|
-
* https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-names
|
|
45
|
-
* (the link cited in the PR #433 review). If the published rules change,
|
|
46
|
-
* re-confirm against that page.
|
|
47
27
|
*
|
|
48
|
-
* @note
|
|
49
|
-
* a name containing a literal dot cannot be expressed in the dotted `source`
|
|
50
|
-
* string at all. The dotted-source arity (exactly three segments) and the
|
|
51
|
-
* 255-char-per-segment cap are enforced structurally by the callers; this
|
|
52
|
-
* pattern only encodes the per-segment allowed character set.
|
|
28
|
+
* @note the regex is the per-segment character set; structure (3 parts) and length are intentionally left to the callers.
|
|
53
29
|
*/
|
|
30
|
+
const MAX_UC_OBJECT_NAME_LENGTH = 255;
|
|
54
31
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
32
|
+
* Basename of the metric-view declarations file, resolved inside a
|
|
33
|
+
* `config/metric-views/` folder. Single source of truth shared by the analytics
|
|
34
|
+
* runtime (`plugins/analytics/mv`), the type-generator, its Vite watcher, and
|
|
35
|
+
* the `generate-types` CLI. It lives in this zod-free module (rather than the
|
|
36
|
+
* canonical `metric-source.ts` schema) so the type-generator can import it
|
|
37
|
+
* without pulling zod into its locked dependency graph.
|
|
57
38
|
*/
|
|
58
|
-
const
|
|
39
|
+
const METRIC_CONFIG_FILE = "definitions.json";
|
|
59
40
|
/**
|
|
60
|
-
* Matches a single, non-empty Unity Catalog object name as it may appear in a
|
|
61
|
-
* backtick-quoted (delimited) identifier - one segment of a metric view FQN.
|
|
62
|
-
*
|
|
63
|
-
* Accepts any non-empty run of characters EXCEPT the UC-prohibited set:
|
|
64
|
-
* period, space, forward slash, ASCII control characters (U+0000-U+001F), and
|
|
65
|
-
* DELETE (U+007F). Length is NOT bounded here - callers enforce
|
|
66
|
-
* {@link MAX_UC_OBJECT_NAME_LENGTH} separately so they can emit a precise
|
|
67
|
-
* "segment too long" message distinct from a charset violation.
|
|
68
|
-
*
|
|
69
|
-
* The negated character class encodes the prohibited set as one contiguous
|
|
70
|
-
* range plus singletons: U+0000-U+0020 (every ASCII control character plus the
|
|
71
|
-
* space, which sits at U+0020 immediately after the control range), U+007F
|
|
72
|
-
* (DELETE), `.` (period - also the FQN segment separator), and `/` (slash).
|
|
41
|
+
* Matches a single, non-empty Unity Catalog object name as it may appear in a backtick-quoted (delimited) identifier.
|
|
73
42
|
*
|
|
74
43
|
* @example
|
|
75
44
|
* UC_FQN_PATTERN.test("revenue_metrics"); // true
|
|
@@ -79,7 +48,70 @@ const MAX_UC_OBJECT_NAME_LENGTH = 255;
|
|
|
79
48
|
* UC_FQN_PATTERN.test("a/b"); // false (slash is prohibited)
|
|
80
49
|
*/
|
|
81
50
|
const UC_FQN_PATTERN = /^[^\x00-\x20\x7f./]+$/;
|
|
51
|
+
/** A metric view FQN is exactly three segments: catalog.schema.metric_view. */
|
|
52
|
+
const FQN_SEGMENT_COUNT = 3;
|
|
53
|
+
/**
|
|
54
|
+
* Total predicate: is `fqn` a well-formed three-part UC metric view FQN?
|
|
55
|
+
* Well-formed = exactly three non-empty, dot-separated segments, each a valid Unity Catalog object name per {@link UC_FQN_PATTERN}.
|
|
56
|
+
* @example
|
|
57
|
+
* isValidFqn("main.analytics.revenue"); // true
|
|
58
|
+
* isValidFqn("prod-data.analytics.rev"); // true (hyphens are UC-legal)
|
|
59
|
+
* isValidFqn("main.analytics"); // false (only two segments)
|
|
60
|
+
*/
|
|
61
|
+
function isValidFqn(fqn) {
|
|
62
|
+
const segments = fqn.split(".");
|
|
63
|
+
if (segments.length !== FQN_SEGMENT_COUNT) return false;
|
|
64
|
+
return segments.every((segment) => UC_FQN_PATTERN.test(segment));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL
|
|
68
|
+
* statement.
|
|
69
|
+
*
|
|
70
|
+
* Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The
|
|
71
|
+
* one character that can break out of a backtick-quoted identifier is the
|
|
72
|
+
* backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick
|
|
73
|
+
* inside a segment is doubled before the segment is wrapped. Control characters
|
|
74
|
+
* and newlines have no valid escape inside a quoted identifier, so a segment
|
|
75
|
+
* containing one is rejected outright.
|
|
76
|
+
*
|
|
77
|
+
* This is a pure, standalone escaper: it is intentionally independent of FQN
|
|
78
|
+
* naming validation ({@link isValidFqn}). Naming validation decides whether an
|
|
79
|
+
* FQN is an acceptable metric source; this function only guarantees that
|
|
80
|
+
* whatever it is handed cannot break out of the quoted identifier it produces.
|
|
81
|
+
* Grammar and quoting live together here so a metric source is validated and
|
|
82
|
+
* escaped against one shared source of truth.
|
|
83
|
+
*
|
|
84
|
+
* An ordinary identifier is unchanged apart from the wrapping backticks:
|
|
85
|
+
* `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.
|
|
86
|
+
*
|
|
87
|
+
* @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).
|
|
88
|
+
* @returns The backtick-quoted, escaped identifier ready for interpolation.
|
|
89
|
+
* @throws If any segment contains a control character or newline.
|
|
90
|
+
*/
|
|
91
|
+
function quoteFqnForSql(fqn) {
|
|
92
|
+
return fqn.split(".").map(quoteIdentifier).join(".");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The Unicode "control" category (`\p{Cc}`): C0 (incl. `\n`, `\r`, `\t`), DEL,
|
|
96
|
+
* and C1 — every control character/newline. These have no valid escape inside
|
|
97
|
+
* a backtick-quoted identifier, so a name containing one cannot be safely
|
|
98
|
+
* quoted and is rejected.
|
|
99
|
+
*/
|
|
100
|
+
const CONTROL_OR_NEWLINE = /\p{Cc}/u;
|
|
101
|
+
function isValidColumnName(name) {
|
|
102
|
+
return name.length > 0 && !CONTROL_OR_NEWLINE.test(name);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Quote a single identifier (one column/measure/dimension name, or one FQN
|
|
106
|
+
* segment) as a backtick-delimited identifier for safe SQL interpolation.
|
|
107
|
+
*
|
|
108
|
+
* @throws If `name` contains a control character or newline.
|
|
109
|
+
*/
|
|
110
|
+
function quoteIdentifier(name) {
|
|
111
|
+
if (CONTROL_OR_NEWLINE.test(name)) throw new Error(`Cannot quote identifier "${name}" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`);
|
|
112
|
+
return `\`${name.replace(/`/g, "``")}\``;
|
|
113
|
+
}
|
|
82
114
|
|
|
83
115
|
//#endregion
|
|
84
|
-
export { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN };
|
|
116
|
+
export { MAX_UC_OBJECT_NAME_LENGTH, METRIC_CONFIG_FILE, UC_FQN_PATTERN, isValidColumnName, isValidFqn, quoteFqnForSql, quoteIdentifier };
|
|
85
117
|
//# sourceMappingURL=metric-fqn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric-fqn.js","names":[],"sources":["../../../../../shared/src/schemas/metric-fqn.ts"],"sourcesContent":["/**\n * Unity Catalog object-name grammar
|
|
1
|
+
{"version":3,"file":"metric-fqn.js","names":[],"sources":["../../../../../shared/src/schemas/metric-fqn.ts"],"sourcesContent":["/**\n * Unity Catalog object-name grammar\n * the single source of truth for metric view FQN (Fully Qualified Name).\n *\n * A metric view's `source` FQN is validated in two places that must agree:\n *\n * 1. The canonical Zod schema (`./metric-source.ts`), which composes the\n * three-part FQN regex from {@link UC_FQN_PATTERN} for IDE/CI and the\n * generated JSON schema (`docs/static/schemas/metric-source.schema.json`).\n * 2. The type-generator runtime (`packages/appkit/src/type-generator/mv-registry/config.ts`),\n * which imports {@link UC_FQN_PATTERN} as a plain value to validate each\n * dot-split segment.\n *\n *\n * A Unity Catalog object name:\n * - cannot exceed 255 characters ({@link MAX_UC_OBJECT_NAME_LENGTH}); and\n * - cannot contain any of these characters:\n * - period (`.`)\n * - space (U+0020)\n * - forward slash (`/`)\n * - all ASCII control characters (U+0000-U+001F)\n * - the DELETE character (U+007F)\n *\n * Every other character is permitted in a quoted name, including non-ASCII\n * letters (the docs demonstrate Chinese/Russian/Portuguese names) and hyphens.\n *\n * @note the regex is the per-segment character set; structure (3 parts) and length are intentionally left to the callers.\n */\n\nexport const MAX_UC_OBJECT_NAME_LENGTH = 255;\n\n/**\n * Basename of the metric-view declarations file, resolved inside a\n * `config/metric-views/` folder. Single source of truth shared by the analytics\n * runtime (`plugins/analytics/mv`), the type-generator, its Vite watcher, and\n * the `generate-types` CLI. It lives in this zod-free module (rather than the\n * canonical `metric-source.ts` schema) so the type-generator can import it\n * without pulling zod into its locked dependency graph.\n */\nexport const METRIC_CONFIG_FILE = \"definitions.json\";\n\n/**\n * Matches a single, non-empty Unity Catalog object name as it may appear in a backtick-quoted (delimited) identifier.\n *\n * @example\n * UC_FQN_PATTERN.test(\"revenue_metrics\"); // true\n * UC_FQN_PATTERN.test(\"prod-data\"); // true (hyphens are UC-legal)\n * UC_FQN_PATTERN.test(\"cafe\\u0301\"); // true (non-ASCII is UC-legal)\n * UC_FQN_PATTERN.test(\"bad name\"); // false (space is prohibited)\n * UC_FQN_PATTERN.test(\"a/b\"); // false (slash is prohibited)\n */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: UC explicitly prohibits ASCII control characters in object names; this negated class encodes that rule.\nexport const UC_FQN_PATTERN = /^[^\\x00-\\x20\\x7f./]+$/;\n\n/** A metric view FQN is exactly three segments: catalog.schema.metric_view. */\nconst FQN_SEGMENT_COUNT = 3;\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid Unity Catalog object name per {@link UC_FQN_PATTERN}.\n * @example\n * isValidFqn(\"main.analytics.revenue\"); // true\n * isValidFqn(\"prod-data.analytics.rev\"); // true (hyphens are UC-legal)\n * isValidFqn(\"main.analytics\"); // false (only two segments)\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL\n * statement.\n *\n * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The\n * one character that can break out of a backtick-quoted identifier is the\n * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick\n * inside a segment is doubled before the segment is wrapped. Control characters\n * and newlines have no valid escape inside a quoted identifier, so a segment\n * containing one is rejected outright.\n *\n * This is a pure, standalone escaper: it is intentionally independent of FQN\n * naming validation ({@link isValidFqn}). Naming validation decides whether an\n * FQN is an acceptable metric source; this function only guarantees that\n * whatever it is handed cannot break out of the quoted identifier it produces.\n * Grammar and quoting live together here so a metric source is validated and\n * escaped against one shared source of truth.\n *\n * An ordinary identifier is unchanged apart from the wrapping backticks:\n * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.\n *\n * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).\n * @returns The backtick-quoted, escaped identifier ready for interpolation.\n * @throws If any segment contains a control character or newline.\n */\nexport function quoteFqnForSql(fqn: string): string {\n return fqn.split(\".\").map(quoteIdentifier).join(\".\");\n}\n\n/**\n * The Unicode \"control\" category (`\\p{Cc}`): C0 (incl. `\\n`, `\\r`, `\\t`), DEL,\n * and C1 — every control character/newline. These have no valid escape inside\n * a backtick-quoted identifier, so a name containing one cannot be safely\n * quoted and is rejected.\n */\nconst CONTROL_OR_NEWLINE = /\\p{Cc}/u;\n\nexport function isValidColumnName(name: string): boolean {\n return name.length > 0 && !CONTROL_OR_NEWLINE.test(name);\n}\n\n/**\n * Quote a single identifier (one column/measure/dimension name, or one FQN\n * segment) as a backtick-delimited identifier for safe SQL interpolation.\n *\n * @throws If `name` contains a control character or newline.\n */\nexport function quoteIdentifier(name: string): string {\n if (CONTROL_OR_NEWLINE.test(name)) {\n throw new Error(\n `Cannot quote identifier \"${name}\" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`,\n );\n }\n // Double every backtick, then wrap in backticks.\n return `\\`${name.replace(/`/g, \"``\")}\\``;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,4BAA4B;;;;;;;;;AAUzC,MAAa,qBAAqB;;;;;;;;;;;AAalC,MAAa,iBAAiB;;AAG9B,MAAM,oBAAoB;;;;;;;;;AAU1B,SAAgB,WAAW,KAAsB;CAC/C,MAAM,WAAW,IAAI,MAAM,IAAI;AAC/B,KAAI,SAAS,WAAW,kBACtB,QAAO;AAET,QAAO,SAAS,OAAO,YAAY,eAAe,KAAK,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BlE,SAAgB,eAAe,KAAqB;AAClD,QAAO,IAAI,MAAM,IAAI,CAAC,IAAI,gBAAgB,CAAC,KAAK,IAAI;;;;;;;;AAStD,MAAM,qBAAqB;AAE3B,SAAgB,kBAAkB,MAAuB;AACvD,QAAO,KAAK,SAAS,KAAK,CAAC,mBAAmB,KAAK,KAAK;;;;;;;;AAS1D,SAAgB,gBAAgB,MAAsB;AACpD,KAAI,mBAAmB,KAAK,KAAK,CAC/B,OAAM,IAAI,MACR,4BAA4B,KAAK,uHAClC;AAGH,QAAO,KAAK,KAAK,QAAQ,MAAM,KAAK,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from "./metric-fqn.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region ../shared/src/schemas/metric-source.ts
|
|
5
|
+
/**
|
|
6
|
+
* AppKit metric-source schema.
|
|
7
|
+
*
|
|
8
|
+
* Single source of truth for `config/metric-views/definitions.json`
|
|
9
|
+
* the config that activates the Analytics' metric-view path.
|
|
10
|
+
*
|
|
11
|
+
* `definitions.json` declares UC Metric Views under a single `metricViews` map.
|
|
12
|
+
* Each entry binds a metric key to a UC metric view FQN plus the executor
|
|
13
|
+
* the query runs as:
|
|
14
|
+
* - `executor: "app_service_principal"` (default) — queried as the app service
|
|
15
|
+
* principal (cache scope shared across all users).
|
|
16
|
+
* - `executor: "user"` — queried as the requesting user (on-behalf-of;
|
|
17
|
+
* cache scope per-user).
|
|
18
|
+
*
|
|
19
|
+
* A single map (rather than per-executor sections) makes metric keys unique
|
|
20
|
+
* by construction — the same key cannot be declared twice with different
|
|
21
|
+
* executors.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Safety cap on the number of declared metric views — a typo / DoS guard, NOT
|
|
25
|
+
* a Unity Catalog limit. Mirrors the type-generator's `MAX_METRIC_VIEWS` so
|
|
26
|
+
* runtime config validation and type generation accept exactly the same
|
|
27
|
+
* configs (a config that fails generation must not silently pass at runtime).
|
|
28
|
+
*/
|
|
29
|
+
const MAX_METRIC_VIEWS = 200;
|
|
30
|
+
/**
|
|
31
|
+
* Whole-FQN length cap: three max-length UC object names plus the two dots.
|
|
32
|
+
* The per-segment cap ({@link MAX_UC_OBJECT_NAME_LENGTH}) is enforced in the
|
|
33
|
+
* `superRefine` below; this whole-string bound is the declarative half (it
|
|
34
|
+
* serializes to a JSON-schema `maxLength`, whereas the per-segment check — like
|
|
35
|
+
* the entry-count cap — cannot be expressed declaratively and lives in the
|
|
36
|
+
* refinement, so it is a runtime/type-generator gate only).
|
|
37
|
+
*/
|
|
38
|
+
const MAX_FQN_LENGTH = MAX_UC_OBJECT_NAME_LENGTH * 3 + 2;
|
|
39
|
+
/**
|
|
40
|
+
* Three-part Unity Catalog FQN matcher, composed from the single-segment
|
|
41
|
+
* {@link UC_FQN_PATTERN} so the per-segment grammar has exactly one source of
|
|
42
|
+
* truth (shared by the type-generator runtime, which imports the zod-free
|
|
43
|
+
* {@link UC_FQN_PATTERN} directly — see `./metric-fqn.ts`).
|
|
44
|
+
*
|
|
45
|
+
* `UC_FQN_PATTERN` is `^<segment>+$`; stripping its `^`/`$` anchors yields the
|
|
46
|
+
* per-segment sub-pattern, which is joined with literal dots into
|
|
47
|
+
* `^<segment>\.<segment>\.<segment>$`. Exactly three dot-separated segments,
|
|
48
|
+
* each a valid UC object name. Arity (and the per-segment length cap) is also
|
|
49
|
+
* enforced structurally by the type-generator's `resolveMetricConfig`.
|
|
50
|
+
*/
|
|
51
|
+
const UC_FQN_SEGMENT_SOURCE = UC_FQN_PATTERN.source.replace(/^\^/, "").replace(/\$$/, "");
|
|
52
|
+
const UC_THREE_PART_FQN_PATTERN = new RegExp(`^${UC_FQN_SEGMENT_SOURCE}\\.${UC_FQN_SEGMENT_SOURCE}\\.${UC_FQN_SEGMENT_SOURCE}$`);
|
|
53
|
+
const metricKeySchema = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/).describe("Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('<key>', ...), and the MetricRegistry augmentation key.");
|
|
54
|
+
const metricExecutorSchema = z.enum(["app_service_principal", "user"]).describe("Who the metric view is queried as. 'app_service_principal' (default) runs as the app service principal with a cache shared across all users; 'user' runs on-behalf-of the requesting user with a per-user cache.");
|
|
55
|
+
/**
|
|
56
|
+
* @note Entries are objects (rather than bare strings) at v1 so future per-entry
|
|
57
|
+
* options (cacheTtl, defaultFilter, allowlists) can ship as additive
|
|
58
|
+
* properties without a breaking change. `executor` is the first such option.
|
|
59
|
+
*/
|
|
60
|
+
const metricEntrySchema = z.object({
|
|
61
|
+
source: z.string().regex(UC_THREE_PART_FQN_PATTERN).max(MAX_FQN_LENGTH).describe("Three-part Unity Catalog FQN of the metric view: <catalog>.<schema>.<metric_view>").meta({ examples: ["appkit_demo.public.revenue_metrics", "main.analytics.customer_metrics"] }),
|
|
62
|
+
executor: metricExecutorSchema.default("app_service_principal")
|
|
63
|
+
}).strict().describe("A single metric view source declaration: the UC FQN to query and the executor to query it as. Future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.");
|
|
64
|
+
const metricSourceSchema = z.object({
|
|
65
|
+
$schema: z.string().optional().describe("Reference to the JSON Schema for validation"),
|
|
66
|
+
metricViews: z.record(metricKeySchema, metricEntrySchema).optional().describe("Metric view declarations, keyed by metric key. Each entry names the UC metric view to query and the executor it runs as.")
|
|
67
|
+
}).strict().describe("Schema for AppKit config/metric-views/definitions.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under 'metricViews' binds a metric key to a UC metric view FQN and an executor ('app_service_principal' shared cache, or 'user' per-user cache). Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.").superRefine((value, ctx) => {
|
|
68
|
+
const entries = value.metricViews ? Object.entries(value.metricViews) : [];
|
|
69
|
+
if (entries.length > MAX_METRIC_VIEWS) ctx.addIssue({
|
|
70
|
+
code: "custom",
|
|
71
|
+
message: `too many metric views: ${entries.length} declared, exceeding the maximum of ${MAX_METRIC_VIEWS}`,
|
|
72
|
+
path: ["metricViews"]
|
|
73
|
+
});
|
|
74
|
+
for (const [key, entry] of entries) {
|
|
75
|
+
const segments = entry.source.split(".");
|
|
76
|
+
for (let i = 0; i < segments.length; i++) if (segments[i].length > MAX_UC_OBJECT_NAME_LENGTH) ctx.addIssue({
|
|
77
|
+
code: "custom",
|
|
78
|
+
message: `metric source segment ${i + 1} is ${segments[i].length} characters, exceeding the per-segment maximum of ${MAX_UC_OBJECT_NAME_LENGTH}`,
|
|
79
|
+
path: [
|
|
80
|
+
"metricViews",
|
|
81
|
+
key,
|
|
82
|
+
"source"
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { metricSourceSchema };
|
|
90
|
+
//# sourceMappingURL=metric-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-source.js","names":[],"sources":["../../../../../shared/src/schemas/metric-source.ts"],"sourcesContent":["/**\n * AppKit metric-source schema.\n *\n * Single source of truth for `config/metric-views/definitions.json`\n * the config that activates the Analytics' metric-view path.\n *\n * `definitions.json` declares UC Metric Views under a single `metricViews` map.\n * Each entry binds a metric key to a UC metric view FQN plus the executor\n * the query runs as:\n * - `executor: \"app_service_principal\"` (default) — queried as the app service\n * principal (cache scope shared across all users).\n * - `executor: \"user\"` — queried as the requesting user (on-behalf-of;\n * cache scope per-user).\n *\n * A single map (rather than per-executor sections) makes metric keys unique\n * by construction — the same key cannot be declared twice with different\n * executors.\n */\n\nimport { z } from \"zod\";\nimport { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from \"./metric-fqn\";\n\n/**\n * Safety cap on the number of declared metric views — a typo / DoS guard, NOT\n * a Unity Catalog limit. Mirrors the type-generator's `MAX_METRIC_VIEWS` so\n * runtime config validation and type generation accept exactly the same\n * configs (a config that fails generation must not silently pass at runtime).\n */\nconst MAX_METRIC_VIEWS = 200;\n\n/**\n * Whole-FQN length cap: three max-length UC object names plus the two dots.\n * The per-segment cap ({@link MAX_UC_OBJECT_NAME_LENGTH}) is enforced in the\n * `superRefine` below; this whole-string bound is the declarative half (it\n * serializes to a JSON-schema `maxLength`, whereas the per-segment check — like\n * the entry-count cap — cannot be expressed declaratively and lives in the\n * refinement, so it is a runtime/type-generator gate only).\n */\nconst MAX_FQN_LENGTH = MAX_UC_OBJECT_NAME_LENGTH * 3 + 2;\n\n/**\n * Three-part Unity Catalog FQN matcher, composed from the single-segment\n * {@link UC_FQN_PATTERN} so the per-segment grammar has exactly one source of\n * truth (shared by the type-generator runtime, which imports the zod-free\n * {@link UC_FQN_PATTERN} directly — see `./metric-fqn.ts`).\n *\n * `UC_FQN_PATTERN` is `^<segment>+$`; stripping its `^`/`$` anchors yields the\n * per-segment sub-pattern, which is joined with literal dots into\n * `^<segment>\\.<segment>\\.<segment>$`. Exactly three dot-separated segments,\n * each a valid UC object name. Arity (and the per-segment length cap) is also\n * enforced structurally by the type-generator's `resolveMetricConfig`.\n */\nconst UC_FQN_SEGMENT_SOURCE = UC_FQN_PATTERN.source\n .replace(/^\\^/, \"\")\n .replace(/\\$$/, \"\");\nconst UC_THREE_PART_FQN_PATTERN = new RegExp(\n `^${UC_FQN_SEGMENT_SOURCE}\\\\.${UC_FQN_SEGMENT_SOURCE}\\\\.${UC_FQN_SEGMENT_SOURCE}$`,\n);\n\nexport const metricKeySchema = z\n .string()\n .regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/)\n .describe(\n \"Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('<key>', ...), and the MetricRegistry augmentation key.\",\n );\n\nexport const metricExecutorSchema = z\n .enum([\"app_service_principal\", \"user\"])\n .describe(\n \"Who the metric view is queried as. 'app_service_principal' (default) runs as the app service principal with a cache shared across all users; 'user' runs on-behalf-of the requesting user with a per-user cache.\",\n );\n\n/**\n * @note Entries are objects (rather than bare strings) at v1 so future per-entry\n * options (cacheTtl, defaultFilter, allowlists) can ship as additive\n * properties without a breaking change. `executor` is the first such option.\n */\nexport const metricEntrySchema = z\n .object({\n source: z\n .string()\n .regex(UC_THREE_PART_FQN_PATTERN)\n .max(MAX_FQN_LENGTH)\n .describe(\n \"Three-part Unity Catalog FQN of the metric view: <catalog>.<schema>.<metric_view>\",\n )\n .meta({\n examples: [\n \"appkit_demo.public.revenue_metrics\",\n \"main.analytics.customer_metrics\",\n ],\n }),\n executor: metricExecutorSchema.default(\"app_service_principal\"),\n })\n .strict()\n .describe(\n \"A single metric view source declaration: the UC FQN to query and the executor to query it as. Future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.\",\n );\n\nexport const metricSourceSchema = z\n .object({\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n metricViews: z\n .record(metricKeySchema, metricEntrySchema)\n .optional()\n .describe(\n \"Metric view declarations, keyed by metric key. Each entry names the UC metric view to query and the executor it runs as.\",\n ),\n })\n .strict()\n .describe(\n \"Schema for AppKit config/metric-views/definitions.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under 'metricViews' binds a metric key to a UC metric view FQN and an executor ('app_service_principal' shared cache, or 'user' per-user cache). Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.\",\n )\n // Caps that cannot be expressed declaratively (zod 4's `z.record` has no\n // `.max`, and a per-dot-segment length bound isn't a whole-string\n // `maxLength`). Enforced here so runtime config validation matches the\n // type-generator's `resolveMetricConfig` exactly — a config that fails type\n // generation must not silently pass at runtime. These refinements are\n // invisible to `z.toJSONSchema`, so the generated JSON schema carries only\n // the declarative `maxLength` on `source`; runtime + type-generator remain\n // the authoritative gates for the entry-count and per-segment caps.\n .superRefine((value, ctx) => {\n const entries = value.metricViews ? Object.entries(value.metricViews) : [];\n\n if (entries.length > MAX_METRIC_VIEWS) {\n ctx.addIssue({\n code: \"custom\",\n message: `too many metric views: ${entries.length} declared, exceeding the maximum of ${MAX_METRIC_VIEWS}`,\n path: [\"metricViews\"],\n });\n }\n\n for (const [key, entry] of entries) {\n const segments = entry.source.split(\".\");\n for (let i = 0; i < segments.length; i++) {\n if (segments[i].length > MAX_UC_OBJECT_NAME_LENGTH) {\n ctx.addIssue({\n code: \"custom\",\n message: `metric source segment ${i + 1} is ${segments[i].length} characters, exceeding the per-segment maximum of ${MAX_UC_OBJECT_NAME_LENGTH}`,\n path: [\"metricViews\", key, \"source\"],\n });\n }\n }\n }\n });\n\nexport type MetricKey = z.infer<typeof metricKeySchema>;\nexport type MetricExecutor = z.infer<typeof metricExecutorSchema>;\nexport type MetricEntry = z.infer<typeof metricEntrySchema>;\nexport type MetricSource = z.infer<typeof metricSourceSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAM,mBAAmB;;;;;;;;;AAUzB,MAAM,iBAAiB,4BAA4B,IAAI;;;;;;;;;;;;;AAcvD,MAAM,wBAAwB,eAAe,OAC1C,QAAQ,OAAO,GAAG,CAClB,QAAQ,OAAO,GAAG;AACrB,MAAM,4BAA4B,IAAI,OACpC,IAAI,sBAAsB,KAAK,sBAAsB,KAAK,sBAAsB,GACjF;AAED,MAAa,kBAAkB,EAC5B,QAAQ,CACR,MAAM,2BAA2B,CACjC,SACC,yPACD;AAEH,MAAa,uBAAuB,EACjC,KAAK,CAAC,yBAAyB,OAAO,CAAC,CACvC,SACC,mNACD;;;;;;AAOH,MAAa,oBAAoB,EAC9B,OAAO;CACN,QAAQ,EACL,QAAQ,CACR,MAAM,0BAA0B,CAChC,IAAI,eAAe,CACnB,SACC,oFACD,CACA,KAAK,EACJ,UAAU,CACR,sCACA,kCACD,EACF,CAAC;CACJ,UAAU,qBAAqB,QAAQ,wBAAwB;CAChE,CAAC,CACD,QAAQ,CACR,SACC,4LACD;AAEH,MAAa,qBAAqB,EAC/B,OAAO;CACN,SAAS,EACN,QAAQ,CACR,UAAU,CACV,SAAS,8CAA8C;CAC1D,aAAa,EACV,OAAO,iBAAiB,kBAAkB,CAC1C,UAAU,CACV,SACC,2HACD;CACJ,CAAC,CACD,QAAQ,CACR,SACC,4ZACD,CASA,aAAa,OAAO,QAAQ;CAC3B,MAAM,UAAU,MAAM,cAAc,OAAO,QAAQ,MAAM,YAAY,GAAG,EAAE;AAE1E,KAAI,QAAQ,SAAS,iBACnB,KAAI,SAAS;EACX,MAAM;EACN,SAAS,0BAA0B,QAAQ,OAAO,sCAAsC;EACxF,MAAM,CAAC,cAAc;EACtB,CAAC;AAGJ,MAAK,MAAM,CAAC,KAAK,UAAU,SAAS;EAClC,MAAM,WAAW,MAAM,OAAO,MAAM,IAAI;AACxC,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,KAAI,SAAS,GAAG,SAAS,0BACvB,KAAI,SAAS;GACX,MAAM;GACN,SAAS,yBAAyB,IAAI,EAAE,MAAM,SAAS,GAAG,OAAO,oDAAoD;GACrH,MAAM;IAAC;IAAe;IAAK;IAAS;GACrC,CAAC;;EAIR"}
|
package/dist/stream/buffers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffers.js","names":[],"sources":["../../src/stream/buffers.ts"],"sourcesContent":["import { ValidationError } from \"../errors\";\nimport type { BufferedEvent } from \"./types\";\n\n// generic ring buffer implementation\
|
|
1
|
+
{"version":3,"file":"buffers.js","names":[],"sources":["../../src/stream/buffers.ts"],"sourcesContent":["import { ValidationError } from \"../errors\";\nimport type { BufferedEvent } from \"./types\";\n\n// generic ring buffer implementation\nclass RingBuffer<T> {\n public buffer: (T | null)[];\n public capacity: number;\n private writeIndex: number;\n private size: number;\n private keyExtractor: (item: T) => string;\n private keyIndex: Map<string, number>;\n\n constructor(capacity: number, keyExtractor: (item: T) => string) {\n if (capacity <= 0) {\n throw ValidationError.invalidValue(\n \"capacity\",\n capacity,\n \"greater than 0\",\n );\n }\n\n this.capacity = capacity;\n this.buffer = new Array(capacity).fill(null);\n this.writeIndex = 0;\n this.size = 0;\n this.keyExtractor = keyExtractor;\n this.keyIndex = new Map();\n }\n\n // add an item to the buffer\n add(item: T): void {\n const key = this.keyExtractor(item);\n\n // check if item already exists\n const existingIndex = this.keyIndex.get(key);\n if (existingIndex !== undefined) {\n // update existing item\n this.buffer[existingIndex] = item;\n return;\n }\n\n // evict least recently used item if at capacity\n const evicted = this.buffer[this.writeIndex];\n if (evicted !== null) {\n const evictedKey = this.keyExtractor(evicted);\n this.keyIndex.delete(evictedKey);\n }\n\n // add new item\n this.buffer[this.writeIndex] = item;\n this.keyIndex.set(key, this.writeIndex);\n\n // update write index and size\n this.writeIndex = (this.writeIndex + 1) % this.capacity;\n this.size = Math.min(this.size + 1, this.capacity);\n }\n\n // get an item from the buffer\n get(key: string): T | null {\n const index = this.keyIndex.get(key);\n if (index === undefined) return null;\n\n return this.buffer[index];\n }\n\n // check if an item exists in the buffer\n has(key: string): boolean {\n return this.keyIndex.has(key);\n }\n\n // remove an item from the buffer\n remove(key: string): void {\n const index = this.keyIndex.get(key);\n if (index === undefined) return;\n\n // remove item from buffer\n this.buffer[index] = null;\n this.keyIndex.delete(key);\n\n // update size\n this.size = Math.max(this.size - 1, 0);\n }\n\n // get all items from the buffer\n getAll(): T[] {\n const result: T[] = [];\n\n // iterate over buffer in order of insertion\n for (let i = 0; i < this.size; i++) {\n // calculate index of item in buffer\n const index =\n (this.writeIndex - this.size + i + this.capacity) % this.capacity;\n // add item to result if not null\n const item = this.buffer[index];\n if (item !== null) {\n result.push(item);\n }\n }\n return result;\n }\n\n // get the size of the buffer\n getSize(): number {\n return this.size;\n }\n\n // clear the buffer\n clear(): void {\n this.buffer = new Array(this.capacity).fill(null);\n this.keyIndex.clear();\n this.writeIndex = 0;\n this.size = 0;\n }\n}\n\n// event ring buffer implementation\nexport class EventRingBuffer {\n private buffer: RingBuffer<BufferedEvent>;\n\n constructor(capacity: number = 100) {\n this.buffer = new RingBuffer<BufferedEvent>(capacity, (event) => event.id);\n }\n\n // add an event to the buffer\n add(event: BufferedEvent): void {\n this.buffer.add(event);\n }\n\n // check if an event exists in the buffer\n has(eventId: string): boolean {\n return this.buffer.has(eventId);\n }\n\n // get all events since a given event id\n getEventsSince(lastEventId: string): BufferedEvent[] {\n const allEvents = this.buffer.getAll();\n const result: BufferedEvent[] = [];\n // flag to track if we've found the last event\n let foundLastEvent = false;\n\n // iterate over all events\n for (const event of allEvents) {\n // if found, add to result\n if (foundLastEvent) {\n result.push(event);\n // if not found, check if it's the last event\n } else if (event.id === lastEventId) {\n foundLastEvent = true;\n }\n }\n return result;\n }\n\n clear(): void {\n this.buffer.clear();\n }\n}\n"],"mappings":";;;;AAIA,IAAM,aAAN,MAAoB;CAClB,AAAO;CACP,AAAO;CACP,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,YAAY,UAAkB,cAAmC;AAC/D,MAAI,YAAY,EACd,OAAM,gBAAgB,aACpB,YACA,UACA,iBACD;AAGH,OAAK,WAAW;AAChB,OAAK,SAAS,IAAI,MAAM,SAAS,CAAC,KAAK,KAAK;AAC5C,OAAK,aAAa;AAClB,OAAK,OAAO;AACZ,OAAK,eAAe;AACpB,OAAK,2BAAW,IAAI,KAAK;;CAI3B,IAAI,MAAe;EACjB,MAAM,MAAM,KAAK,aAAa,KAAK;EAGnC,MAAM,gBAAgB,KAAK,SAAS,IAAI,IAAI;AAC5C,MAAI,kBAAkB,QAAW;AAE/B,QAAK,OAAO,iBAAiB;AAC7B;;EAIF,MAAM,UAAU,KAAK,OAAO,KAAK;AACjC,MAAI,YAAY,MAAM;GACpB,MAAM,aAAa,KAAK,aAAa,QAAQ;AAC7C,QAAK,SAAS,OAAO,WAAW;;AAIlC,OAAK,OAAO,KAAK,cAAc;AAC/B,OAAK,SAAS,IAAI,KAAK,KAAK,WAAW;AAGvC,OAAK,cAAc,KAAK,aAAa,KAAK,KAAK;AAC/C,OAAK,OAAO,KAAK,IAAI,KAAK,OAAO,GAAG,KAAK,SAAS;;CAIpD,IAAI,KAAuB;EACzB,MAAM,QAAQ,KAAK,SAAS,IAAI,IAAI;AACpC,MAAI,UAAU,OAAW,QAAO;AAEhC,SAAO,KAAK,OAAO;;CAIrB,IAAI,KAAsB;AACxB,SAAO,KAAK,SAAS,IAAI,IAAI;;CAI/B,OAAO,KAAmB;EACxB,MAAM,QAAQ,KAAK,SAAS,IAAI,IAAI;AACpC,MAAI,UAAU,OAAW;AAGzB,OAAK,OAAO,SAAS;AACrB,OAAK,SAAS,OAAO,IAAI;AAGzB,OAAK,OAAO,KAAK,IAAI,KAAK,OAAO,GAAG,EAAE;;CAIxC,SAAc;EACZ,MAAM,SAAc,EAAE;AAGtB,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,MAAM,KAAK;GAElC,MAAM,SACH,KAAK,aAAa,KAAK,OAAO,IAAI,KAAK,YAAY,KAAK;GAE3D,MAAM,OAAO,KAAK,OAAO;AACzB,OAAI,SAAS,KACX,QAAO,KAAK,KAAK;;AAGrB,SAAO;;CAIT,UAAkB;AAChB,SAAO,KAAK;;CAId,QAAc;AACZ,OAAK,SAAS,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,KAAK;AACjD,OAAK,SAAS,OAAO;AACrB,OAAK,aAAa;AAClB,OAAK,OAAO;;;AAKhB,IAAa,kBAAb,MAA6B;CAC3B,AAAQ;CAER,YAAY,WAAmB,KAAK;AAClC,OAAK,SAAS,IAAI,WAA0B,WAAW,UAAU,MAAM,GAAG;;CAI5E,IAAI,OAA4B;AAC9B,OAAK,OAAO,IAAI,MAAM;;CAIxB,IAAI,SAA0B;AAC5B,SAAO,KAAK,OAAO,IAAI,QAAQ;;CAIjC,eAAe,aAAsC;EACnD,MAAM,YAAY,KAAK,OAAO,QAAQ;EACtC,MAAM,SAA0B,EAAE;EAElC,IAAI,iBAAiB;AAGrB,OAAK,MAAM,SAAS,UAElB,KAAI,eACF,QAAO,KAAK,MAAM;WAET,MAAM,OAAO,YACtB,kBAAiB;AAGrB,SAAO;;CAGT,QAAc;AACZ,OAAK,OAAO,OAAO"}
|
package/dist/stream/defaults.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../src/stream/defaults.ts"],"sourcesContent":["export const streamDefaults = {\n bufferSize: 100,\n // 1 MiB. SSE carries only short JSON control messages — JSON_ARRAY result\n // rows (already row-size-bounded by the warehouse) plus warehouse-readiness\n // and error events. ARROW_STREAM never uses SSE: the raw Arrow IPC bytes\n // stream back on the query response body (`_handleArrowStreamQuery`).\n maxEventSize: 1 * 1024 * 1024,\n bufferTTL: 10 * 60 * 1000, // 10 minutes\n
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../src/stream/defaults.ts"],"sourcesContent":["export const streamDefaults = {\n bufferSize: 100,\n // 1 MiB. SSE carries only short JSON control messages — JSON_ARRAY result\n // rows (already row-size-bounded by the warehouse) plus warehouse-readiness\n // and error events. ARROW_STREAM never uses SSE: the raw Arrow IPC bytes\n // stream back on the query response body (`_handleArrowStreamQuery`).\n maxEventSize: 1 * 1024 * 1024,\n bufferTTL: 10 * 60 * 1000, // 10 minutes\n heartbeatInterval: 10 * 1000, // 10 seconds\n maxActiveStreams: 1000, // 1000 streams\n disconnectGraceMs: 15_000, // 15 seconds\n} as const;\n"],"mappings":";AAAA,MAAa,iBAAiB;CAC5B,YAAY;CAKZ,cAAc,IAAI,OAAO;CACzB,WAAW,MAAU;CACrB,mBAAmB,KAAK;CACxB,kBAAkB;CAClB,mBAAmB;CACpB"}
|
|
@@ -20,10 +20,10 @@ declare class StreamManager {
|
|
|
20
20
|
private _combineSignals;
|
|
21
21
|
private _broadcastEventsToClients;
|
|
22
22
|
private _broadcastErrorToClients;
|
|
23
|
+
private _finalizeStream;
|
|
23
24
|
private _closeAllClients;
|
|
24
25
|
private _scheduleGraceAbort;
|
|
25
|
-
private
|
|
26
|
-
private _cleanupStream;
|
|
26
|
+
private _scheduleRemovalAfterTTL;
|
|
27
27
|
private _categorizeError;
|
|
28
28
|
}
|
|
29
29
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"mappings":";;;;;cAiBa,aAAA;EAAA,QACH,gBAAA;EAAA,QACA,cAAA;EAAA,QACA,SAAA;EAAA,QACA,YAAA;EAAA,QACA,SAAA;EAAA,QACA,iBAAA;cAEI,OAAA,GAAU,YAAA;EAahB,MAAA,CACJ,GAAA,EAAK,YAAA,EACL,OAAA,GAAU,MAAA,EAAQ,WAAA,KAAgB,cAAA,sBAClC,OAAA,GAAU,YAAA,EACV,QAAA,YACC,OAAA;EAsCH,QAAA,CAAA;EAaA,cAAA,CAAA;EAAA,QAKc,uBAAA;EAAA,QA+EA,gBAAA;EAAA,QAiFA,6BAAA;EAAA,QAwGN,eAAA;EAAA,QA6BA,yBAAA;EAAA,QAaA,wBAAA;EAAA,QAwBA,eAAA;EAAA,QAYA,gBAAA;EAAA,QAUA,mBAAA;EAAA,QAuBA,wBAAA;EAAA,QA8BA,gBAAA;AAAA"}
|
|
@@ -6,6 +6,7 @@ import { streamDefaults } from "./defaults.js";
|
|
|
6
6
|
import { SSEErrorCode } from "./types.js";
|
|
7
7
|
import { StreamValidator } from "./validator.js";
|
|
8
8
|
import { SSEWriter } from "./sse-writer.js";
|
|
9
|
+
import { clearGraceTimer, clearRemovalTimer } from "./timers.js";
|
|
9
10
|
import { StreamRegistry } from "./stream-registry.js";
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { context } from "@opentelemetry/api";
|
|
@@ -67,7 +68,8 @@ var StreamManager = class {
|
|
|
67
68
|
}
|
|
68
69
|
} else this.sseWriter.writeBufferOverflowWarning(res, validEventId);
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
clearGraceTimer(streamEntry);
|
|
72
|
+
clearRemovalTimer(streamEntry);
|
|
71
73
|
streamEntry.clients.add(res);
|
|
72
74
|
streamEntry.lastAccess = Date.now();
|
|
73
75
|
const combinedSignal = this._combineSignals(streamEntry.abortController.signal, options?.userSignal);
|
|
@@ -83,14 +85,14 @@ var StreamManager = class {
|
|
|
83
85
|
streamEntry.clients.delete(res);
|
|
84
86
|
this.activeOperations.delete(streamOperation);
|
|
85
87
|
if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) this._scheduleGraceAbort(streamEntry);
|
|
86
|
-
|
|
87
|
-
if (streamEntry.clients.size === 0) this.streamRegistry.remove(streamEntry.streamId);
|
|
88
|
-
}, this.bufferTTL);
|
|
88
|
+
this._scheduleRemovalAfterTTL(streamEntry);
|
|
89
89
|
});
|
|
90
90
|
if (streamEntry.isCompleted) {
|
|
91
91
|
res.end();
|
|
92
92
|
this.activeOperations.delete(streamOperation);
|
|
93
93
|
clearInterval(heartbeat);
|
|
94
|
+
streamEntry.clients.delete(res);
|
|
95
|
+
this._scheduleRemovalAfterTTL(streamEntry);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
async _createNewStream(res, handler, options, ownerKey) {
|
|
@@ -128,6 +130,7 @@ var StreamManager = class {
|
|
|
128
130
|
this.activeOperations.delete(streamOperation);
|
|
129
131
|
streamEntry.clients.delete(res);
|
|
130
132
|
if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) this._scheduleGraceAbort(streamEntry);
|
|
133
|
+
this._scheduleRemovalAfterTTL(streamEntry);
|
|
131
134
|
});
|
|
132
135
|
await this._processGeneratorInBackground(streamEntry);
|
|
133
136
|
clearInterval(heartbeat);
|
|
@@ -155,10 +158,7 @@ var StreamManager = class {
|
|
|
155
158
|
this._broadcastEventsToClients(streamEntry, eventId, event);
|
|
156
159
|
streamEntry.lastAccess = Date.now();
|
|
157
160
|
}
|
|
158
|
-
streamEntry
|
|
159
|
-
this._clearGraceTimer(streamEntry);
|
|
160
|
-
this._closeAllClients(streamEntry);
|
|
161
|
-
this._cleanupStream(streamEntry);
|
|
161
|
+
this._finalizeStream(streamEntry);
|
|
162
162
|
} catch (error) {
|
|
163
163
|
const rawMsg = error instanceof Error ? error.message : "Internal server error";
|
|
164
164
|
const clientMsg = error instanceof AppKitError ? error.clientMessage : "Internal server error";
|
|
@@ -167,10 +167,7 @@ var StreamManager = class {
|
|
|
167
167
|
const errorCode = this._categorizeError(error);
|
|
168
168
|
if (errorCode === SSEErrorCode.STREAM_ABORTED) {
|
|
169
169
|
logger.info("Stream aborted by client (code=%s)", errorCode);
|
|
170
|
-
streamEntry
|
|
171
|
-
this._clearGraceTimer(streamEntry);
|
|
172
|
-
this._closeAllClients(streamEntry);
|
|
173
|
-
this._cleanupStream(streamEntry);
|
|
170
|
+
this._finalizeStream(streamEntry);
|
|
174
171
|
return;
|
|
175
172
|
}
|
|
176
173
|
logger.error("Stream execution failed: %s (code=%s upstreamCode=%s)", rawMsg, errorCode, upstreamCode ?? "n/a");
|
|
@@ -186,8 +183,7 @@ var StreamManager = class {
|
|
|
186
183
|
timestamp: Date.now()
|
|
187
184
|
});
|
|
188
185
|
this._broadcastErrorToClients(streamEntry, errorEventId, clientMsg, errorCode, true, upstreamCode);
|
|
189
|
-
streamEntry
|
|
190
|
-
this._clearGraceTimer(streamEntry);
|
|
186
|
+
this._finalizeStream(streamEntry);
|
|
191
187
|
}
|
|
192
188
|
});
|
|
193
189
|
}
|
|
@@ -215,11 +211,18 @@ var StreamManager = class {
|
|
|
215
211
|
if (closeClients) client.end();
|
|
216
212
|
}
|
|
217
213
|
}
|
|
214
|
+
_finalizeStream(streamEntry) {
|
|
215
|
+
streamEntry.isCompleted = true;
|
|
216
|
+
clearGraceTimer(streamEntry);
|
|
217
|
+
this._closeAllClients(streamEntry);
|
|
218
|
+
this._scheduleRemovalAfterTTL(streamEntry);
|
|
219
|
+
}
|
|
218
220
|
_closeAllClients(streamEntry) {
|
|
219
221
|
for (const client of streamEntry.clients) if (!client.writableEnded) client.end();
|
|
222
|
+
streamEntry.clients.clear();
|
|
220
223
|
}
|
|
221
224
|
_scheduleGraceAbort(streamEntry) {
|
|
222
|
-
|
|
225
|
+
clearGraceTimer(streamEntry);
|
|
223
226
|
const timer = setTimeout(() => {
|
|
224
227
|
streamEntry.disconnectGraceTimer = void 0;
|
|
225
228
|
if (streamEntry.clients.size === 0 && !streamEntry.isCompleted) streamEntry.abortController.abort(new DOMException("Client disconnected (grace expired)", "AbortError"));
|
|
@@ -227,16 +230,15 @@ var StreamManager = class {
|
|
|
227
230
|
timer.unref?.();
|
|
228
231
|
streamEntry.disconnectGraceTimer = timer;
|
|
229
232
|
}
|
|
230
|
-
|
|
231
|
-
if (streamEntry.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (streamEntry.clients.size === 0) setTimeout(() => {
|
|
238
|
-
if (streamEntry.clients.size === 0) this.streamRegistry.remove(streamEntry.streamId);
|
|
233
|
+
_scheduleRemovalAfterTTL(streamEntry) {
|
|
234
|
+
if (!streamEntry.isCompleted || streamEntry.clients.size > 0) return;
|
|
235
|
+
clearRemovalTimer(streamEntry);
|
|
236
|
+
streamEntry.lastAccess = Date.now();
|
|
237
|
+
streamEntry.removalTimer = setTimeout(() => {
|
|
238
|
+
streamEntry.removalTimer = void 0;
|
|
239
|
+
if (streamEntry.clients.size === 0 && Date.now() - streamEntry.lastAccess >= this.bufferTTL) this.streamRegistry.remove(streamEntry.streamId);
|
|
239
240
|
}, this.bufferTTL);
|
|
241
|
+
streamEntry.removalTimer.unref?.();
|
|
240
242
|
}
|
|
241
243
|
_categorizeError(error) {
|
|
242
244
|
if (error instanceof Error) {
|