@hypequery/clickhouse 2.7.0 → 2.8.1
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/dist/core/adapters/clickhouse-adapter.d.ts +12 -4
- package/dist/core/adapters/clickhouse-adapter.d.ts.map +1 -1
- package/dist/core/adapters/clickhouse-adapter.js +42 -37
- package/dist/core/adapters/database-adapter.d.ts +2 -0
- package/dist/core/adapters/database-adapter.d.ts.map +1 -1
- package/dist/core/cache/cache-manager.d.ts.map +1 -1
- package/dist/core/cache/cache-manager.js +59 -18
- package/dist/core/cache/controller.d.ts.map +1 -1
- package/dist/core/cache/controller.js +15 -7
- package/dist/core/cache/mutation-coordinator.d.ts +20 -0
- package/dist/core/cache/mutation-coordinator.d.ts.map +1 -0
- package/dist/core/cache/mutation-coordinator.js +56 -0
- package/dist/core/cache/runtime-context.d.ts +4 -1
- package/dist/core/cache/runtime-context.d.ts.map +1 -1
- package/dist/core/cache/runtime-context.js +2 -0
- package/dist/core/cache/shared-fetch.d.ts +14 -0
- package/dist/core/cache/shared-fetch.d.ts.map +1 -0
- package/dist/core/cache/shared-fetch.js +43 -0
- package/dist/core/features/executor.d.ts +5 -1
- package/dist/core/features/executor.d.ts.map +1 -1
- package/dist/core/features/executor.js +3 -1
- package/dist/core/features/insert-executor.d.ts +1 -0
- package/dist/core/features/insert-executor.d.ts.map +1 -1
- package/dist/core/features/insert-executor.js +1 -0
- package/dist/core/insert-builder.d.ts +1 -0
- package/dist/core/insert-builder.d.ts.map +1 -1
- package/dist/core/query-builder.d.ts +26 -3
- package/dist/core/query-builder.d.ts.map +1 -1
- package/dist/core/query-builder.js +4 -4
- package/dist/core/utils/abort.d.ts +5 -0
- package/dist/core/utils/abort.d.ts.map +1 -0
- package/dist/core/utils/abort.js +42 -0
- package/dist/core/utils/abortable-result.d.ts +7 -0
- package/dist/core/utils/abortable-result.d.ts.map +1 -0
- package/dist/core/utils/abortable-result.js +27 -0
- package/dist/core/utils/clickhouse-adapter-config.d.ts +7 -0
- package/dist/core/utils/clickhouse-adapter-config.d.ts.map +1 -0
- package/dist/core/utils/clickhouse-adapter-config.js +20 -0
- package/dist/core/utils/integer-json-encoding.d.ts +18 -0
- package/dist/core/utils/integer-json-encoding.d.ts.map +1 -0
- package/dist/core/utils/integer-json-encoding.js +47 -0
- package/dist/core/utils/streaming-helpers.d.ts +1 -1
- package/dist/core/utils/streaming-helpers.d.ts.map +1 -1
- package/dist/core/utils/streaming-helpers.js +72 -26
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { DatabaseAdapter, QueryExecutionOptions, InsertExecutionOptions, InsertResultSummary } from './database-adapter.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ClickHouseAdapterConfig } from '../query-builder.js';
|
|
3
3
|
export declare class ClickHouseAdapter implements DatabaseAdapter {
|
|
4
|
-
private config;
|
|
5
4
|
readonly name = "clickhouse";
|
|
6
5
|
readonly namespace?: string;
|
|
7
6
|
private client;
|
|
8
|
-
|
|
7
|
+
/** Connection-level settings, so they can outrank the adapter's own defaults. */
|
|
8
|
+
private readonly configSettings?;
|
|
9
|
+
private readonly integerJsonEncoding;
|
|
10
|
+
constructor(config: ClickHouseAdapterConfig);
|
|
11
|
+
/**
|
|
12
|
+
* Converts only rejections of the adapter-owned precision setting into an
|
|
13
|
+
* actionable configuration error. Caller-owned settings and unrelated
|
|
14
|
+
* readonly failures retain their original error identity.
|
|
15
|
+
*/
|
|
16
|
+
private withReadonlyGuidance;
|
|
9
17
|
query<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<T[]>;
|
|
10
18
|
stream<T>(sql: string, params?: unknown[], options?: QueryExecutionOptions): Promise<ReadableStream<T[]>>;
|
|
11
19
|
insert<T extends Record<string, unknown>>(table: string, rows: T[], options?: InsertExecutionOptions): Promise<InsertResultSummary>;
|
|
12
20
|
close(): Promise<void>;
|
|
13
21
|
render(sql: string, params?: unknown[]): string;
|
|
14
22
|
}
|
|
15
|
-
export declare function createClickHouseAdapter(config:
|
|
23
|
+
export declare function createClickHouseAdapter(config: ClickHouseAdapterConfig): DatabaseAdapter;
|
|
16
24
|
//# sourceMappingURL=clickhouse-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickhouse-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/clickhouse-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"clickhouse-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/clickhouse-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EACV,uBAAuB,EAExB,MAAM,qBAAqB,CAAC;AA2B7B,qBAAa,iBAAkB,YAAW,eAAe;IACvD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAmB;IACjC,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsB;gBAE9C,MAAM,EAAE,uBAAuB;IAO3C;;;;OAIG;YACW,oBAAoB;IAqB5B,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAwB5F,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;IAkB7G,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,mBAAmB,CAAC;IAyBzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,MAAM;CAGpD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,uBAAuB,GAAG,eAAe,CAExF"}
|
|
@@ -1,64 +1,68 @@
|
|
|
1
|
-
import { isClientConfig } from '../query-builder.js';
|
|
2
1
|
import { substituteParameters } from '../utils.js';
|
|
3
|
-
import { getConnectionEndpoint } from '../utils/connection-endpoint.js';
|
|
4
2
|
import { createJsonEachRowStream } from '../utils/streaming-helpers.js';
|
|
5
|
-
import { getAutoClientModule } from '../env/auto-client.js';
|
|
6
3
|
import { assertSafeInsertIdentifiers } from '../utils/insert-identifiers.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const clientModule = getAutoClientModule();
|
|
12
|
-
return clientModule.createClient(config);
|
|
13
|
-
}
|
|
14
|
-
function deriveNamespace(config) {
|
|
15
|
-
if ('client' in config && config.client) {
|
|
16
|
-
return 'client';
|
|
17
|
-
}
|
|
18
|
-
const endpoint = getConnectionEndpoint(config);
|
|
19
|
-
const database = 'database' in config ? config.database : 'default';
|
|
20
|
-
const username = 'username' in config ? config.username : 'default';
|
|
21
|
-
return `${endpoint || 'unknown-host'}|${database || 'default'}|${username || 'default'}`;
|
|
22
|
-
}
|
|
23
|
-
function jsonOutputSettings(settings) {
|
|
24
|
-
return {
|
|
25
|
-
// Matches generated Int64+ result types and prevents JSON.parse precision loss.
|
|
26
|
-
output_format_json_quote_64bit_integers: 1,
|
|
27
|
-
...settings,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
4
|
+
import { consumeResultWithAbort } from '../utils/abortable-result.js';
|
|
5
|
+
import { throwIfAborted } from '../utils/abort.js';
|
|
6
|
+
import { createClickHouseClient, deriveClickHouseNamespace, } from '../utils/clickhouse-adapter-config.js';
|
|
7
|
+
import { buildIntegerJsonSettings, createReadonlyIntegerJsonError, } from '../utils/integer-json-encoding.js';
|
|
30
8
|
export class ClickHouseAdapter {
|
|
31
|
-
config;
|
|
32
9
|
name = 'clickhouse';
|
|
33
10
|
namespace;
|
|
34
11
|
client;
|
|
12
|
+
/** Connection-level settings, so they can outrank the adapter's own defaults. */
|
|
13
|
+
configSettings;
|
|
14
|
+
integerJsonEncoding;
|
|
35
15
|
constructor(config) {
|
|
36
|
-
this.
|
|
37
|
-
this.namespace = deriveNamespace(config);
|
|
16
|
+
this.namespace = deriveClickHouseNamespace(config);
|
|
38
17
|
this.client = createClickHouseClient(config);
|
|
18
|
+
this.configSettings = config.clickhouse_settings;
|
|
19
|
+
this.integerJsonEncoding = config.integerJsonEncoding ?? 'quoted';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts only rejections of the adapter-owned precision setting into an
|
|
23
|
+
* actionable configuration error. Caller-owned settings and unrelated
|
|
24
|
+
* readonly failures retain their original error identity.
|
|
25
|
+
*/
|
|
26
|
+
async withReadonlyGuidance(optionSettings, send) {
|
|
27
|
+
const attempt = buildIntegerJsonSettings(this.integerJsonEncoding, this.configSettings, optionSettings);
|
|
28
|
+
try {
|
|
29
|
+
return await send(attempt.settings);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const guidanceError = createReadonlyIntegerJsonError(error, attempt.adapterDefaultApplied);
|
|
33
|
+
throw guidanceError ?? error;
|
|
34
|
+
}
|
|
39
35
|
}
|
|
40
36
|
async query(sql, params = [], options) {
|
|
37
|
+
// The ClickHouse clients never check an already-aborted signal, so fail before sending anything.
|
|
38
|
+
throwIfAborted(options?.abortSignal);
|
|
41
39
|
const finalSQL = substituteParameters(sql, params);
|
|
42
|
-
const result = await this.client.query({
|
|
40
|
+
const result = await this.withReadonlyGuidance(options?.clickhouseSettings, clickhouseSettings => this.client.query({
|
|
43
41
|
query: finalSQL,
|
|
44
42
|
format: 'JSONEachRow',
|
|
45
|
-
clickhouse_settings:
|
|
43
|
+
clickhouse_settings: clickhouseSettings,
|
|
46
44
|
query_id: options?.queryId,
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
abort_signal: options?.abortSignal,
|
|
46
|
+
}));
|
|
47
|
+
// The web client types `json()` as the union of every format's shape; with
|
|
48
|
+
// `format: 'JSONEachRow'` it is always a row array.
|
|
49
|
+
return consumeResultWithAbort(options?.abortSignal, result, () => result.json());
|
|
49
50
|
}
|
|
50
51
|
async stream(sql, params = [], options) {
|
|
52
|
+
throwIfAborted(options?.abortSignal);
|
|
51
53
|
const finalSQL = substituteParameters(sql, params);
|
|
52
|
-
const result = await this.client.query({
|
|
54
|
+
const result = await this.withReadonlyGuidance(options?.clickhouseSettings, clickhouseSettings => this.client.query({
|
|
53
55
|
query: finalSQL,
|
|
54
56
|
format: 'JSONEachRow',
|
|
55
|
-
clickhouse_settings:
|
|
57
|
+
clickhouse_settings: clickhouseSettings,
|
|
56
58
|
query_id: options?.queryId,
|
|
57
|
-
|
|
59
|
+
abort_signal: options?.abortSignal,
|
|
60
|
+
}));
|
|
58
61
|
const stream = result.stream();
|
|
59
|
-
return createJsonEachRowStream(stream);
|
|
62
|
+
return createJsonEachRowStream(stream, options?.abortSignal);
|
|
60
63
|
}
|
|
61
64
|
async insert(table, rows, options) {
|
|
65
|
+
throwIfAborted(options?.abortSignal);
|
|
62
66
|
assertSafeInsertIdentifiers(table, options?.columns);
|
|
63
67
|
const result = await this.client.insert({
|
|
64
68
|
table,
|
|
@@ -73,6 +77,7 @@ export class ClickHouseAdapter {
|
|
|
73
77
|
...options?.clickhouseSettings,
|
|
74
78
|
},
|
|
75
79
|
query_id: options?.queryId,
|
|
80
|
+
abort_signal: options?.abortSignal,
|
|
76
81
|
});
|
|
77
82
|
return {
|
|
78
83
|
queryId: result.query_id,
|
|
@@ -2,10 +2,12 @@ import type { ClickHouseSettings, ClickHouseSummary } from '@clickhouse/client-c
|
|
|
2
2
|
export interface QueryExecutionOptions {
|
|
3
3
|
clickhouseSettings?: ClickHouseSettings;
|
|
4
4
|
queryId?: string;
|
|
5
|
+
abortSignal?: AbortSignal;
|
|
5
6
|
}
|
|
6
7
|
export interface InsertExecutionOptions {
|
|
7
8
|
clickhouseSettings?: ClickHouseSettings;
|
|
8
9
|
queryId?: string;
|
|
10
|
+
abortSignal?: AbortSignal;
|
|
9
11
|
/** Explicit column subset; adapters must preserve it so omitted columns take table DEFAULTs. */
|
|
10
12
|
columns?: string[];
|
|
11
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/database-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEvF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"database-adapter.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/database-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEvF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,mFAAmF;IACnF,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACzF,sFAAsF;IACtF,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3G;;;;;OAKG;IACH,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChC,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;CAClD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../../src/core/cache/cache-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../../src/core/cache/cache-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AA8DnF,wBAAsB,gBAAgB,CACpC,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,eAAe,EAE7B,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,EACpC,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAyM5B"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { computeCacheKey } from './key.js';
|
|
2
2
|
import { mergeCacheOptions } from './runtime-context.js';
|
|
3
|
+
import { joinSharedFetch } from './shared-fetch.js';
|
|
4
|
+
import { raceWithAbort, throwIfAborted } from '../utils/abort.js';
|
|
3
5
|
import { logger } from '../utils/logger.js';
|
|
4
6
|
function isCacheable(options) {
|
|
5
7
|
const ttl = options.ttlMs ?? 0;
|
|
@@ -33,6 +35,7 @@ async function logCacheHit({ sql, parameters, status, cacheKey, options, rowCoun
|
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
export async function executeWithCache(builder, options) {
|
|
38
|
+
throwIfAborted(options?.abortSignal);
|
|
36
39
|
const runtime = builder.getRuntimeContext();
|
|
37
40
|
const provider = runtime.provider;
|
|
38
41
|
const normalizedExecuteCache = options?.cache === false
|
|
@@ -56,7 +59,7 @@ export async function executeWithCache(builder, options) {
|
|
|
56
59
|
version: runtime.versionTag,
|
|
57
60
|
tableName
|
|
58
61
|
});
|
|
59
|
-
const entry = await activeProvider.get(key);
|
|
62
|
+
const entry = await raceWithAbort(activeProvider.get(key), options?.abortSignal);
|
|
60
63
|
if (!entry) {
|
|
61
64
|
runtime.parsedValues.delete(key);
|
|
62
65
|
}
|
|
@@ -65,13 +68,14 @@ export async function executeWithCache(builder, options) {
|
|
|
65
68
|
const deserialize = mergedOptions.deserialize || runtime.deserialize;
|
|
66
69
|
const serialize = mergedOptions.serialize || runtime.serialize;
|
|
67
70
|
const respondFromCache = async (cacheEntry, status) => {
|
|
71
|
+
throwIfAborted(options?.abortSignal);
|
|
68
72
|
const memoized = runtime.parsedValues.get(key);
|
|
69
73
|
let rows;
|
|
70
74
|
if (memoized && memoized.createdAt === cacheEntry.createdAt) {
|
|
71
75
|
rows = memoized.rows;
|
|
72
76
|
}
|
|
73
77
|
else {
|
|
74
|
-
rows = await deserialize(cacheEntry.value);
|
|
78
|
+
rows = await raceWithAbort(Promise.resolve().then(() => deserialize(cacheEntry.value)), options?.abortSignal);
|
|
75
79
|
runtime.parsedValues.set(key, { createdAt: cacheEntry.createdAt, rows, tags: cacheEntry.tags });
|
|
76
80
|
}
|
|
77
81
|
const cacheAge = Date.now() - cacheEntry.createdAt;
|
|
@@ -81,7 +85,7 @@ export async function executeWithCache(builder, options) {
|
|
|
81
85
|
else if (status === 'stale-hit') {
|
|
82
86
|
runtime.stats.staleHits += 1;
|
|
83
87
|
}
|
|
84
|
-
await logCacheHit({
|
|
88
|
+
await raceWithAbort(logCacheHit({
|
|
85
89
|
sql,
|
|
86
90
|
parameters,
|
|
87
91
|
status,
|
|
@@ -90,7 +94,7 @@ export async function executeWithCache(builder, options) {
|
|
|
90
94
|
rowCount: cacheEntry.rowCount ?? rows.length,
|
|
91
95
|
ageMs: cacheAge,
|
|
92
96
|
queryId: options?.queryId
|
|
93
|
-
});
|
|
97
|
+
}), options?.abortSignal);
|
|
94
98
|
return rows;
|
|
95
99
|
};
|
|
96
100
|
if (mode === 'cache-first') {
|
|
@@ -98,7 +102,7 @@ export async function executeWithCache(builder, options) {
|
|
|
98
102
|
return respondFromCache(entry, 'hit');
|
|
99
103
|
}
|
|
100
104
|
runtime.stats.misses += 1;
|
|
101
|
-
return fetchAndStore('miss');
|
|
105
|
+
return fetchAndStore('miss', options?.abortSignal);
|
|
102
106
|
}
|
|
103
107
|
if (mode === 'stale-while-revalidate') {
|
|
104
108
|
if (entry && fresh) {
|
|
@@ -109,14 +113,18 @@ export async function executeWithCache(builder, options) {
|
|
|
109
113
|
return respondFromCache(entry, 'stale-hit');
|
|
110
114
|
}
|
|
111
115
|
runtime.stats.misses += 1;
|
|
112
|
-
return fetchAndStore('miss');
|
|
116
|
+
return fetchAndStore('miss', options?.abortSignal);
|
|
113
117
|
}
|
|
114
118
|
if (mode === 'network-first') {
|
|
115
119
|
try {
|
|
116
120
|
runtime.stats.misses += 1;
|
|
117
|
-
return await fetchAndStore('miss');
|
|
121
|
+
return await fetchAndStore('miss', options?.abortSignal);
|
|
118
122
|
}
|
|
119
123
|
catch (error) {
|
|
124
|
+
// An abort is a caller decision, not a network failure staleIfError should mask.
|
|
125
|
+
if (options?.abortSignal?.aborted) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
120
128
|
if (mergedOptions.staleIfError && entry && staleAcceptable) {
|
|
121
129
|
return respondFromCache(entry, 'stale-hit');
|
|
122
130
|
}
|
|
@@ -124,16 +132,43 @@ export async function executeWithCache(builder, options) {
|
|
|
124
132
|
}
|
|
125
133
|
}
|
|
126
134
|
return runWithoutCache('bypass');
|
|
127
|
-
async function fetchAndStore(cacheStatus) {
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
async function fetchAndStore(cacheStatus, abortSignal) {
|
|
136
|
+
// A pre-aborted creator would otherwise start a zero-waiter fetch nobody can cancel.
|
|
137
|
+
throwIfAborted(abortSignal);
|
|
138
|
+
if (mergedOptions.dedupe === false) {
|
|
139
|
+
return runFetch(cacheStatus, abortSignal);
|
|
140
|
+
}
|
|
141
|
+
// A cancelled fetch lingers in the map until its promise settles.
|
|
142
|
+
let shared = runtime.inFlight.get(key);
|
|
143
|
+
if (!shared || shared.controller.signal.aborted) {
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const created = {
|
|
146
|
+
promise: runFetch(cacheStatus, controller.signal),
|
|
147
|
+
controller,
|
|
148
|
+
abortableWaiters: 0,
|
|
149
|
+
pinnedWaiters: 0
|
|
150
|
+
};
|
|
151
|
+
runtime.inFlight.set(key, created);
|
|
152
|
+
created.promise
|
|
153
|
+
.catch(() => undefined)
|
|
154
|
+
.finally(() => {
|
|
155
|
+
if (runtime.inFlight.get(key) === created) {
|
|
156
|
+
runtime.inFlight.delete(key);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
shared = created;
|
|
130
160
|
}
|
|
131
|
-
|
|
161
|
+
return joinSharedFetch(shared, abortSignal);
|
|
162
|
+
}
|
|
163
|
+
function runFetch(cacheStatus, abortSignal) {
|
|
164
|
+
return (async () => {
|
|
132
165
|
const rows = await builder.getExecutor().execute({
|
|
133
166
|
queryId: options?.queryId,
|
|
167
|
+
abortSignal,
|
|
134
168
|
logContext: { cacheStatus, cacheKey: key, cacheMode: mode }
|
|
135
169
|
});
|
|
136
|
-
|
|
170
|
+
throwIfAborted(abortSignal);
|
|
171
|
+
const encoded = await raceWithAbort(Promise.resolve().then(() => serialize(rows)), abortSignal);
|
|
137
172
|
const ttlMs = mergedOptions.ttlMs ?? 0;
|
|
138
173
|
const staleTtlMs = mergedOptions.staleTtlMs ?? 0;
|
|
139
174
|
const cacheTimeMs = mergedOptions.cacheTimeMs ?? ttlMs + staleTtlMs;
|
|
@@ -150,18 +185,23 @@ export async function executeWithCache(builder, options) {
|
|
|
150
185
|
byteSize: encoded.byteSize,
|
|
151
186
|
sqlFingerprint: key
|
|
152
187
|
};
|
|
153
|
-
|
|
188
|
+
const write = runtime.mutations.enqueueWrite({
|
|
189
|
+
key,
|
|
190
|
+
namespace,
|
|
191
|
+
tags: newEntry.tags || []
|
|
192
|
+
}, async () => {
|
|
193
|
+
// A queued write whose caller was cancelled before it started is safe to skip.
|
|
194
|
+
throwIfAborted(abortSignal);
|
|
195
|
+
await activeProvider.set(key, newEntry);
|
|
196
|
+
});
|
|
197
|
+
await raceWithAbort(write, abortSignal);
|
|
154
198
|
runtime.parsedValues.set(key, { createdAt: newEntry.createdAt, rows, tags: newEntry.tags });
|
|
155
199
|
return rows;
|
|
156
200
|
})();
|
|
157
|
-
if (mergedOptions.dedupe !== false) {
|
|
158
|
-
runtime.inFlight.set(key, promise);
|
|
159
|
-
promise.finally(() => runtime.inFlight.delete(key));
|
|
160
|
-
}
|
|
161
|
-
return promise;
|
|
162
201
|
}
|
|
163
202
|
function scheduleRevalidation() {
|
|
164
203
|
runtime.stats.revalidations += 1;
|
|
204
|
+
// Background refresh serves later callers, so it must outlive the aborting one.
|
|
165
205
|
fetchAndStore('revalidate').catch(() => undefined);
|
|
166
206
|
}
|
|
167
207
|
function runWithoutCache(cacheStatus) {
|
|
@@ -170,6 +210,7 @@ export async function executeWithCache(builder, options) {
|
|
|
170
210
|
}
|
|
171
211
|
return builder.getExecutor().execute({
|
|
172
212
|
queryId: options?.queryId,
|
|
213
|
+
abortSignal: options?.abortSignal,
|
|
173
214
|
logContext: { cacheStatus, cacheMode: mode }
|
|
174
215
|
});
|
|
175
216
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/core/cache/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,qBAAa,eAAe;IACd,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,mBAAmB;IAE1C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/core/cache/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,qBAAa,eAAe;IACd,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,mBAAmB;IAE1C,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzC,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,QAAQ,IAAI,UAAU,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE;IAO5C,OAAO,CAAC,wBAAwB;CAWjC"}
|
|
@@ -5,15 +5,17 @@ export class CacheController {
|
|
|
5
5
|
this.context = context;
|
|
6
6
|
}
|
|
7
7
|
async invalidateKey(key) {
|
|
8
|
+
const provider = this.context.provider;
|
|
9
|
+
if (provider) {
|
|
10
|
+
await this.context.mutations.invalidate(scope => scope.key === key, () => provider.delete(key));
|
|
11
|
+
}
|
|
8
12
|
this.context.parsedValues.delete(key);
|
|
9
|
-
if (!this.context.provider)
|
|
10
|
-
return;
|
|
11
|
-
await this.context.provider.delete(key);
|
|
12
13
|
}
|
|
13
14
|
async invalidateTags(tags) {
|
|
14
15
|
if (!tags.length)
|
|
15
16
|
return;
|
|
16
|
-
const
|
|
17
|
+
const provider = this.context.provider;
|
|
18
|
+
const deleteByTag = provider?.deleteByTag;
|
|
17
19
|
if (!deleteByTag) {
|
|
18
20
|
logger.warn('Cache provider does not support tag invalidation. Tags ignored.', {
|
|
19
21
|
namespace: this.context.namespace,
|
|
@@ -22,12 +24,18 @@ export class CacheController {
|
|
|
22
24
|
this.removeParsedValuesByTags(tags);
|
|
23
25
|
return;
|
|
24
26
|
}
|
|
25
|
-
|
|
27
|
+
const target = new Set(tags);
|
|
28
|
+
await this.context.mutations.invalidate(scope => scope.namespace === this.context.namespace
|
|
29
|
+
&& scope.tags.some(tag => target.has(tag)), async () => {
|
|
30
|
+
await Promise.all(tags.map(tag => deleteByTag.call(provider, this.context.namespace, tag)));
|
|
31
|
+
});
|
|
26
32
|
this.removeParsedValuesByTags(tags);
|
|
27
33
|
}
|
|
28
34
|
async clear() {
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const provider = this.context.provider;
|
|
36
|
+
const clearNamespace = provider?.clearNamespace;
|
|
37
|
+
if (clearNamespace) {
|
|
38
|
+
await this.context.mutations.invalidate(scope => scope.namespace === this.context.namespace, () => clearNamespace.call(provider, this.context.namespace));
|
|
31
39
|
}
|
|
32
40
|
this.context.parsedValues.clear();
|
|
33
41
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CacheWriteScope {
|
|
2
|
+
key: string;
|
|
3
|
+
namespace: string;
|
|
4
|
+
tags: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
type ScopeMatcher = (scope: CacheWriteScope) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Orders writes and invalidations without serializing mutations for unrelated
|
|
9
|
+
* keys. Earlier matching writes finish before an invalidation, while matching
|
|
10
|
+
* writes registered afterward wait for that invalidation to finish.
|
|
11
|
+
*/
|
|
12
|
+
export declare class CacheMutationCoordinator {
|
|
13
|
+
private tails;
|
|
14
|
+
private pendingWrites;
|
|
15
|
+
private pendingInvalidations;
|
|
16
|
+
enqueueWrite(scope: CacheWriteScope, write: () => Promise<void>): Promise<void>;
|
|
17
|
+
invalidate(matches: ScopeMatcher, operation: () => Promise<void>): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=mutation-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-coordinator.d.ts","sourceRoot":"","sources":["../../../src/core/cache/mutation-coordinator.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACzB;AAED,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC;AAgBxD;;;;GAIG;AACH,qBAAa,wBAAwB;IACnC,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,oBAAoB,CAAkC;IAE9D,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB/E,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAuBjF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function settle(operation) {
|
|
2
|
+
return operation.catch(() => undefined);
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Orders writes and invalidations without serializing mutations for unrelated
|
|
6
|
+
* keys. Earlier matching writes finish before an invalidation, while matching
|
|
7
|
+
* writes registered afterward wait for that invalidation to finish.
|
|
8
|
+
*/
|
|
9
|
+
export class CacheMutationCoordinator {
|
|
10
|
+
tails = new Map();
|
|
11
|
+
pendingWrites = new Set();
|
|
12
|
+
pendingInvalidations = new Set();
|
|
13
|
+
enqueueWrite(scope, write) {
|
|
14
|
+
const previous = this.tails.get(scope.key);
|
|
15
|
+
const invalidations = Array.from(this.pendingInvalidations)
|
|
16
|
+
.filter(invalidation => invalidation.matches(scope))
|
|
17
|
+
.map(invalidation => settle(invalidation.promise));
|
|
18
|
+
const prerequisites = previous
|
|
19
|
+
? [settle(previous), ...invalidations]
|
|
20
|
+
: invalidations;
|
|
21
|
+
const current = Promise.all(prerequisites).then(() => write());
|
|
22
|
+
const pending = { scope, promise: current };
|
|
23
|
+
this.tails.set(scope.key, current);
|
|
24
|
+
this.pendingWrites.add(pending);
|
|
25
|
+
void current
|
|
26
|
+
.finally(() => {
|
|
27
|
+
this.pendingWrites.delete(pending);
|
|
28
|
+
if (this.tails.get(scope.key) === current) {
|
|
29
|
+
this.tails.delete(scope.key);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch(() => undefined);
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
invalidate(matches, operation) {
|
|
36
|
+
const writes = Array.from(this.pendingWrites)
|
|
37
|
+
.filter(write => matches(write.scope))
|
|
38
|
+
.map(write => settle(write.promise));
|
|
39
|
+
// Invalidation is rare, and serializing overlapping calls makes their
|
|
40
|
+
// ordering deterministic without blocking unrelated cache writes.
|
|
41
|
+
const invalidations = Array.from(this.pendingInvalidations)
|
|
42
|
+
.map(invalidation => settle(invalidation.promise));
|
|
43
|
+
const current = Promise.all([...writes, ...invalidations])
|
|
44
|
+
.then(() => operation());
|
|
45
|
+
const pending = { matches, promise: current };
|
|
46
|
+
// Register the barrier synchronously so later matching writes cannot race
|
|
47
|
+
// the provider operation while this invalidation waits for earlier writes.
|
|
48
|
+
this.pendingInvalidations.add(pending);
|
|
49
|
+
void current
|
|
50
|
+
.finally(() => {
|
|
51
|
+
this.pendingInvalidations.delete(pending);
|
|
52
|
+
})
|
|
53
|
+
.catch(() => undefined);
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CacheConfig, CacheOptions, CacheProvider, CacheSerializeFn, CacheDeserializeFn, CacheStats } from './types.js';
|
|
2
|
+
import type { SharedFetch } from './shared-fetch.js';
|
|
3
|
+
import { CacheMutationCoordinator } from './mutation-coordinator.js';
|
|
2
4
|
export interface QueryRuntimeContext {
|
|
3
5
|
provider?: CacheProvider;
|
|
4
6
|
defaults: CacheOptions;
|
|
@@ -6,7 +8,8 @@ export interface QueryRuntimeContext {
|
|
|
6
8
|
versionTag: string;
|
|
7
9
|
serialize: CacheSerializeFn;
|
|
8
10
|
deserialize: CacheDeserializeFn;
|
|
9
|
-
inFlight: Map<string,
|
|
11
|
+
inFlight: Map<string, SharedFetch>;
|
|
12
|
+
mutations: CacheMutationCoordinator;
|
|
10
13
|
stats: CacheStats;
|
|
11
14
|
parsedValues: Map<string, ParsedValueEntry>;
|
|
12
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-context.d.ts","sourceRoot":"","sources":["../../../src/core/cache/runtime-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACX,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-context.d.ts","sourceRoot":"","sources":["../../../src/core/cache/runtime-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAUrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,WAAW,EAAE,kBAAkB,CAAC;IAChC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACnC,SAAS,EAAE,wBAAwB,CAAC;IACpC,KAAK,EAAE,UAAU,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAE7C;AAQD,wBAAgB,iBAAiB,CAAC,GAAG,UAAU,EAAE,KAAK,CAAC,YAAY,GAAG,SAAS,CAAC,GAAG,YAAY,CAe9F;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,mBAAmB,CAanF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,iBAAiB,EAAE,MAAM,GAAG,kBAAkB,CAUjH"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defaultSerialize, defaultDeserialize } from './serialization.js';
|
|
2
|
+
import { CacheMutationCoordinator } from './mutation-coordinator.js';
|
|
2
3
|
const DEFAULT_CACHE_OPTIONS = {
|
|
3
4
|
mode: 'no-store',
|
|
4
5
|
ttlMs: 0,
|
|
@@ -41,6 +42,7 @@ export function buildRuntimeContext(config) {
|
|
|
41
42
|
serialize: config.serialize,
|
|
42
43
|
deserialize: config.deserialize,
|
|
43
44
|
inFlight: new Map(),
|
|
45
|
+
mutations: new CacheMutationCoordinator(),
|
|
44
46
|
stats: createCacheStats(),
|
|
45
47
|
parsedValues: new Map()
|
|
46
48
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One deduplicated execution shared by every caller of the same cache key. */
|
|
2
|
+
export interface SharedFetch {
|
|
3
|
+
promise: Promise<unknown>;
|
|
4
|
+
controller: AbortController;
|
|
5
|
+
abortableWaiters: number;
|
|
6
|
+
pinnedWaiters: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Joins a caller to a shared fetch without allowing that caller to cancel
|
|
10
|
+
* anyone else. The underlying fetch is aborted only after its final waiter
|
|
11
|
+
* leaves.
|
|
12
|
+
*/
|
|
13
|
+
export declare function joinSharedFetch<T>(shared: SharedFetch, abortSignal?: AbortSignal): Promise<T>;
|
|
14
|
+
//# sourceMappingURL=shared-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-fetch.d.ts","sourceRoot":"","sources":["../../../src/core/cache/shared-fetch.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1B,UAAU,EAAE,eAAe,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAyC7F"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { abortReason } from '../utils/abort.js';
|
|
2
|
+
/**
|
|
3
|
+
* Joins a caller to a shared fetch without allowing that caller to cancel
|
|
4
|
+
* anyone else. The underlying fetch is aborted only after its final waiter
|
|
5
|
+
* leaves.
|
|
6
|
+
*/
|
|
7
|
+
export function joinSharedFetch(shared, abortSignal) {
|
|
8
|
+
if (!abortSignal) {
|
|
9
|
+
shared.pinnedWaiters += 1;
|
|
10
|
+
return shared.promise.finally(() => {
|
|
11
|
+
shared.pinnedWaiters -= 1;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (abortSignal.aborted) {
|
|
15
|
+
return Promise.reject(abortReason(abortSignal));
|
|
16
|
+
}
|
|
17
|
+
shared.abortableWaiters += 1;
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let waiting = true;
|
|
20
|
+
const leave = () => {
|
|
21
|
+
if (!waiting)
|
|
22
|
+
return;
|
|
23
|
+
waiting = false;
|
|
24
|
+
shared.abortableWaiters -= 1;
|
|
25
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
26
|
+
};
|
|
27
|
+
const onAbort = () => {
|
|
28
|
+
leave();
|
|
29
|
+
if (shared.abortableWaiters === 0 && shared.pinnedWaiters === 0) {
|
|
30
|
+
shared.controller.abort(abortSignal.reason);
|
|
31
|
+
}
|
|
32
|
+
reject(abortReason(abortSignal));
|
|
33
|
+
};
|
|
34
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
35
|
+
shared.promise.then(value => {
|
|
36
|
+
leave();
|
|
37
|
+
resolve(value);
|
|
38
|
+
}, error => {
|
|
39
|
+
leave();
|
|
40
|
+
reject(error);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -3,8 +3,12 @@ import { QueryBuilder } from '../query-builder.js';
|
|
|
3
3
|
import { type QueryLog } from '../utils/logger.js';
|
|
4
4
|
interface ExecutorRunOptions {
|
|
5
5
|
queryId?: string;
|
|
6
|
+
abortSignal?: AbortSignal;
|
|
6
7
|
logContext?: Partial<QueryLog>;
|
|
7
8
|
}
|
|
9
|
+
interface ExecutorStreamOptions {
|
|
10
|
+
abortSignal?: AbortSignal;
|
|
11
|
+
}
|
|
8
12
|
export declare class ExecutorFeature<Schema extends SchemaDefinition<Schema>, State extends BuilderState<Schema, string, any, keyof Schema, Partial<Record<string, keyof Schema>>, any, any>> {
|
|
9
13
|
private builder;
|
|
10
14
|
constructor(builder: QueryBuilder<Schema, State>);
|
|
@@ -14,7 +18,7 @@ export declare class ExecutorFeature<Schema extends SchemaDefinition<Schema>, St
|
|
|
14
18
|
};
|
|
15
19
|
toSQL(): string;
|
|
16
20
|
execute(options?: ExecutorRunOptions): Promise<State['output'][]>;
|
|
17
|
-
stream(): Promise<ReadableStream<State['output'][]>>;
|
|
21
|
+
stream(options?: ExecutorStreamOptions): Promise<ReadableStream<State['output'][]>>;
|
|
18
22
|
private compileQuery;
|
|
19
23
|
}
|
|
20
24
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/core/features/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG3D,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChC;AAED,qBAAa,eAAe,CAC1B,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC;IAElG,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAExD,eAAe,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,GAAG,EAAE,CAAA;KAAE;IAKrD,KAAK,IAAI,MAAM;IAMT,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/core/features/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG3D,UAAU,kBAAkB;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChC;AAED,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,eAAe,CAC1B,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC;IAElG,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAExD,eAAe,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,GAAG,EAAE,CAAA;KAAE;IAKrD,KAAK,IAAI,MAAM;IAMT,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;IAsDjE,MAAM,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAgDzF,OAAO,CAAC,YAAY;CAMrB"}
|
|
@@ -31,6 +31,7 @@ export class ExecutorFeature {
|
|
|
31
31
|
const rows = await adapter.query(sql, parameters, {
|
|
32
32
|
clickhouseSettings: queryNode.settings,
|
|
33
33
|
queryId: options?.queryId,
|
|
34
|
+
abortSignal: options?.abortSignal,
|
|
34
35
|
});
|
|
35
36
|
const endTime = Date.now();
|
|
36
37
|
logger.logQuery({
|
|
@@ -63,7 +64,7 @@ export class ExecutorFeature {
|
|
|
63
64
|
throw error;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
|
-
async stream() {
|
|
67
|
+
async stream(options) {
|
|
67
68
|
const adapter = this.builder.getAdapter();
|
|
68
69
|
const queryNode = this.builder.toQueryNode();
|
|
69
70
|
const { sql, parameters } = this.toSQLWithParams();
|
|
@@ -80,6 +81,7 @@ export class ExecutorFeature {
|
|
|
80
81
|
}
|
|
81
82
|
const webStream = await adapter.stream(sql, parameters, {
|
|
82
83
|
clickhouseSettings: queryNode.settings,
|
|
84
|
+
abortSignal: options?.abortSignal,
|
|
83
85
|
});
|
|
84
86
|
const endTime = Date.now();
|
|
85
87
|
logger.logQuery({
|