@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.
Files changed (47) hide show
  1. package/dist/core/adapters/clickhouse-adapter.d.ts +12 -4
  2. package/dist/core/adapters/clickhouse-adapter.d.ts.map +1 -1
  3. package/dist/core/adapters/clickhouse-adapter.js +42 -37
  4. package/dist/core/adapters/database-adapter.d.ts +2 -0
  5. package/dist/core/adapters/database-adapter.d.ts.map +1 -1
  6. package/dist/core/cache/cache-manager.d.ts.map +1 -1
  7. package/dist/core/cache/cache-manager.js +59 -18
  8. package/dist/core/cache/controller.d.ts.map +1 -1
  9. package/dist/core/cache/controller.js +15 -7
  10. package/dist/core/cache/mutation-coordinator.d.ts +20 -0
  11. package/dist/core/cache/mutation-coordinator.d.ts.map +1 -0
  12. package/dist/core/cache/mutation-coordinator.js +56 -0
  13. package/dist/core/cache/runtime-context.d.ts +4 -1
  14. package/dist/core/cache/runtime-context.d.ts.map +1 -1
  15. package/dist/core/cache/runtime-context.js +2 -0
  16. package/dist/core/cache/shared-fetch.d.ts +14 -0
  17. package/dist/core/cache/shared-fetch.d.ts.map +1 -0
  18. package/dist/core/cache/shared-fetch.js +43 -0
  19. package/dist/core/features/executor.d.ts +5 -1
  20. package/dist/core/features/executor.d.ts.map +1 -1
  21. package/dist/core/features/executor.js +3 -1
  22. package/dist/core/features/insert-executor.d.ts +1 -0
  23. package/dist/core/features/insert-executor.d.ts.map +1 -1
  24. package/dist/core/features/insert-executor.js +1 -0
  25. package/dist/core/insert-builder.d.ts +1 -0
  26. package/dist/core/insert-builder.d.ts.map +1 -1
  27. package/dist/core/query-builder.d.ts +26 -3
  28. package/dist/core/query-builder.d.ts.map +1 -1
  29. package/dist/core/query-builder.js +4 -4
  30. package/dist/core/utils/abort.d.ts +5 -0
  31. package/dist/core/utils/abort.d.ts.map +1 -0
  32. package/dist/core/utils/abort.js +42 -0
  33. package/dist/core/utils/abortable-result.d.ts +7 -0
  34. package/dist/core/utils/abortable-result.d.ts.map +1 -0
  35. package/dist/core/utils/abortable-result.js +27 -0
  36. package/dist/core/utils/clickhouse-adapter-config.d.ts +7 -0
  37. package/dist/core/utils/clickhouse-adapter-config.d.ts.map +1 -0
  38. package/dist/core/utils/clickhouse-adapter-config.js +20 -0
  39. package/dist/core/utils/integer-json-encoding.d.ts +18 -0
  40. package/dist/core/utils/integer-json-encoding.d.ts.map +1 -0
  41. package/dist/core/utils/integer-json-encoding.js +47 -0
  42. package/dist/core/utils/streaming-helpers.d.ts +1 -1
  43. package/dist/core/utils/streaming-helpers.d.ts.map +1 -1
  44. package/dist/core/utils/streaming-helpers.js +72 -26
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/package.json +2 -2
@@ -1,16 +1,24 @@
1
1
  import type { DatabaseAdapter, QueryExecutionOptions, InsertExecutionOptions, InsertResultSummary } from './database-adapter.js';
2
- import type { ClickHouseConfig } from '../query-builder.js';
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
- constructor(config: ClickHouseConfig);
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: ClickHouseConfig): DatabaseAdapter;
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,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAsC5D,qBAAa,iBAAkB,YAAW,eAAe;IAK3C,OAAO,CAAC,MAAM;IAJ1B,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,MAAM,CAAmB;gBAEb,MAAM,EAAE,gBAAgB;IAKtC,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAW5F,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;IAY7G,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;IAuBzB,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,gBAAgB,GAAG,eAAe,CAEjF"}
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
- function createClickHouseClient(config) {
8
- if (isClientConfig(config)) {
9
- return config.client;
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.config = config;
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: jsonOutputSettings(options?.clickhouseSettings),
43
+ clickhouse_settings: clickhouseSettings,
46
44
  query_id: options?.queryId,
47
- });
48
- return result.json();
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: jsonOutputSettings(options?.clickhouseSettings),
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;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,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
+ {"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;AA4DnF,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,CA2J5B"}
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
- if (mergedOptions.dedupe !== false && runtime.inFlight.has(key)) {
129
- return runtime.inFlight.get(key);
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
- const promise = (async () => {
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
- const encoded = await serialize(rows);
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
- await activeProvider.set(key, newEntry);
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;IAMzC,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,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"}
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 deleteByTag = this.context.provider?.deleteByTag;
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
- await Promise.all(tags.map(tag => deleteByTag.call(this.context.provider, this.context.namespace, tag)));
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
- if (this.context.provider?.clearNamespace) {
30
- await this.context.provider.clearNamespace(this.context.namespace);
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, Promise<unknown>>;
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;AAWpB,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,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACxC,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,CAYnF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,iBAAiB,EAAE,MAAM,GAAG,kBAAkB,CAUjH"}
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;IAqDjE,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IA+C1D,OAAO,CAAC,YAAY;CAMrB"}
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({