@query-farm/vgi 0.15.0 → 0.18.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.
@@ -32,6 +32,18 @@ export interface TableProcessParams<TArgs = Record<string, any>> {
32
32
  */
33
33
  atUnit?: string;
34
34
  atValue?: string;
35
+ /**
36
+ * Conditional-revalidation validator (the client's stored ETag). Set when the
37
+ * client holds a stale-but-revalidatable cached result and asks the worker to
38
+ * confirm freshness cheaply; a worker that advertised `revalidatable` compares
39
+ * it and, if unchanged, emits a 0-row batch tagged
40
+ * `cacheControlMetadata({ notModified: true })` instead of re-streaming.
41
+ * Undefined on a normal call.
42
+ */
43
+ ifNoneMatch?: string;
44
+ /** Conditional-revalidation validator (the client's stored Last-Modified).
45
+ * Companion to {@link ifNoneMatch}. Undefined on a normal call. */
46
+ ifModifiedSince?: string;
35
47
  }
36
48
  export interface TableFunctionConfig<TArgs = Record<string, any>, TState = null> {
37
49
  name: string;
@@ -58,7 +58,7 @@ export interface TableInfo {
58
58
  cardinality_max?: number | null;
59
59
  column_statistics?: Uint8Array | null;
60
60
  bind_result?: Uint8Array | null;
61
- required_field_filter_paths: string[];
61
+ required_filters: string[][];
62
62
  }
63
63
  export interface ViewInfo {
64
64
  comment?: string | null;
@@ -0,0 +1,35 @@
1
+ import { type ProtocolConfig } from "../protocol/dispatch.js";
2
+ import type { LandingDescribeProvider } from "./describe-json.js";
3
+ export interface VgiFetchOptions {
4
+ /** Wire-protocol config (registry + catalogInterface). */
5
+ protocol: Omit<ProtocolConfig, "recoverExchangeState">;
6
+ /** HMAC key for state-token signing. Pass a stable, secret 32-byte key
7
+ * (e.g. derived from a Wrangler secret). Required because Workers don't
8
+ * preserve in-memory state across requests/instances. */
9
+ signingKey: Uint8Array;
10
+ /** State-token TTL in seconds (default 3600). */
11
+ tokenTtl?: number;
12
+ /** URL path prefix for VGI requests (default "/vgi"). Pass "" to mount at
13
+ * the root, which is what a dedicated single-worker HTTP server wants. */
14
+ prefix?: string;
15
+ /** Server ID for state-token attribution (default "vgi-cf"). */
16
+ serverId?: string;
17
+ /** CORS allowed origins. When set, CORS headers are added to all responses,
18
+ * and the preflight `OPTIONS` that browser clients (e.g. the hosted Cupola
19
+ * UI) send before `__describe__` will succeed. Omit to disable CORS. */
20
+ corsOrigins?: string;
21
+ /** Public source-repository URL, surfaced on the landing page. */
22
+ repositoryUrl?: string;
23
+ /** Enables the standardized VGI landing surface: `GET /` serves the shared
24
+ * landing.html, `GET /describe.json` the catalog contract, and
25
+ * `GET /describe/{catalog}/{schema}/{table}.json` lazy per-object columns.
26
+ * Build one with `createLandingDescribe(catalogInterface, workerInfo)`. */
27
+ landingDescribe?: LandingDescribeProvider;
28
+ }
29
+ /**
30
+ * Build a fetch handler suitable for `export default { fetch }` in a CF
31
+ * Worker module, or for `Bun.serve({ fetch })`. The returned handler is
32
+ * async-safe across Workers' isolate-per-request execution model — all state
33
+ * round-trips through the signed state token created here.
34
+ */
35
+ export declare function createVgiFetch(opts: VgiFetchOptions): (req: Request) => Promise<Response>;
@@ -1,3 +1,4 @@
1
+ export { CACHE_ETAG_KEY, CACHE_EXPIRES_KEY, CACHE_IF_MODIFIED_SINCE_KEY, CACHE_IF_NONE_MATCH_KEY, CACHE_LAST_MODIFIED_KEY, CACHE_NO_STORE_KEY, CACHE_NOT_MODIFIED_KEY, CACHE_REVALIDATABLE_KEY, CACHE_SCOPE_CATALOG, CACHE_SCOPE_KEY, CACHE_SCOPE_TRANSACTION, CACHE_STALE_IF_ERROR_KEY, CACHE_STALE_WHILE_REVALIDATE_KEY, CACHE_TTL_KEY, type CacheControl, cacheControlMetadata, type CacheScope, } from "./cache-control.js";
1
2
  export { defineScalarFunction, type ScalarFunctionConfig, type ScalarBindParameters, type ScalarParameterDef } from "./functions/scalar.js";
2
3
  export { defineTableFunction, type TableFunctionConfig, type TableBindParams, type TableProcessParams } from "./functions/table.js";
3
4
  export { defineAggregate, GROUP_COLUMN_NAME, type AggregateFunctionConfig, type AggregateBindParams, type AggregateUpdateParams, type AggregateFinalizeParams, } from "./functions/aggregate.js";
package/dist/index.js CHANGED
@@ -2546,6 +2546,55 @@ var init_storage = __esm(() => {
2546
2546
  });
2547
2547
  });
2548
2548
 
2549
+ // src/cache-control.ts
2550
+ var CACHE_TTL_KEY = "vgi.cache.ttl";
2551
+ var CACHE_EXPIRES_KEY = "vgi.cache.expires";
2552
+ var CACHE_NO_STORE_KEY = "vgi.cache.no_store";
2553
+ var CACHE_SCOPE_KEY = "vgi.cache.scope";
2554
+ var CACHE_ETAG_KEY = "vgi.cache.etag";
2555
+ var CACHE_LAST_MODIFIED_KEY = "vgi.cache.last_modified";
2556
+ var CACHE_REVALIDATABLE_KEY = "vgi.cache.revalidatable";
2557
+ var CACHE_STALE_WHILE_REVALIDATE_KEY = "vgi.cache.stale_while_revalidate";
2558
+ var CACHE_STALE_IF_ERROR_KEY = "vgi.cache.stale_if_error";
2559
+ var CACHE_NOT_MODIFIED_KEY = "vgi.cache.not_modified";
2560
+ var CACHE_IF_NONE_MATCH_KEY = "vgi.cache.if_none_match";
2561
+ var CACHE_IF_MODIFIED_SINCE_KEY = "vgi.cache.if_modified_since";
2562
+ var CACHE_SCOPE_CATALOG = "catalog";
2563
+ var CACHE_SCOPE_TRANSACTION = "transaction";
2564
+ var VALID_SCOPES = [CACHE_SCOPE_CATALOG, CACHE_SCOPE_TRANSACTION];
2565
+ function cacheControlMetadata(cc, extra) {
2566
+ const scope = cc.scope ?? CACHE_SCOPE_CATALOG;
2567
+ if (!VALID_SCOPES.includes(scope)) {
2568
+ throw new Error(`CacheControl.scope must be one of ${VALID_SCOPES.join(", ")}, got ${JSON.stringify(scope)}`);
2569
+ }
2570
+ for (const name of ["ttl", "staleWhileRevalidate", "staleIfError"]) {
2571
+ const value = cc[name];
2572
+ if (value !== undefined && value < 0) {
2573
+ throw new Error(`CacheControl.${name} must be >= 0, got ${value}`);
2574
+ }
2575
+ }
2576
+ const md = new Map(extra ?? []);
2577
+ if (cc.ttl !== undefined)
2578
+ md.set(CACHE_TTL_KEY, String(cc.ttl));
2579
+ if (cc.expires !== undefined)
2580
+ md.set(CACHE_EXPIRES_KEY, cc.expires);
2581
+ if (cc.noStore)
2582
+ md.set(CACHE_NO_STORE_KEY, "1");
2583
+ md.set(CACHE_SCOPE_KEY, scope);
2584
+ if (cc.etag !== undefined)
2585
+ md.set(CACHE_ETAG_KEY, cc.etag);
2586
+ if (cc.lastModified !== undefined)
2587
+ md.set(CACHE_LAST_MODIFIED_KEY, cc.lastModified);
2588
+ if (cc.revalidatable)
2589
+ md.set(CACHE_REVALIDATABLE_KEY, "1");
2590
+ if (cc.staleWhileRevalidate !== undefined)
2591
+ md.set(CACHE_STALE_WHILE_REVALIDATE_KEY, String(cc.staleWhileRevalidate));
2592
+ if (cc.staleIfError !== undefined)
2593
+ md.set(CACHE_STALE_IF_ERROR_KEY, String(cc.staleIfError));
2594
+ if (cc.notModified)
2595
+ md.set(CACHE_NOT_MODIFIED_KEY, "1");
2596
+ return md;
2597
+ }
2549
2598
  // src/functions/scalar.ts
2550
2599
  init_arrow();
2551
2600
 
@@ -3989,6 +4038,12 @@ function defineTableFunction(config) {
3989
4038
  onTick: (pState, tickMetadata) => {
3990
4039
  if (!tickMetadata)
3991
4040
  return;
4041
+ const ifNoneMatch = tickMetadata.get(CACHE_IF_NONE_MATCH_KEY);
4042
+ const ifModifiedSince = tickMetadata.get(CACHE_IF_MODIFIED_SINCE_KEY);
4043
+ if (ifNoneMatch !== undefined)
4044
+ pState.processParams.ifNoneMatch = ifNoneMatch;
4045
+ if (ifModifiedSince !== undefined)
4046
+ pState.processParams.ifModifiedSince = ifModifiedSince;
3992
4047
  const encoded = tickMetadata.get("vgi_pushdown_filters");
3993
4048
  if (!encoded)
3994
4049
  return;
@@ -5741,7 +5796,7 @@ var TableInfoSchema = schema2([
5741
5796
  field2("cardinality_max", int642(), false),
5742
5797
  field2("column_statistics", binary2(), false),
5743
5798
  field2("bind_result", binary2(), false),
5744
- field2("required_field_filter_paths", list2(field2("item", utf82(), true)), false)
5799
+ field2("required_filters", list2(field2("item", list2(field2("item", utf82(), true)), true)), false)
5745
5800
  ]);
5746
5801
  var ViewInfoSchema = schema2([
5747
5802
  field2("comment", utf82(), true),
@@ -6813,16 +6868,21 @@ class ReadOnlyCatalogInterface extends CatalogInterface {
6813
6868
  const uniqueIndices = resolveIndexGroups(colSchema, t.unique);
6814
6869
  const pkIndices = resolveIndexGroups(colSchema, t.primaryKey);
6815
6870
  const fkBytes = serializeForeignKeys(t.foreignKey, name);
6816
- const requiredFieldFilterPaths = t.requiredFieldFilterPaths ?? [];
6817
- if (requiredFieldFilterPaths.length > 0) {
6871
+ const requiredFilters = t.requiredFilters ?? [];
6872
+ if (requiredFilters.length > 0) {
6818
6873
  const columnNames = new Set(colSchema.fields.map((f) => f.name));
6819
- for (const path2 of requiredFieldFilterPaths) {
6820
- if (!path2) {
6821
- throw new Error(`Table '${t.name}': requiredFieldFilterPaths must not contain empty strings`);
6874
+ for (const group of requiredFilters) {
6875
+ if (!group || group.length === 0) {
6876
+ throw new Error(`Table '${t.name}': requiredFilters must not contain empty groups`);
6822
6877
  }
6823
- const head = path2.split(".", 1)[0];
6824
- if (!columnNames.has(head)) {
6825
- throw new Error(`Table '${t.name}': requiredFieldFilterPaths path '${path2}' references ` + `unknown column '${head}'. Available columns: ${[...columnNames].sort().join(", ")}`);
6878
+ for (const path2 of group) {
6879
+ if (!path2) {
6880
+ throw new Error(`Table '${t.name}': requiredFilters must not contain empty strings`);
6881
+ }
6882
+ const head = path2.split(".", 1)[0];
6883
+ if (!columnNames.has(head)) {
6884
+ throw new Error(`Table '${t.name}': requiredFilters path '${path2}' references ` + `unknown column '${head}'. Available columns: ${[...columnNames].sort().join(", ")}`);
6885
+ }
6826
6886
  }
6827
6887
  }
6828
6888
  }
@@ -6848,7 +6908,7 @@ class ReadOnlyCatalogInterface extends CatalogInterface {
6848
6908
  delete_function: null,
6849
6909
  cardinality_estimate: t.inlinedCardinality ? Number(t.inlinedCardinality.estimate) : null,
6850
6910
  cardinality_max: t.inlinedCardinality ? Number(t.inlinedCardinality.max) : null,
6851
- required_field_filter_paths: requiredFieldFilterPaths
6911
+ required_filters: requiredFilters
6852
6912
  };
6853
6913
  }));
6854
6914
  }
@@ -11267,6 +11327,145 @@ class VgiClient {
11267
11327
  // src/index.core.ts
11268
11328
  import { str, bytes, int as int3, int32 as int323, float, float32 as float323, bool as bool3, toSchema, OutputCollector, AuthContext } from "@query-farm/vgi-rpc";
11269
11329
  import { subprocessConnect, httpConnect, tcpConnect } from "@query-farm/vgi-rpc";
11330
+ // src/worker.ts
11331
+ import { VgiRpcServer, serveUnix, serveTcp, serveStream as serveStreamRpc } from "@query-farm/vgi-rpc";
11332
+ function parseTcpSpec(spec, out) {
11333
+ const idx = spec.lastIndexOf(":");
11334
+ if (idx >= 0) {
11335
+ const h = spec.slice(0, idx);
11336
+ out.tcpHost = h === "" ? "127.0.0.1" : h;
11337
+ out.tcpPort = Number(spec.slice(idx + 1));
11338
+ } else {
11339
+ out.tcpHost = "127.0.0.1";
11340
+ out.tcpPort = Number(spec);
11341
+ }
11342
+ }
11343
+ function parseLauncherArgs(argv) {
11344
+ const out = {};
11345
+ for (let i = 0;i < argv.length; i++) {
11346
+ const a = argv[i];
11347
+ if (a === "--unix" && i + 1 < argv.length) {
11348
+ out.unixPath = argv[++i];
11349
+ } else if (a.startsWith("--unix=")) {
11350
+ out.unixPath = a.slice("--unix=".length);
11351
+ } else if (a === "--tcp" && i + 1 < argv.length) {
11352
+ parseTcpSpec(argv[++i], out);
11353
+ } else if (a.startsWith("--tcp=")) {
11354
+ parseTcpSpec(a.slice("--tcp=".length), out);
11355
+ } else if (a === "--idle-timeout" && i + 1 < argv.length) {
11356
+ out.idleTimeout = Number(argv[++i]);
11357
+ } else if (a.startsWith("--idle-timeout=")) {
11358
+ out.idleTimeout = Number(a.slice("--idle-timeout=".length));
11359
+ }
11360
+ }
11361
+ return out;
11362
+ }
11363
+
11364
+ class Worker {
11365
+ _registry;
11366
+ _catalogInterface;
11367
+ _catalogName;
11368
+ constructor(config) {
11369
+ this._registry = new FunctionRegistry;
11370
+ this._catalogName = config.catalogName;
11371
+ if (config.functions) {
11372
+ for (const func of config.functions) {
11373
+ this._registry.register(func);
11374
+ }
11375
+ }
11376
+ if (config.catalog) {
11377
+ for (const schema3 of config.catalog.schemas) {
11378
+ if (schema3.functions) {
11379
+ for (const func of schema3.functions) {
11380
+ this._registry.register(func);
11381
+ }
11382
+ }
11383
+ }
11384
+ }
11385
+ if (config.catalogInterface) {
11386
+ this._catalogInterface = config.catalogInterface;
11387
+ } else if (config.catalog) {
11388
+ const base = new ReadOnlyCatalogInterface(config.catalog, this._registry);
11389
+ this._catalogInterface = config.catalogInterfaceFactory ? config.catalogInterfaceFactory(base) : base;
11390
+ }
11391
+ }
11392
+ run(argv = process.argv.slice(2)) {
11393
+ process.stderr.write(`[worker] starting, pid=${process.pid}
11394
+ `);
11395
+ try {
11396
+ const protocol = this.buildProtocol();
11397
+ process.stderr.write(`[worker] protocol built
11398
+ `);
11399
+ const launcher = parseLauncherArgs(argv);
11400
+ if (launcher.unixPath !== undefined) {
11401
+ const envOverride = process.env.VGI_WORKER_IDLE_TIMEOUT;
11402
+ const idleTimeout = envOverride !== undefined && envOverride !== "" ? Number(envOverride) : launcher.idleTimeout;
11403
+ process.stderr.write(`[worker] AF_UNIX mode: ${launcher.unixPath} idle=${idleTimeout ?? 300}s${envOverride !== undefined && envOverride !== "" ? " (env override)" : ""}
11404
+ `);
11405
+ serveUnix(protocol, {
11406
+ unixPath: launcher.unixPath,
11407
+ idleTimeout
11408
+ }).then((handle) => handle.done).then(() => {
11409
+ process.stderr.write(`[worker] serveUnix done (idle shutdown)
11410
+ `);
11411
+ }).catch((err2) => {
11412
+ process.stderr.write(`Worker error: ${err2.message}
11413
+ ${err2.stack}
11414
+ `);
11415
+ process.exit(1);
11416
+ });
11417
+ return;
11418
+ }
11419
+ if (launcher.tcpPort !== undefined) {
11420
+ const envOverride = process.env.VGI_WORKER_IDLE_TIMEOUT;
11421
+ const idleTimeout = envOverride !== undefined && envOverride !== "" ? Number(envOverride) : launcher.idleTimeout;
11422
+ process.stderr.write(`[worker] TCP mode: ${launcher.tcpHost}:${launcher.tcpPort} idle=${idleTimeout ?? 300}s${envOverride !== undefined && envOverride !== "" ? " (env override)" : ""}
11423
+ `);
11424
+ serveTcp(protocol, {
11425
+ host: launcher.tcpHost,
11426
+ port: launcher.tcpPort,
11427
+ idleTimeout
11428
+ }).then((handle) => handle.done).then(() => {
11429
+ process.stderr.write(`[worker] serveTcp done (idle shutdown)
11430
+ `);
11431
+ }).catch((err2) => {
11432
+ process.stderr.write(`Worker error: ${err2.message}
11433
+ ${err2.stack}
11434
+ `);
11435
+ process.exit(1);
11436
+ });
11437
+ return;
11438
+ }
11439
+ const server = new VgiRpcServer(protocol);
11440
+ process.stderr.write(`[worker] server created, calling run()
11441
+ `);
11442
+ server.run().then(() => {
11443
+ process.stderr.write(`[worker] server.run() resolved cleanly
11444
+ `);
11445
+ }).catch((err2) => {
11446
+ process.stderr.write(`Worker error: ${err2.message}
11447
+ ${err2.stack}
11448
+ `);
11449
+ process.exit(1);
11450
+ });
11451
+ } catch (err2) {
11452
+ process.stderr.write(`Worker init error: ${err2.message}
11453
+ ${err2.stack}
11454
+ `);
11455
+ process.exit(1);
11456
+ }
11457
+ }
11458
+ buildProtocol() {
11459
+ return buildVgiProtocol({
11460
+ registry: this._registry,
11461
+ catalogInterface: this._catalogInterface,
11462
+ catalogName: this._catalogName
11463
+ });
11464
+ }
11465
+ async serveStream(readable, writable) {
11466
+ await serveStreamRpc(this.buildProtocol(), { readable, writable });
11467
+ }
11468
+ }
11270
11469
  export {
11271
11470
  utf82 as utf8,
11272
11471
  union2 as union,
@@ -11402,6 +11601,7 @@ export {
11402
11601
  constraintSpecFields,
11403
11602
  columnFromArray,
11404
11603
  codecFor,
11604
+ cacheControlMetadata,
11405
11605
  bytes,
11406
11606
  buildVgiProtocol,
11407
11607
  buildScanBranchesResult,
@@ -11475,6 +11675,20 @@ export {
11475
11675
  CatalogFunctionType,
11476
11676
  CatalogAlreadyExistsError,
11477
11677
  CUPOLA_BASE,
11678
+ CACHE_TTL_KEY,
11679
+ CACHE_STALE_WHILE_REVALIDATE_KEY,
11680
+ CACHE_STALE_IF_ERROR_KEY,
11681
+ CACHE_SCOPE_TRANSACTION,
11682
+ CACHE_SCOPE_KEY,
11683
+ CACHE_SCOPE_CATALOG,
11684
+ CACHE_REVALIDATABLE_KEY,
11685
+ CACHE_NO_STORE_KEY,
11686
+ CACHE_NOT_MODIFIED_KEY,
11687
+ CACHE_LAST_MODIFIED_KEY,
11688
+ CACHE_IF_NONE_MATCH_KEY,
11689
+ CACHE_IF_MODIFIED_SINCE_KEY,
11690
+ CACHE_EXPIRES_KEY,
11691
+ CACHE_ETAG_KEY,
11478
11692
  BoundStorage,
11479
11693
  BindResultSchema,
11480
11694
  AuthContext,
@@ -11482,4 +11696,4 @@ export {
11482
11696
  ArgumentValidationError
11483
11697
  };
11484
11698
 
11485
- //# debugId=B7CCB77D8D0375B864756E2164756E21
11699
+ //# debugId=B98CA527867E993364756E2164756E21