@databricks/appkit 0.44.0 → 0.46.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/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/connectors/sql-warehouse/arrow-schema.js +283 -0
- package/dist/connectors/sql-warehouse/arrow-schema.js.map +1 -0
- package/dist/connectors/sql-warehouse/client.js +166 -60
- package/dist/connectors/sql-warehouse/client.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/errors/base.d.ts +18 -0
- package/dist/errors/base.d.ts.map +1 -1
- package/dist/errors/base.js +20 -0
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/execution.d.ts +34 -2
- package/dist/errors/execution.d.ts.map +1 -1
- package/dist/errors/execution.js +40 -5
- package/dist/errors/execution.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 +84 -8
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +296 -61
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/index.js +1 -1
- package/dist/plugins/analytics/result-delivery.js +287 -0
- package/dist/plugins/analytics/result-delivery.js.map +1 -0
- package/dist/plugins/analytics/types.d.ts +8 -0
- package/dist/plugins/analytics/types.d.ts.map +1 -1
- package/dist/plugins/analytics/types.js.map +1 -1
- package/dist/plugins/files/plugin.js +1 -1
- package/dist/plugins/genie/genie.js +1 -1
- package/dist/plugins/jobs/types.d.ts +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 +19 -8
- 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/shared/src/execute.d.ts +1 -3
- package/dist/shared/src/execute.d.ts.map +1 -1
- package/dist/shared/src/index.d.ts +1 -0
- package/dist/shared/src/plugin.d.ts +16 -0
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/sse/analytics.d.ts +1 -0
- package/dist/shared/src/sse/analytics.js +40 -0
- package/dist/shared/src/sse/analytics.js.map +1 -0
- package/dist/stream/arrow-stream-processor.js +93 -117
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/stream/buffers.js +1 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/defaults.js +1 -3
- package/dist/stream/defaults.js.map +1 -1
- package/dist/stream/sse-writer.js +3 -2
- package/dist/stream/sse-writer.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 +41 -33
- 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/query-registry.js +32 -2
- package/dist/type-generator/query-registry.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.AppKitError.md +40 -8
- package/docs/api/appkit/Class.AuthenticationError.md +56 -16
- package/docs/api/appkit/Class.ConfigurationError.md +57 -17
- package/docs/api/appkit/Class.ConnectionError.md +57 -17
- package/docs/api/appkit/Class.ExecutionError.md +80 -22
- package/docs/api/appkit/Class.InitializationError.md +55 -15
- package/docs/api/appkit/Class.Plugin.md +2 -0
- package/docs/api/appkit/Class.ResourceRegistry.md +1 -1
- package/docs/api/appkit/Class.ServerError.md +55 -15
- package/docs/api/appkit/Class.TunnelError.md +56 -16
- package/docs/api/appkit/Class.ValidationError.md +55 -15
- 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/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -6,7 +6,23 @@ import { JSONSchema7 } from "json-schema";
|
|
|
6
6
|
/** Base plugin interface. */
|
|
7
7
|
interface BasePlugin {
|
|
8
8
|
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* Cancel in-flight work (abort signals, SSE streams). Runs in the first
|
|
11
|
+
* phase of graceful shutdown, BEFORE any plugin's `shutdown()` hook —
|
|
12
|
+
* so it must not tear down shared resources (e.g. connection pools)
|
|
13
|
+
* that other plugins' hooks may still need. Put teardown in `shutdown()`.
|
|
14
|
+
*/
|
|
9
15
|
abortActiveOperations?(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Optional graceful-shutdown hook.
|
|
18
|
+
*
|
|
19
|
+
* Invoked by AppKit's core lifecycle manager during graceful shutdown
|
|
20
|
+
* (SIGTERM/SIGINT), after `abortActiveOperations()` has run. Use it to
|
|
21
|
+
* drain in-flight work or release resources. Each plugin's hook is bounded
|
|
22
|
+
* by a per-plugin timeout and runs concurrently with other plugins' hooks;
|
|
23
|
+
* errors are logged and never abort the shutdown.
|
|
24
|
+
*/
|
|
25
|
+
shutdown?(): Promise<void> | void;
|
|
10
26
|
setup(): Promise<void>;
|
|
11
27
|
injectRoutes(router: express.Router): void;
|
|
12
28
|
getEndpoints(): PluginEndpointMap;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../../shared/src/plugin.ts"],"mappings":";;;;;;UAeiB,UAAA;EACf,IAAA
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../../../shared/src/plugin.ts"],"mappings":";;;;;;UAeiB,UAAA;EACf,IAAA;;;;;;;EAQA,qBAAA;EA+BoB;;;;;;;;;EApBpB,QAAA,KAAa,OAAA;EAEb,KAAA,IAAS,OAAA;EAET,YAAA,CAAa,MAAA,EAAQ,OAAA,CAAQ,MAAA;EAE7B,YAAA,IAAgB,iBAAA;EAEhB,uBAAA,KAA4B,WAAA;EAE5B,OAAA;EAEA,YAAA,KAAiB,MAAA;EAJW;;;;EAU5B,aAAA,EAAe,IAAA;IACb,OAAA;IACA,eAAA,GAAkB,gBAAA;EAAA;AAAA;;UAKL,gBAAA;EACf,IAAA;EACA,IAAA;EAAA,CAEC,GAAA;EAMD,SAAA,GAAY,gBAAA;AAAA;AAAA,KAGF,gBAAA;EAGN,MAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,KAQM,WAAA;;;;;KAMA,iBAAA,KACN,gBAAA,YACM,UAAA,GAAa,UAAA,UAEvB,MAAA,EAAQ,CAAA,KACL,CAAA;EACH,cAAA,GAAiB,MAAA;EACjB,KAAA,GAAQ,WAAA;;;;AAPV;EAYE,QAAA,EAAU,cAAA;EAZiB;;;;EAiB3B,uBAAA,EAAyB,MAAA,EAAQ,CAAA,GAAI,mBAAA;AAAA;;;;;;;;;;UAYtB,cAAA,wCACP,IAAA,CACN,gBAAA;EAgBF,IAAA,EAAM,KAAA;EACN,SAAA;IACE,QAAA,EAAU,IAAA,CAAK,mBAAA;IACf,QAAA,EAAU,IAAA,CAAK,mBAAA;EAAA;EAEjB,MAAA;IACE,MAAA,EAAQ,WAAA;EAAA;EA9CF;EAiDR,WAAA;EA5CU;EA8CV,WAAA;EAzCiC;EA2CjC,MAAA;EA3CqC;EA6CrC,OAAA;EA7CwD;EA+CxD,UAAA;EAnC6B;EAqC7B,QAAA;EAnCE;EAqCF,OAAA;EAnBiB;EAqBjB,cAAA;EApBiB;EAsBjB,MAAA;EAnBU;EAqBV,SAAA;AAAA;;;;;;;;;;;;;;;UAiBe,mBAAA;EACf,IAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;EACA,UAAA;EACA,MAAA,EAAQ,MAAA,SAAe,kBAAA;EACvB,QAAA;AAAA;;;;AAoCF;;KAAY,aAAA,WAAwB,UAAA,IAClC,CAAA,sCAAqC,CAAA,GAAI,MAAA;;;;;;;;KAS/B,UAAA,QAAkB,GAAA,cAAgB,IAAA,mBAC1C,GAAA,GACA,GAAA;EAX+B;;;;;EAiB7B,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,GAAA;AAAA;;;;;;;;;KAWxB,SAAA,oBACS,UAAA,CAAW,iBAAA,gCAExB,CAAA,YAAa,CAAA,WAAY,UAAA,CAC7B,aAAA,CAAc,YAAA,CAAa,CAAA;;KAKnB,UAAA;EAAwB,MAAA,EAAQ,CAAA;EAAG,MAAA,EAAQ,CAAA;EAAG,IAAA,EAAM,CAAA;AAAA;;KAEpD,QAAA,4BACV,MAAA,GAAS,CAAA,KACN,UAAA,CAAW,CAAA,EAAG,CAAA,EAAG,CAAA;AAbtB;AAAA,KAgBY,UAAA,GAAa,OAAA,CAAQ,MAAA;AAAA,KACrB,YAAA,GAAe,OAAA,CAAQ,QAAA;AAAA,KACvB,WAAA,GAAc,OAAA,CAAQ,OAAA;AAAA,KAEtB,UAAA;AAAA,KAEA,WAAA;EAnBS,+DAqBnB,IAAA;EACA,MAAA,EAAQ,UAAA;EACR,IAAA;EACA,OAAA,GAAU,GAAA,EAAK,WAAA,EAAa,GAAA,EAAK,YAAA,KAAiB,OAAA,QAxBT;EA0BzC,eAAA;AAAA;;KAIU,iBAAA,GAAoB,MAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region ../shared/src/sse/analytics.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wire protocol for analytics SSE messages emitted by `/api/analytics/query`.
|
|
6
|
+
*
|
|
7
|
+
* The SSE channel carries only the JSON_ARRAY path (warehouse-readiness
|
|
8
|
+
* events + a `result` message of rows). ARROW_STREAM does NOT use SSE —
|
|
9
|
+
* the server streams the raw Arrow IPC bytes back on the query response body
|
|
10
|
+
* (`_handleArrowStreamQuery`) and the client reads them directly
|
|
11
|
+
* (`fetchArrowDirect`), so there is no `arrow` message type here.
|
|
12
|
+
*
|
|
13
|
+
* These schemas are the single source of truth for the JSON contract between
|
|
14
|
+
* the server (`AnalyticsPlugin._handleQueryRoute`) and the client
|
|
15
|
+
* (`useAnalyticsQuery`). Both sides validate with the same schema:
|
|
16
|
+
*
|
|
17
|
+
* - Server uses the typed builder (`makeResultMessage`) to construct messages
|
|
18
|
+
* with compile-time guarantees that all required fields are present.
|
|
19
|
+
* - Client calls `AnalyticsSseMessage.safeParse(JSON.parse(event.data))` to
|
|
20
|
+
* fail loudly on a malformed payload instead of silently treating an
|
|
21
|
+
* undefined field as data.
|
|
22
|
+
*/
|
|
23
|
+
/** Successful row-shaped result (JSON_ARRAY format, or empty results). */
|
|
24
|
+
const AnalyticsResultMessage = z.object({
|
|
25
|
+
type: z.literal("result"),
|
|
26
|
+
data: z.array(z.unknown()).optional(),
|
|
27
|
+
status: z.unknown().optional(),
|
|
28
|
+
statement_id: z.string().optional()
|
|
29
|
+
});
|
|
30
|
+
function makeResultMessage(data, extras = {}) {
|
|
31
|
+
return {
|
|
32
|
+
type: "result",
|
|
33
|
+
data,
|
|
34
|
+
...extras
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { makeResultMessage };
|
|
40
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.js","names":[],"sources":["../../../../../shared/src/sse/analytics.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Wire protocol for analytics SSE messages emitted by `/api/analytics/query`.\n *\n * The SSE channel carries only the JSON_ARRAY path (warehouse-readiness\n * events + a `result` message of rows). ARROW_STREAM does NOT use SSE —\n * the server streams the raw Arrow IPC bytes back on the query response body\n * (`_handleArrowStreamQuery`) and the client reads them directly\n * (`fetchArrowDirect`), so there is no `arrow` message type here.\n *\n * These schemas are the single source of truth for the JSON contract between\n * the server (`AnalyticsPlugin._handleQueryRoute`) and the client\n * (`useAnalyticsQuery`). Both sides validate with the same schema:\n *\n * - Server uses the typed builder (`makeResultMessage`) to construct messages\n * with compile-time guarantees that all required fields are present.\n * - Client calls `AnalyticsSseMessage.safeParse(JSON.parse(event.data))` to\n * fail loudly on a malformed payload instead of silently treating an\n * undefined field as data.\n */\n\n/** Successful row-shaped result (JSON_ARRAY format, or empty results). */\nexport const AnalyticsResultMessage = z.object({\n type: z.literal(\"result\"),\n // `data` is intentionally `z.array(z.unknown())` rather than a deep\n // row schema. Validating every row's keys for shape costs O(rows × cols)\n // CPU and main-thread blocking time on the *client* (the schema is\n // also reused for `safeParse` in `useAnalyticsQuery`); for large JSON\n // results that pushes hundreds of ms to seconds of TBT into the\n // render pipeline. The server constructs `data` via the typed\n // `makeResultMessage` builder, so the per-row shape is enforced at\n // the source, not at validation time. The TS-level interface below\n // narrows `data` to `Record<string, unknown>[]` for callers.\n data: z.array(z.unknown()).optional(),\n // Status is opaque metadata forwarded from the warehouse — keep it as\n // `unknown` so we don't bake the SDK's detailed shape into the contract.\n status: z.unknown().optional(),\n statement_id: z.string().optional(),\n});\n\n/**\n * TS-level shape of a successful row-shaped result message.\n *\n * **Kept in sync by hand** with `AnalyticsResultMessage` above. The Zod\n * schema is intentionally loose (`z.array(z.unknown())`) to keep client\n * validation cheap; this interface narrows `data` to\n * `Record<string, unknown>[]` so consumers don't have to cast at every\n * call site. If you add a field to the Zod schema, add it here too.\n */\nexport interface AnalyticsResultMessage {\n type: \"result\";\n data?: Record<string, unknown>[];\n status?: unknown;\n statement_id?: string;\n}\n\n/**\n * Every message the analytics SSE stream may emit. Currently only the\n * row-shaped `result` message (JSON_ARRAY path); `warehouse_status` and\n * `error` events are handled off-schema by the client. ARROW_STREAM never\n * uses SSE.\n */\nexport const AnalyticsSseMessage = AnalyticsResultMessage;\nexport type AnalyticsSseMessage = z.infer<typeof AnalyticsSseMessage>;\n\n// ────────────────────────────────────────────────────────────────────────────\n// Typed builder — call from the server route handler. The compiler enforces\n// that every required field is supplied, and the return type narrows so\n// downstream code (executeStream / SSE writer) keeps full type information.\n// ────────────────────────────────────────────────────────────────────────────\n\nexport function makeResultMessage(\n data: Record<string, unknown>[] | undefined,\n extras: { status?: unknown; statement_id?: string } = {},\n): AnalyticsResultMessage {\n return { type: \"result\", data, ...extras };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,yBAAyB,EAAE,OAAO;CAC7C,MAAM,EAAE,QAAQ,SAAS;CAUzB,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,UAAU;CAGrC,QAAQ,EAAE,SAAS,CAAC,UAAU;CAC9B,cAAc,EAAE,QAAQ,CAAC,UAAU;CACpC,CAAC;AAiCF,SAAgB,kBACd,MACA,SAAsD,EAAE,EAChC;AACxB,QAAO;EAAE,MAAM;EAAU;EAAM,GAAG;EAAQ"}
|
|
@@ -6,148 +6,124 @@ import "../errors/index.js";
|
|
|
6
6
|
//#region src/stream/arrow-stream-processor.ts
|
|
7
7
|
const logger = createLogger("stream:arrow");
|
|
8
8
|
const BACKOFF_MULTIPLIER = 1e3;
|
|
9
|
+
/**
|
|
10
|
+
* Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks
|
|
11
|
+
* without ever buffering a whole chunk — each chunk's response body is piped
|
|
12
|
+
* through as it arrives, so peak memory is a single network read rather than
|
|
13
|
+
* a full chunk (let alone the full result). No Arrow parsing on the server;
|
|
14
|
+
* the client parses the concatenated IPC bytes.
|
|
15
|
+
*/
|
|
9
16
|
var ArrowStreamProcessor = class ArrowStreamProcessor {
|
|
10
|
-
static DEFAULT_MAX_CONCURRENT_DOWNLOADS = 5;
|
|
11
17
|
static DEFAULT_TIMEOUT = 3e4;
|
|
12
18
|
static DEFAULT_RETRIES = 3;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
timeout: ArrowStreamProcessor.DEFAULT_TIMEOUT,
|
|
16
|
-
retries: ArrowStreamProcessor.DEFAULT_RETRIES
|
|
17
|
-
}) {
|
|
18
|
-
this.options = options;
|
|
19
|
+
options;
|
|
20
|
+
constructor(options) {
|
|
19
21
|
this.options = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
retries: options.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES
|
|
22
|
+
timeout: options?.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,
|
|
23
|
+
retries: options?.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* Stream Arrow chunks in array order, piping each chunk's response body.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* The client is responsible for parsing the IPC bytes.
|
|
34
|
-
*
|
|
35
|
-
* @param chunks - External links to Arrow IPC data
|
|
36
|
-
* @param schema - Schema from Databricks manifest
|
|
37
|
-
* @param signal - Optional abort signal
|
|
38
|
-
* @returns Raw concatenated IPC bytes with schema
|
|
29
|
+
* Yields network-sized pieces as they arrive (not whole chunks), so peak
|
|
30
|
+
* memory is one read buffer. Chunks are fetched sequentially; the
|
|
31
|
+
* concatenation of everything yielded is byte-identical to the raw Arrow
|
|
32
|
+
* result, so the client parses it exactly as a buffered response.
|
|
39
33
|
*/
|
|
40
|
-
async
|
|
34
|
+
async *streamChunks(chunks, signal, refresh) {
|
|
41
35
|
if (chunks.length === 0) throw ValidationError.missingField("chunks");
|
|
42
|
-
const
|
|
43
|
-
return {
|
|
44
|
-
data: this.concatenateBuffers(buffers),
|
|
45
|
-
schema
|
|
46
|
-
};
|
|
36
|
+
for (const chunk of chunks) yield* this.streamChunkBody(chunk, signal, refresh);
|
|
47
37
|
}
|
|
48
38
|
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
return await this.downloadChunkRaw(chunk, signal);
|
|
57
|
-
} finally {
|
|
58
|
-
semaphore.release();
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
return Promise.all(downloadPromises);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Download a single chunk as raw bytes with retry logic.
|
|
39
|
+
* Pipe one chunk's response body.
|
|
40
|
+
*
|
|
41
|
+
* Retry applies only while *establishing* the response (connection failure
|
|
42
|
+
* or non-2xx) — once bytes have been yielded downstream we cannot re-fetch,
|
|
43
|
+
* so a mid-body failure propagates and the caller aborts the response. An
|
|
44
|
+
* idle timeout, reset before every read, aborts a stalled download.
|
|
65
45
|
*/
|
|
66
|
-
async
|
|
67
|
-
let
|
|
46
|
+
async *streamChunkBody(chunk, signal, refresh) {
|
|
47
|
+
let externalLink = chunk.external_link;
|
|
48
|
+
if (!externalLink) throw ExecutionError.statementFailed(`External link missing for chunk ${chunk.chunk_index}`);
|
|
49
|
+
let response;
|
|
50
|
+
let controller;
|
|
51
|
+
let onOuterAbort;
|
|
52
|
+
let lastError;
|
|
68
53
|
for (let attempt = 0; attempt < this.options.retries; attempt++) {
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
54
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
55
|
+
const attemptController = new AbortController();
|
|
56
|
+
const listener = () => attemptController.abort();
|
|
57
|
+
signal?.addEventListener("abort", listener, { once: true });
|
|
58
|
+
const timer = setTimeout(() => attemptController.abort(), this.options.timeout);
|
|
74
59
|
try {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
83
|
-
return new Uint8Array(arrayBuffer);
|
|
60
|
+
const r = await fetch(externalLink, { signal: attemptController.signal });
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (!r.ok) throw ExecutionError.statementFailed(`Failed to download chunk ${chunk.chunk_index}: ${r.status} ${r.statusText}`);
|
|
63
|
+
response = r;
|
|
64
|
+
controller = attemptController;
|
|
65
|
+
onOuterAbort = listener;
|
|
66
|
+
break;
|
|
84
67
|
} catch (error) {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
signal?.removeEventListener("abort", listener);
|
|
85
70
|
lastError = error;
|
|
86
|
-
if (timeoutController.signal.aborted) lastError = /* @__PURE__ */ new Error(`Chunk ${chunk.chunk_index} download timed out after ${this.options.timeout}ms`);
|
|
87
71
|
if (signal?.aborted) throw ExecutionError.canceled();
|
|
88
|
-
if (attempt < this.options.retries - 1)
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
if (attempt < this.options.retries - 1) {
|
|
73
|
+
await this.delay(2 ** attempt * BACKOFF_MULTIPLIER, signal);
|
|
74
|
+
if (refresh && chunk.chunk_index != null) try {
|
|
75
|
+
const fresh = await refresh(chunk.chunk_index, signal);
|
|
76
|
+
if (fresh?.external_link) externalLink = fresh.external_link;
|
|
77
|
+
} catch (refreshError) {
|
|
78
|
+
logger.warn("Failed to re-resolve link for chunk %s: %O", chunk.chunk_index, refreshError);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
91
81
|
}
|
|
92
82
|
}
|
|
93
|
-
throw ExecutionError.statementFailed(`Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${lastError
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*/
|
|
115
|
-
combineAbortSignals(...signals) {
|
|
116
|
-
const controller = new AbortController();
|
|
117
|
-
for (const signal of signals) {
|
|
118
|
-
if (signal.aborted) {
|
|
119
|
-
controller.abort();
|
|
120
|
-
return controller.signal;
|
|
83
|
+
if (!response || !controller) throw ExecutionError.statementFailed(`Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
84
|
+
const body = response.body;
|
|
85
|
+
let idleTimer;
|
|
86
|
+
try {
|
|
87
|
+
if (!body) return;
|
|
88
|
+
const reader = body.getReader();
|
|
89
|
+
try {
|
|
90
|
+
while (true) {
|
|
91
|
+
idleTimer = setTimeout(() => controller.abort(), this.options.timeout);
|
|
92
|
+
let done;
|
|
93
|
+
let value;
|
|
94
|
+
try {
|
|
95
|
+
({done, value} = await reader.read());
|
|
96
|
+
} finally {
|
|
97
|
+
clearTimeout(idleTimer);
|
|
98
|
+
}
|
|
99
|
+
if (done) break;
|
|
100
|
+
if (value && value.byteLength > 0) yield value;
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
reader.cancel().catch(() => {});
|
|
121
104
|
}
|
|
122
|
-
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
107
|
+
logger.error("Failed streaming chunk %s body: %O", chunk.chunk_index, error);
|
|
108
|
+
throw error instanceof ExecutionError ? error : ExecutionError.statementFailed(`Failed streaming chunk ${chunk.chunk_index}: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(idleTimer);
|
|
111
|
+
if (onOuterAbort) signal?.removeEventListener("abort", onOuterAbort);
|
|
123
112
|
}
|
|
124
|
-
return controller.signal;
|
|
125
113
|
}
|
|
126
|
-
delay(ms) {
|
|
127
|
-
return new Promise((resolve) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (this.permits > 0) {
|
|
138
|
-
this.permits--;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
return new Promise((resolve) => {
|
|
142
|
-
this.waiting.push(resolve);
|
|
114
|
+
delay(ms, signal) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
signal?.removeEventListener("abort", onAbort);
|
|
118
|
+
resolve();
|
|
119
|
+
}, ms);
|
|
120
|
+
const onAbort = () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
reject(ExecutionError.canceled());
|
|
123
|
+
};
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
143
125
|
});
|
|
144
126
|
}
|
|
145
|
-
release() {
|
|
146
|
-
if (this.waiting.length > 0) {
|
|
147
|
-
const next = this.waiting.shift();
|
|
148
|
-
if (next) next();
|
|
149
|
-
} else this.permits++;
|
|
150
|
-
}
|
|
151
127
|
};
|
|
152
128
|
|
|
153
129
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrow-stream-processor.js","names":[],"sources":["../../src/stream/arrow-stream-processor.ts"],"sourcesContent":["import type { sql } from \"@databricks/sdk-experimental\";\nimport { ExecutionError, ValidationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"stream:arrow\");\n\ntype ResultManifest = sql.ResultManifest;\ntype ExternalLink = sql.ExternalLink;\n\ninterface ArrowStreamOptions {\n maxConcurrentDownloads: number;\n timeout: number;\n retries: number;\n}\n\n/**\n * Result from zero-copy Arrow chunk processing.\n * Contains raw IPC bytes without server-side parsing.\n */\ninterface ArrowRawResult {\n /** Concatenated raw Arrow IPC bytes */\n data: Uint8Array;\n /** Schema from Databricks manifest (not parsed from Arrow) */\n schema: ResultManifest[\"schema\"];\n}\n\nconst BACKOFF_MULTIPLIER = 1000;\n\nexport class ArrowStreamProcessor {\n static readonly DEFAULT_MAX_CONCURRENT_DOWNLOADS = 5;\n static readonly DEFAULT_TIMEOUT = 30000;\n static readonly DEFAULT_RETRIES = 3;\n\n constructor(\n private options: ArrowStreamOptions = {\n maxConcurrentDownloads:\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n timeout: ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n },\n ) {\n this.options = {\n maxConcurrentDownloads:\n options.maxConcurrentDownloads ??\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n timeout: options.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: options.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES,\n };\n }\n\n /**\n * Process Arrow chunks using zero-copy proxy pattern.\n *\n * Downloads raw IPC bytes from external links and concatenates them\n * without parsing into Arrow Tables on the server. This reduces:\n * - Memory usage by ~50% (no parsed Table representation)\n * - CPU usage (no tableFromIPC/tableToIPC calls)\n *\n * The client is responsible for parsing the IPC bytes.\n *\n * @param chunks - External links to Arrow IPC data\n * @param schema - Schema from Databricks manifest\n * @param signal - Optional abort signal\n * @returns Raw concatenated IPC bytes with schema\n */\n async processChunks(\n chunks: ExternalLink[],\n schema: ResultManifest[\"schema\"],\n signal?: AbortSignal,\n ): Promise<ArrowRawResult> {\n if (chunks.length === 0) {\n throw ValidationError.missingField(\"chunks\");\n }\n\n const buffers = await this.downloadChunksRaw(chunks, signal);\n const data = this.concatenateBuffers(buffers);\n\n return { data, schema };\n }\n\n /**\n * Download all chunks as raw bytes with concurrency control.\n */\n private async downloadChunksRaw(\n chunks: ExternalLink[],\n signal?: AbortSignal,\n ): Promise<Uint8Array[]> {\n const semaphore = new Semaphore(this.options.maxConcurrentDownloads);\n\n const downloadPromises = chunks.map(async (chunk) => {\n await semaphore.acquire();\n try {\n return await this.downloadChunkRaw(chunk, signal);\n } finally {\n semaphore.release();\n }\n });\n\n return Promise.all(downloadPromises);\n }\n\n /**\n * Download a single chunk as raw bytes with retry logic.\n */\n private async downloadChunkRaw(\n chunk: ExternalLink,\n signal?: AbortSignal,\n ): Promise<Uint8Array> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < this.options.retries; attempt++) {\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => {\n timeoutController.abort();\n }, this.options.timeout);\n\n const combinedSignal = signal\n ? this.combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const externalLink = chunk.external_link;\n if (!externalLink) {\n logger.error(\"External link is required for chunk: %O\", chunk);\n continue;\n }\n\n const response = await fetch(externalLink, {\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index}: ${response.status} ${response.statusText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n return new Uint8Array(arrayBuffer);\n } catch (error) {\n lastError = error as Error;\n\n if (timeoutController.signal.aborted) {\n lastError = new Error(\n `Chunk ${chunk.chunk_index} download timed out after ${this.options.timeout}ms`,\n );\n }\n\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n if (attempt < this.options.retries - 1) {\n await this.delay(2 ** attempt * BACKOFF_MULTIPLIER);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${lastError?.message}`,\n );\n }\n\n /**\n * Concatenate multiple Uint8Array buffers into a single buffer.\n * Pre-allocates the result array for efficiency.\n */\n private concatenateBuffers(buffers: Uint8Array[]): Uint8Array {\n if (buffers.length === 0) {\n throw ValidationError.missingField(\"buffers\");\n }\n\n if (buffers.length === 1) {\n return buffers[0];\n }\n\n const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);\n const result = new Uint8Array(totalLength);\n\n let offset = 0;\n for (const buffer of buffers) {\n result.set(buffer, offset);\n offset += buffer.length;\n }\n\n return result;\n }\n\n /**\n * Combines multiple AbortSignals into one.\n * The combined signal aborts when any of the input signals abort.\n */\n private combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), {\n once: true,\n });\n }\n\n return controller.signal;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nclass Semaphore {\n private permits: number;\n private waiting: (() => void)[] = [];\n\n constructor(permits: number) {\n this.permits = permits;\n }\n\n async acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n }\n\n release(): void {\n if (this.waiting.length > 0) {\n const next = this.waiting.shift();\n\n if (next) {\n next();\n }\n } else {\n this.permits++;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"arrow-stream-processor.js","names":[],"sources":["../../src/stream/arrow-stream-processor.ts"],"sourcesContent":["import type { sql } from \"@databricks/sdk-experimental\";\nimport { ExecutionError, ValidationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"stream:arrow\");\n\ntype ExternalLink = sql.ExternalLink;\n\n/**\n * Re-mint a chunk's pre-signed URL. DBSQL external links expire in <= 15 min,\n * so a large result whose tail chunks are reached after the earlier chunks\n * finish draining can hit an expired link; this fetches a fresh one for the\n * given chunk index. Created in the request's identity context (it captures the\n * caller's workspace client), so it stays valid even though streaming runs\n * outside that context. Returns `undefined` if the link can't be re-resolved.\n */\nexport type RefreshChunkLink = (\n chunkIndex: number,\n signal?: AbortSignal,\n) => Promise<ExternalLink | undefined>;\n\ninterface ArrowStreamOptions {\n /** Idle timeout (ms) for a single chunk: no progress → abort the download. */\n timeout: number;\n /** Attempts to establish a chunk's response before any bytes are yielded. */\n retries: number;\n}\n\nconst BACKOFF_MULTIPLIER = 1000;\n\n/**\n * Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks\n * without ever buffering a whole chunk — each chunk's response body is piped\n * through as it arrives, so peak memory is a single network read rather than\n * a full chunk (let alone the full result). No Arrow parsing on the server;\n * the client parses the concatenated IPC bytes.\n */\nexport class ArrowStreamProcessor {\n static readonly DEFAULT_TIMEOUT = 30000;\n static readonly DEFAULT_RETRIES = 3;\n\n private options: ArrowStreamOptions;\n\n constructor(options?: Partial<ArrowStreamOptions>) {\n this.options = {\n timeout: options?.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: options?.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES,\n };\n }\n\n /**\n * Stream Arrow chunks in array order, piping each chunk's response body.\n *\n * Yields network-sized pieces as they arrive (not whole chunks), so peak\n * memory is one read buffer. Chunks are fetched sequentially; the\n * concatenation of everything yielded is byte-identical to the raw Arrow\n * result, so the client parses it exactly as a buffered response.\n */\n async *streamChunks(\n chunks: ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n if (chunks.length === 0) {\n throw ValidationError.missingField(\"chunks\");\n }\n\n for (const chunk of chunks) {\n yield* this.streamChunkBody(chunk, signal, refresh);\n }\n }\n\n /**\n * Pipe one chunk's response body.\n *\n * Retry applies only while *establishing* the response (connection failure\n * or non-2xx) — once bytes have been yielded downstream we cannot re-fetch,\n * so a mid-body failure propagates and the caller aborts the response. An\n * idle timeout, reset before every read, aborts a stalled download.\n */\n private async *streamChunkBody(\n chunk: ExternalLink,\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n let externalLink = chunk.external_link;\n if (!externalLink) {\n // A missing link cannot be fixed by retrying — fail loudly.\n throw ExecutionError.statementFailed(\n `External link missing for chunk ${chunk.chunk_index}`,\n );\n }\n\n let response: Response | undefined;\n let controller: AbortController | undefined;\n let onOuterAbort: (() => void) | undefined;\n let lastError: unknown;\n\n for (let attempt = 0; attempt < this.options.retries; attempt++) {\n if (signal?.aborted) throw ExecutionError.canceled();\n\n const attemptController = new AbortController();\n const listener = () => attemptController.abort();\n signal?.addEventListener(\"abort\", listener, { once: true });\n const timer = setTimeout(\n () => attemptController.abort(),\n this.options.timeout,\n );\n\n try {\n const r = await fetch(externalLink, {\n signal: attemptController.signal,\n });\n clearTimeout(timer);\n if (!r.ok) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index}: ${r.status} ${r.statusText}`,\n );\n }\n // Keep this attempt's controller alive to drive the body read + idle\n // timeout below.\n response = r;\n controller = attemptController;\n onOuterAbort = listener;\n break;\n } catch (error) {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", listener);\n lastError = error;\n if (signal?.aborted) throw ExecutionError.canceled();\n if (attempt < this.options.retries - 1) {\n await this.delay(2 ** attempt * BACKOFF_MULTIPLIER, signal);\n // Pre-signed URLs expire (<= 15 min). Before retrying, re-mint this\n // chunk's link — a stale URL would just 403 again on the same address.\n // Only meaningful before any bytes are yielded (below), which is why\n // this lives in the establish-response loop.\n if (refresh && chunk.chunk_index != null) {\n try {\n const fresh = await refresh(chunk.chunk_index, signal);\n if (fresh?.external_link) externalLink = fresh.external_link;\n } catch (refreshError) {\n // Keep retrying the current URL; surface the original error if\n // all attempts fail.\n logger.warn(\n \"Failed to re-resolve link for chunk %s: %O\",\n chunk.chunk_index,\n refreshError,\n );\n }\n }\n }\n }\n }\n\n if (!response || !controller) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${\n lastError instanceof Error ? lastError.message : String(lastError)\n }`,\n );\n }\n\n const body = response.body;\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n try {\n if (!body) return; // empty chunk (0 rows)\n const reader = body.getReader();\n try {\n while (true) {\n // The idle timeout must cover ONLY the upstream read, never the\n // downstream `yield`. Arm it right before `reader.read()` and clear\n // it the instant the read resolves — if it stayed armed across the\n // yield, a slow client backpressuring `writeChunk` would look like an\n // upstream stall and abort a perfectly healthy download.\n idleTimer = setTimeout(\n () => controller.abort(),\n this.options.timeout,\n );\n let done: boolean;\n let value: Uint8Array | undefined;\n try {\n ({ done, value } = await reader.read());\n } finally {\n clearTimeout(idleTimer);\n }\n if (done) break;\n if (value && value.byteLength > 0) yield value;\n }\n } finally {\n // Release the stream on early exit (downstream abort / error).\n reader.cancel().catch(() => {});\n }\n } catch (error) {\n if (signal?.aborted) throw ExecutionError.canceled();\n logger.error(\n \"Failed streaming chunk %s body: %O\",\n chunk.chunk_index,\n error,\n );\n throw error instanceof ExecutionError\n ? error\n : ExecutionError.statementFailed(\n `Failed streaming chunk ${chunk.chunk_index}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n } finally {\n clearTimeout(idleTimer);\n if (onOuterAbort) signal?.removeEventListener(\"abort\", onOuterAbort);\n }\n }\n\n private delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n}\n"],"mappings":"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"}
|
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,10 +1,8 @@
|
|
|
1
1
|
//#region src/stream/defaults.ts
|
|
2
2
|
const streamDefaults = {
|
|
3
3
|
bufferSize: 100,
|
|
4
|
-
maxEventSize: 1024 * 1024,
|
|
4
|
+
maxEventSize: 1 * 1024 * 1024,
|
|
5
5
|
bufferTTL: 600 * 1e3,
|
|
6
|
-
cleanupInterval: 300 * 1e3,
|
|
7
|
-
maxPersistentBuffers: 1e4,
|
|
8
6
|
heartbeatInterval: 10 * 1e3,
|
|
9
7
|
maxActiveStreams: 1e3,
|
|
10
8
|
disconnectGraceMs: 15e3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../src/stream/defaults.ts"],"sourcesContent":["export const streamDefaults = {\n bufferSize: 100,\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"}
|
|
@@ -18,11 +18,12 @@ var SSEWriter = class {
|
|
|
18
18
|
res.write(`event: ${eventType}\n`);
|
|
19
19
|
res.write(`data: ${eventData}\n\n`);
|
|
20
20
|
}
|
|
21
|
-
writeError(res, eventId, error, code = SSEErrorCode.INTERNAL_ERROR) {
|
|
21
|
+
writeError(res, eventId, error, code = SSEErrorCode.INTERNAL_ERROR, errorCode) {
|
|
22
22
|
if (res.writableEnded) return;
|
|
23
23
|
const errorData = {
|
|
24
24
|
error,
|
|
25
|
-
code
|
|
25
|
+
code,
|
|
26
|
+
...errorCode ? { errorCode } : {}
|
|
26
27
|
};
|
|
27
28
|
res.write(`id: ${eventId}\n`);
|
|
28
29
|
res.write(`event: error\n`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.js","names":[],"sources":["../../src/stream/sse-writer.ts"],"sourcesContent":["import type { IAppResponse } from \"shared\";\nimport { streamDefaults } from \"./defaults\";\nimport {\n type BufferedEvent,\n type SSEError,\n SSEErrorCode,\n SSEWarningCode,\n} from \"./types\";\nimport { StreamValidator } from \"./validator\";\n\nexport class SSEWriter {\n // setup SSE headers\n setupHeaders(res: IAppResponse): void {\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n // X-Accel-Buffering: no — disables nginx-style proxy response buffering\n // for SSE (used by Cloudflare, AWS, GCP, and most corporate proxies).\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n // Intentionally NOT setting:\n // - Connection: keep-alive (HTTP/2 forbids it; Node manages keep-alive)\n // - Content-Encoding: none (invalid value; can trigger 502/RST in\n // strict intermediaries)\n res.flushHeaders?.();\n }\n\n // write a single event to the response\n writeEvent(res: IAppResponse, eventId: string, event: any): void {\n if (res.writableEnded) return;\n\n const eventType = StreamValidator.sanitizeEventType(event.type);\n const eventData = JSON.stringify(event);\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: ${eventType}\\n`);\n res.write(`data: ${eventData}\\n\\n`);\n }\n writeError(\n res: IAppResponse,\n eventId: string,\n error: string,\n code: SSEErrorCode = SSEErrorCode.INTERNAL_ERROR,\n ): void {\n if (res.writableEnded) return;\n\n const errorData: SSEError = {\n error,\n code,\n };\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: error\\n`);\n res.write(`data: ${JSON.stringify(errorData)}\\n\\n`);\n }\n\n // write a buffered event for replay\n writeBufferedEvent(res: IAppResponse, event: BufferedEvent): void {\n if (res.writableEnded) return;\n\n res.write(`id: ${event.id}\\n`);\n res.write(`event: ${event.type}\\n`);\n res.write(`data: ${event.data}\\n\\n`);\n }\n\n // write a buffer overflow warning\n writeBufferOverflowWarning(res: IAppResponse, lastEventId: string): void {\n if (res.writableEnded) return;\n\n try {\n res.write(`event: warning\\n`);\n res.write(\n `data: ${JSON.stringify({\n warning: \"Buffer overflow detected - some events were lost\",\n code: SSEWarningCode.BUFFER_OVERFLOW_RESTART,\n lastEventId,\n })}\\n\\n`,\n );\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n\n // start the heartbeat interval\n startHeartbeat(\n res: IAppResponse,\n signal: AbortSignal,\n interval?: number,\n ): NodeJS.Timeout {\n const heartbeatInterval = interval ?? streamDefaults.heartbeatInterval;\n\n return setInterval(() => {\n if (!signal.aborted && !res.writableEnded) {\n try {\n res.write(`: heartbeat\\n\\n`);\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n }, heartbeatInterval);\n }\n}\n"],"mappings":";;;;;AAUA,IAAa,YAAb,MAAuB;CAErB,aAAa,KAAyB;AACpC,MAAI,UAAU,gBAAgB,mCAAmC;AACjE,MAAI,UAAU,iBAAiB,yBAAyB;AAGxD,MAAI,UAAU,qBAAqB,KAAK;AAKxC,MAAI,gBAAgB;;CAItB,WAAW,KAAmB,SAAiB,OAAkB;AAC/D,MAAI,IAAI,cAAe;EAEvB,MAAM,YAAY,gBAAgB,kBAAkB,MAAM,KAAK;EAC/D,MAAM,YAAY,KAAK,UAAU,MAAM;AAEvC,MAAI,MAAM,OAAO,QAAQ,IAAI;AAC7B,MAAI,MAAM,UAAU,UAAU,IAAI;AAClC,MAAI,MAAM,SAAS,UAAU,MAAM;;CAErC,WACE,KACA,SACA,OACA,OAAqB,aAAa,
|
|
1
|
+
{"version":3,"file":"sse-writer.js","names":[],"sources":["../../src/stream/sse-writer.ts"],"sourcesContent":["import type { IAppResponse } from \"shared\";\nimport { streamDefaults } from \"./defaults\";\nimport {\n type BufferedEvent,\n type SSEError,\n SSEErrorCode,\n SSEWarningCode,\n} from \"./types\";\nimport { StreamValidator } from \"./validator\";\n\nexport class SSEWriter {\n // setup SSE headers\n setupHeaders(res: IAppResponse): void {\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n // X-Accel-Buffering: no — disables nginx-style proxy response buffering\n // for SSE (used by Cloudflare, AWS, GCP, and most corporate proxies).\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n // Intentionally NOT setting:\n // - Connection: keep-alive (HTTP/2 forbids it; Node manages keep-alive)\n // - Content-Encoding: none (invalid value; can trigger 502/RST in\n // strict intermediaries)\n res.flushHeaders?.();\n }\n\n // write a single event to the response\n writeEvent(res: IAppResponse, eventId: string, event: any): void {\n if (res.writableEnded) return;\n\n const eventType = StreamValidator.sanitizeEventType(event.type);\n const eventData = JSON.stringify(event);\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: ${eventType}\\n`);\n res.write(`data: ${eventData}\\n\\n`);\n }\n writeError(\n res: IAppResponse,\n eventId: string,\n error: string,\n code: SSEErrorCode = SSEErrorCode.INTERNAL_ERROR,\n errorCode?: string,\n ): void {\n if (res.writableEnded) return;\n\n const errorData: SSEError = {\n error,\n code,\n ...(errorCode ? { errorCode } : {}),\n };\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: error\\n`);\n res.write(`data: ${JSON.stringify(errorData)}\\n\\n`);\n }\n\n // write a buffered event for replay\n writeBufferedEvent(res: IAppResponse, event: BufferedEvent): void {\n if (res.writableEnded) return;\n\n res.write(`id: ${event.id}\\n`);\n res.write(`event: ${event.type}\\n`);\n res.write(`data: ${event.data}\\n\\n`);\n }\n\n // write a buffer overflow warning\n writeBufferOverflowWarning(res: IAppResponse, lastEventId: string): void {\n if (res.writableEnded) return;\n\n try {\n res.write(`event: warning\\n`);\n res.write(\n `data: ${JSON.stringify({\n warning: \"Buffer overflow detected - some events were lost\",\n code: SSEWarningCode.BUFFER_OVERFLOW_RESTART,\n lastEventId,\n })}\\n\\n`,\n );\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n\n // start the heartbeat interval\n startHeartbeat(\n res: IAppResponse,\n signal: AbortSignal,\n interval?: number,\n ): NodeJS.Timeout {\n const heartbeatInterval = interval ?? streamDefaults.heartbeatInterval;\n\n return setInterval(() => {\n if (!signal.aborted && !res.writableEnded) {\n try {\n res.write(`: heartbeat\\n\\n`);\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n }, heartbeatInterval);\n }\n}\n"],"mappings":";;;;;AAUA,IAAa,YAAb,MAAuB;CAErB,aAAa,KAAyB;AACpC,MAAI,UAAU,gBAAgB,mCAAmC;AACjE,MAAI,UAAU,iBAAiB,yBAAyB;AAGxD,MAAI,UAAU,qBAAqB,KAAK;AAKxC,MAAI,gBAAgB;;CAItB,WAAW,KAAmB,SAAiB,OAAkB;AAC/D,MAAI,IAAI,cAAe;EAEvB,MAAM,YAAY,gBAAgB,kBAAkB,MAAM,KAAK;EAC/D,MAAM,YAAY,KAAK,UAAU,MAAM;AAEvC,MAAI,MAAM,OAAO,QAAQ,IAAI;AAC7B,MAAI,MAAM,UAAU,UAAU,IAAI;AAClC,MAAI,MAAM,SAAS,UAAU,MAAM;;CAErC,WACE,KACA,SACA,OACA,OAAqB,aAAa,gBAClC,WACM;AACN,MAAI,IAAI,cAAe;EAEvB,MAAM,YAAsB;GAC1B;GACA;GACA,GAAI,YAAY,EAAE,WAAW,GAAG,EAAE;GACnC;AAED,MAAI,MAAM,OAAO,QAAQ,IAAI;AAC7B,MAAI,MAAM,iBAAiB;AAC3B,MAAI,MAAM,SAAS,KAAK,UAAU,UAAU,CAAC,MAAM;;CAIrD,mBAAmB,KAAmB,OAA4B;AAChE,MAAI,IAAI,cAAe;AAEvB,MAAI,MAAM,OAAO,MAAM,GAAG,IAAI;AAC9B,MAAI,MAAM,UAAU,MAAM,KAAK,IAAI;AACnC,MAAI,MAAM,SAAS,MAAM,KAAK,MAAM;;CAItC,2BAA2B,KAAmB,aAA2B;AACvE,MAAI,IAAI,cAAe;AAEvB,MAAI;AACF,OAAI,MAAM,mBAAmB;AAC7B,OAAI,MACF,SAAS,KAAK,UAAU;IACtB,SAAS;IACT,MAAM,eAAe;IACrB;IACD,CAAC,CAAC,MACJ;WACM,QAAQ;;CAMnB,eACE,KACA,QACA,UACgB;EAChB,MAAM,oBAAoB,YAAY,eAAe;AAErD,SAAO,kBAAkB;AACvB,OAAI,CAAC,OAAO,WAAW,CAAC,IAAI,cAC1B,KAAI;AACF,QAAI,MAAM,kBAAkB;YACrB,QAAQ;KAIlB,kBAAkB"}
|
|
@@ -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"}
|