@kanzo-tech/mosaic 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,22 @@ import { engine } from "@kanzo-tech/mosaic";
25
25
  DuckDB-WASM's `File already registered` was refusing. `hold(name, bytes)` registers a copy of a
26
26
  buffer; `drop(names)` forgets. It is fossil's `Engine` structurally, without depending on fossil.
27
27
 
28
+ It boots with DuckDB's `httpfs` loaded, so `s3://` is readable once a secret says how:
29
+ `query("CREATE OR REPLACE SECRET job (TYPE s3, …, SCOPE 's3://bucket/prefix/')")`, then name the
30
+ objects by their URLs. A lent name has no scheme: `https://…` and `s3://…` in SQL are `httpfs`'s,
31
+ and it answers before the registry is asked.
32
+
33
+ **Nothing is fetched from a CDN.** DuckDB-WASM's worker and module come from
34
+ `@duckdb/duckdb-wasm`, a dependency at the exact release `httpfs` was built for; `httpfs` ships in
35
+ this package (`extensions/`, fetched and hash-pinned at build by `scripts/extensions.mjs`). All
36
+ three are reached through `new URL(…, import.meta.url)`, so your bundler emits them as assets —
37
+ under Next, `/_next/static/media/duckdb-browser-eh.worker.<hash>.js`, `duckdb-eh.<hash>.wasm` and
38
+ `httpfs.duckdb_extension.<hash>.wasm`, or their `mvp` siblings on a browser without WebAssembly
39
+ exceptions — and there is nothing to copy. Serve `.wasm` as `application/wasm` (Next does), and
40
+ the page needs no origin but yours:
41
+ `script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self' <your storage>`. No
42
+ `blob:`: the worker is started from its own URL.
43
+
28
44
  - **Re-exports** of the coordinator, the clients, the five clause builders and the loaders — so a
29
45
  consumer never needs a direct `@uwdata` import. The DuckDB-WASM connector is not among them:
30
46
  `engine()` is the only boot.
package/dist/arrow.js CHANGED
@@ -1,29 +1,61 @@
1
- function b(r, n) {
2
- var u;
3
- const o = (u = r.getChild) == null ? void 0 : u.call(r, n);
4
- return o ? Array.from(o.toArray()) : Array.from(r, (c) => c[n]);
1
+ /**
2
+ * Reading one column out of whatever a Mosaic query handed back.
3
+ *
4
+ * The coordinator answers with an Arrow table, and Arrow only offers a typed column when the type
5
+ * allows one: `getChild` gives an array for an integer column and nothing usable for a
6
+ * dictionary-encoded string. Both shapes come back from ordinary queries — an `id` is the first, a
7
+ * `label` is the second — so a `queryResult` that assumes the typed path is a crash waiting for the
8
+ * first query that selects a string.
9
+ *
10
+ * This is the half of the client protocol the protocol itself does not give you. Declaring a query
11
+ * and publishing a clause is documented and small; turning the answer into values is where every
12
+ * call site independently writes `as { getChild(name: string): … }`, which is a cast asserting the
13
+ * shape rather than checking it.
14
+ */
15
+ /** Every value in `field`, in row order. */
16
+ export function column(data, field) {
17
+ const child = data.getChild?.(field);
18
+ if (child)
19
+ return Array.from(child.toArray());
20
+ return Array.from(data, (row) => row[field]);
5
21
  }
6
- function y(r, n) {
7
- return b(r, n).map(Number);
22
+ /** The same, coerced — Arrow hands back `BigInt` for some integer widths. */
23
+ export function numbers(data, field) {
24
+ return column(data, field).map(Number);
8
25
  }
9
- function g(r, n, o, u = 0, c = 1) {
10
- var h, f;
11
- const i = Math.max(0, Math.ceil((o.length - u) / c)), t = (f = (h = r.getChild) == null ? void 0 : h.call(r, n)) == null ? void 0 : f.toArray();
12
- if (t) {
13
- const l = Math.min(t.length, i);
14
- for (let m = 0; m < l; m++) o[u + m * c] = Number(t[m]);
15
- return l;
16
- }
17
- let e = 0;
18
- for (const l of r) {
19
- if (e >= i) break;
20
- o[u + e * c] = Number(l[n]), e++;
21
- }
22
- return e;
26
+ /**
27
+ * One numeric column, written straight into a typed array the caller already owns.
28
+ *
29
+ * `numbers()` is the wrong tool once a result is large: `toArray()` already hands back a typed
30
+ * buffer, and `Array.from(...).map(Number)` turns it into two full-length boxed `number[]` on the
31
+ * way to a third array that was the actual destination. Three allocations to move nothing. This is
32
+ * the same pass with none of them — which is what lets a caller size once against its own limit and
33
+ * fill from the Arrow batches as they arrive.
34
+ *
35
+ * `stride` and `offset` are how interleaving is expressed: `x` at `(0, 2)` and `y` at `(1, 2)` fill
36
+ * one `[x0, y0, x1, y1, …]` buffer with no seam between them.
37
+ *
38
+ * Returns how many rows were written, which is `min(rows, capacity)` — a caller that sized against
39
+ * a `LIMIT` uses it to `subarray` down to what actually came back.
40
+ */
41
+ export function fillColumn(data, field, into, offset = 0, stride = 1) {
42
+ const capacity = Math.max(0, Math.ceil((into.length - offset) / stride));
43
+ const values = data.getChild?.(field)?.toArray();
44
+ if (values) {
45
+ const n = Math.min(values.length, capacity);
46
+ // `Number` is identity on the numeric widths and the conversion on the BigInt ones; either way
47
+ // it reads an element rather than allocating one.
48
+ for (let i = 0; i < n; i++)
49
+ into[offset + i * stride] = Number(values[i]);
50
+ return n;
51
+ }
52
+ let i = 0;
53
+ for (const row of data) {
54
+ if (i >= capacity)
55
+ break;
56
+ into[offset + i * stride] = Number(row[field]);
57
+ i++;
58
+ }
59
+ return i;
23
60
  }
24
- export {
25
- b as column,
26
- g as fillColumn,
27
- y as numbers
28
- };
29
- //# sourceMappingURL=arrow.js.map
61
+ //# sourceMappingURL=arrow.js.map
package/dist/arrow.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"arrow.js","sources":["../src/arrow.ts"],"sourcesContent":["/**\n * Reading one column out of whatever a Mosaic query handed back.\n *\n * The coordinator answers with an Arrow table, and Arrow only offers a typed column when the type\n * allows one: `getChild` gives an array for an integer column and nothing usable for a\n * dictionary-encoded string. Both shapes come back from ordinary queries — an `id` is the first, a\n * `label` is the second — so a `queryResult` that assumes the typed path is a crash waiting for the\n * first query that selects a string.\n *\n * This is the half of the client protocol the protocol itself does not give you. Declaring a query\n * and publishing a clause is documented and small; turning the answer into values is where every\n * call site independently writes `as { getChild(name: string): … }`, which is a cast asserting the\n * shape rather than checking it.\n */\n\ninterface ArrowLike {\n getChild?: (name: string) => { toArray(): ArrayLike<unknown> } | null;\n}\n\n/** Every value in `field`, in row order. */\nexport function column(data: unknown, field: string): unknown[] {\n const child = (data as ArrowLike).getChild?.(field);\n if (child) return Array.from(child.toArray());\n return Array.from(data as Iterable<Record<string, unknown>>, (row) => row[field]);\n}\n\n/** The same, coerced — Arrow hands back `BigInt` for some integer widths. */\nexport function numbers(data: unknown, field: string): number[] {\n return column(data, field).map(Number);\n}\n\n/** A pre-sized destination `fillColumn` can write into. */\nexport type NumericArray = Float32Array | Float64Array | Uint32Array | Uint16Array | Int32Array;\n\n/**\n * One numeric column, written straight into a typed array the caller already owns.\n *\n * `numbers()` is the wrong tool once a result is large: `toArray()` already hands back a typed\n * buffer, and `Array.from(...).map(Number)` turns it into two full-length boxed `number[]` on the\n * way to a third array that was the actual destination. Three allocations to move nothing. This is\n * the same pass with none of them — which is what lets a caller size once against its own limit and\n * fill from the Arrow batches as they arrive.\n *\n * `stride` and `offset` are how interleaving is expressed: `x` at `(0, 2)` and `y` at `(1, 2)` fill\n * one `[x0, y0, x1, y1, …]` buffer with no seam between them.\n *\n * Returns how many rows were written, which is `min(rows, capacity)` — a caller that sized against\n * a `LIMIT` uses it to `subarray` down to what actually came back.\n */\nexport function fillColumn(\n data: unknown,\n field: string,\n into: NumericArray,\n offset = 0,\n stride = 1,\n): number {\n const capacity = Math.max(0, Math.ceil((into.length - offset) / stride));\n const values = (data as ArrowLike).getChild?.(field)?.toArray();\n if (values) {\n const n = Math.min(values.length, capacity);\n // `Number` is identity on the numeric widths and the conversion on the BigInt ones; either way\n // it reads an element rather than allocating one.\n for (let i = 0; i < n; i++) into[offset + i * stride] = Number(values[i]);\n return n;\n }\n let i = 0;\n for (const row of data as Iterable<Record<string, unknown>>) {\n if (i >= capacity) break;\n into[offset + i * stride] = Number(row[field]);\n i++;\n }\n return i;\n}\n"],"names":["column","data","field","_a","child","row","numbers","fillColumn","into","offset","stride","_b","capacity","values","n","i"],"mappings":"AAoBO,SAASA,EAAOC,GAAeC,GAA0B;AAAzD,MAAAC;AACL,QAAMC,KAASD,IAAAF,EAAmB,aAAnB,gBAAAE,EAAA,KAAAF,GAA8BC;AAC7C,SAAIE,IAAc,MAAM,KAAKA,EAAM,SAAS,IACrC,MAAM,KAAKH,GAA2C,CAACI,MAAQA,EAAIH,CAAK,CAAC;AAClF;AAGO,SAASI,EAAQL,GAAeC,GAAyB;AAC9D,SAAOF,EAAOC,GAAMC,CAAK,EAAE,IAAI,MAAM;AACvC;AAoBO,SAASK,EACdN,GACAC,GACAM,GACAC,IAAS,GACTC,IAAS,GACD;AAnCH,MAAAP,GAAAQ;AAoCL,QAAMC,IAAW,KAAK,IAAI,GAAG,KAAK,MAAMJ,EAAK,SAASC,KAAUC,CAAM,CAAC,GACjEG,KAAUF,KAAAR,IAAAF,EAAmB,aAAnB,gBAAAE,EAAA,KAAAF,GAA8BC,OAA9B,gBAAAS,EAAsC;AACtD,MAAIE,GAAQ;AACV,UAAMC,IAAI,KAAK,IAAID,EAAO,QAAQD,CAAQ;AAG1C,aAASG,IAAI,GAAGA,IAAID,GAAGC,IAAK,CAAAP,EAAKC,IAASM,IAAIL,CAAM,IAAI,OAAOG,EAAOE,CAAC,CAAC;AACxE,WAAOD;AAAA,EACT;AACA,MAAIC,IAAI;AACR,aAAWV,KAAOJ,GAA2C;AAC3D,QAAIc,KAAKH,EAAU;AACnB,IAAAJ,EAAKC,IAASM,IAAIL,CAAM,IAAI,OAAOL,EAAIH,CAAK,CAAC,GAC7Ca;AAAA,EACF;AACA,SAAOA;AACT;"}
1
+ {"version":3,"file":"arrow.js","sourceRoot":"","sources":["../src/arrow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,4CAA4C;AAC5C,MAAM,UAAU,MAAM,CAAC,IAAa,EAAE,KAAa;IACjD,MAAM,KAAK,GAAI,IAAkB,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAyC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,OAAO,CAAC,IAAa,EAAE,KAAa;IAClD,OAAO,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC;AAKD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,UAAU,CACxB,IAAa,EACb,KAAa,EACb,IAAkB,EAClB,MAAM,GAAG,CAAC,EACV,MAAM,GAAG,CAAC;IAEV,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IACzE,MAAM,MAAM,GAAI,IAAkB,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAChE,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC5C,+FAA+F;QAC/F,kDAAkD;QAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,MAAM,GAAG,IAAI,IAAyC,EAAE,CAAC;QAC5D,IAAI,CAAC,IAAI,QAAQ;YAAE,MAAM;QACzB,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC,EAAE,CAAC;IACN,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
package/dist/engine.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Coordinator } from '@uwdata/mosaic-core';
1
+ import { Coordinator } from "@uwdata/mosaic-core";
2
2
  /**
3
3
  * The page's one database: a DuckDB-WASM instance, the Mosaic `Coordinator` over it, and the file
4
4
  * registry, owned in one place.
@@ -15,14 +15,26 @@ import { Coordinator } from '@uwdata/mosaic-core';
15
15
  * the old lease and registers the new. That indirection is also why the registry is kept rather than
16
16
  * reading `https://…` directly — a view names a stable file, and only the lease behind it rotates.
17
17
  *
18
- * It satisfies fossil's `Engine` structurally; this package does not depend on fossil, and does not
19
- * name `@duckdb/duckdb-wasm` either — the connector hands the database over untyped.
18
+ * **It reads `s3://` from boot.** DuckDB's `httpfs` is loaded before the engine is handed out, so a
19
+ * reader that holds a credential scoped to a prefix makes it readable with one statement through
20
+ * `query` — `CREATE OR REPLACE SECRET … (TYPE s3, …, SCOPE 's3://bucket/prefix/')` — and names the
21
+ * objects by their own URLs. There is no method for it: the secret is SQL, and `query` is the door.
22
+ *
23
+ * **Nothing is fetched from a CDN.** DuckDB-WASM's worker and module and its `httpfs` are named with
24
+ * `new URL(…, import.meta.url)`, so the host's bundler emits them as assets and the page loads all of
25
+ * it from its own origin: `script-src 'self'`, `worker-src 'self'` and `connect-src 'self' <storage>`.
26
+ *
27
+ * It satisfies fossil's `Engine` structurally; this package does not depend on fossil.
20
28
  */
21
29
  export interface Engine {
22
30
  readonly coordinator: Coordinator;
23
31
  /** Rows as objects, uncached: a read after a `lend` must see the new lease. */
24
32
  query(sql: string): Promise<Record<string, unknown>[]>;
25
- /** name → URL. The same URL is a no-op; a different one replaces the lease. */
33
+ /**
34
+ * name → URL. The same URL is a no-op; a different one replaces the lease. The name has no scheme:
35
+ * with `httpfs` loaded, `https://…` or `s3://…` in SQL is read by `httpfs` before the registry is
36
+ * asked, so a lease under such a name is never consulted.
37
+ */
26
38
  lend(files: Record<string, string>): Promise<void>;
27
39
  /** Registers a copy of `bytes` under `name`, replacing whatever was there. */
28
40
  hold(name: string, bytes: Uint8Array): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,qBAAqB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+EAA+E;IAC/E,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACvD,+EAA+E;IAC/E,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,8EAA8E;IAC9E,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAiFD,kDAAkD;AAClD,wBAAgB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAIxC"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiB,MAAM,qBAAqB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+EAA+E;IAC/E,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,8EAA8E;IAC9E,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AA4GD,kDAAkD;AAClD,wBAAgB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAIxC"}
package/dist/engine.js CHANGED
@@ -1,38 +1,103 @@
1
- import { wasmConnector as u, Coordinator as d } from "@uwdata/mosaic-core";
2
- const h = 4, w = { filesystem: { forceFullHTTPReads: !1 } }, y = Symbol("held");
3
- async function b() {
4
- const o = u({ config: w }), c = new d(o), s = await o.getDuckDB();
5
- await c.exec("SET enable_http_metadata_cache = true");
6
- const n = /* @__PURE__ */ new Map();
7
- let f = Promise.resolve();
8
- const r = (e) => {
9
- const t = f.then(e);
10
- return f = t.catch(() => {
11
- }), t;
12
- }, i = async (e) => {
13
- n.has(e) && (await s.dropFile(e), n.delete(e));
14
- };
15
- return {
16
- coordinator: c,
17
- query: (e) => c.query(e, { type: "json", cache: !1 }),
18
- lend: (e) => r(async () => {
19
- for (const [t, l] of Object.entries(e))
20
- n.get(t) !== l && (await i(t), await s.registerFileURL(t, l, h, !1), n.set(t, l));
21
- }),
22
- hold: (e, t) => r(async () => {
23
- await i(e), await s.registerFileBuffer(e, t.slice()), n.set(e, y);
24
- }),
25
- drop: (e) => r(async () => {
26
- for (const t of e) await i(t);
27
- })
28
- };
1
+ import { AsyncDuckDB, DuckDBDataProtocol, VoidLogger, selectBundle } from "@duckdb/duckdb-wasm";
2
+ import { Coordinator, wasmConnector } from "@uwdata/mosaic-core";
3
+ /**
4
+ * DuckDB-WASM >= 1.30 downloads a lent file whole unless full reads are refused outright: its range
5
+ * probe reads `Content-Length` where it meant `Content-Range` and falls through to a whole-file GET
6
+ * (duckdb/duckdb-wasm#2228). Refused, it reaches its `HEAD` fallback and reads by range. Measured on
7
+ * a 160 MiB Parquet file, a count, a point lookup and a max: 1.3 MiB in eight ranges with this, the
8
+ * whole file in one GET without it.
9
+ *
10
+ * It governs `lend` alone. `s3://` and a URL named in SQL go through `httpfs`, which reads by range
11
+ * either way (the same queries: 1.8 MiB in four requests, flag or not). What still lends is a store
12
+ * DuckDB-WASM has no extension for — Azure, reached by a SAS URL — and that is a URL which answers
13
+ * `HEAD`, which the fallback needs.
14
+ */
15
+ const RANGE_READS = { filesystem: { forceFullHTTPReads: false } };
16
+ /** Absolute, against the page: webpack under Next hands back a root-relative URL. */
17
+ const served = (asset) => new URL(asset.href, location.href).href;
18
+ /**
19
+ * One build per bundle `selectBundle` chooses between, each with the `httpfs` built for it. The worker
20
+ * and module are `@duckdb/duckdb-wasm`'s own, the release this package pins, named by specifier as
21
+ * DuckDB-WASM documents for webpack: the bundler resolves it to that dependency. `httpfs` is not on npm,
22
+ * so `scripts/extensions.mjs` fetches the pinned builds into `extensions/` and the tarball carries
23
+ * them. It is `LOAD`ed by URL, which DuckDB accepts under a hashed file name as long as the name still
24
+ * starts `httpfs.` — the entrypoint is looked up by that prefix.
25
+ */
26
+ function builds() {
27
+ return {
28
+ mvp: {
29
+ mainModule: served(new URL("@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm", import.meta.url)),
30
+ mainWorker: served(new URL("@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js", import.meta.url)),
31
+ httpfs: served(new URL("../extensions/wasm_mvp/httpfs.duckdb_extension.wasm", import.meta.url)),
32
+ },
33
+ eh: {
34
+ mainModule: served(new URL("@duckdb/duckdb-wasm/dist/duckdb-eh.wasm", import.meta.url)),
35
+ mainWorker: served(new URL("@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js", import.meta.url)),
36
+ httpfs: served(new URL("../extensions/wasm_eh/httpfs.duckdb_extension.wasm", import.meta.url)),
37
+ },
38
+ };
29
39
  }
30
- const a = Symbol.for("@kanzo-tech/mosaic/engine");
31
- function m() {
32
- const o = globalThis;
33
- return o[a] ?? (o[a] = b()), o[a];
40
+ /** A buffer is not a URL, so a held name never compares equal to a lent one. */
41
+ const HELD = Symbol("held");
42
+ async function boot() {
43
+ const all = builds();
44
+ const { mainModule } = await selectBundle(all);
45
+ const build = mainModule === all.eh.mainModule ? all.eh : all.mvp;
46
+ const duckdb = new AsyncDuckDB(new VoidLogger(), new Worker(build.mainWorker));
47
+ await duckdb.instantiate(build.mainModule);
48
+ const connector = wasmConnector({ duckdb, config: RANGE_READS });
49
+ const coordinator = new Coordinator(connector);
50
+ await coordinator.exec(`LOAD '${build.httpfs}'`);
51
+ // Files are read lazily by range; caching their metadata is what keeps a pan from re-probing.
52
+ await coordinator.exec("SET enable_http_metadata_cache = true");
53
+ const behind = new Map();
54
+ // Registry changes run one at a time: two `lend`s of one name racing each other would each see the
55
+ // old entry, and the second register would be refused.
56
+ let tail = Promise.resolve();
57
+ const serial = (step) => {
58
+ const run = tail.then(step);
59
+ tail = run.catch(() => { });
60
+ return run;
61
+ };
62
+ const release = async (name) => {
63
+ if (!behind.has(name))
64
+ return;
65
+ await duckdb.dropFile(name);
66
+ behind.delete(name);
67
+ };
68
+ return {
69
+ coordinator,
70
+ query: (sql) => coordinator.query(sql, { type: "json", cache: false }),
71
+ lend: (files) => serial(async () => {
72
+ for (const [name, url] of Object.entries(files)) {
73
+ if (behind.get(name) === url)
74
+ continue;
75
+ await release(name);
76
+ await duckdb.registerFileURL(name, url, DuckDBDataProtocol.HTTP, false);
77
+ behind.set(name, url);
78
+ }
79
+ }),
80
+ hold: (name, bytes) => serial(async () => {
81
+ await release(name);
82
+ // A copy, because DuckDB-WASM transfers the buffer to its worker and detaches the caller's.
83
+ await duckdb.registerFileBuffer(name, bytes.slice());
84
+ behind.set(name, HELD);
85
+ }),
86
+ drop: (names) => serial(async () => {
87
+ for (const name of names)
88
+ await release(name);
89
+ }),
90
+ };
34
91
  }
35
- export {
36
- m as engine
37
- };
38
- //# sourceMappingURL=engine.js.map
92
+ /**
93
+ * Keyed on the document's global rather than on this module, so two copies of the package in one
94
+ * bundle still share one database — which is the invariant, not the module identity.
95
+ */
96
+ const KEY = Symbol.for("@kanzo-tech/mosaic/engine");
97
+ /** The page's one engine, booted on first ask. */
98
+ export function engine() {
99
+ const scope = globalThis;
100
+ scope[KEY] ??= boot();
101
+ return scope[KEY];
102
+ }
103
+ //# sourceMappingURL=engine.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sources":["../src/engine.ts"],"sourcesContent":["import { Coordinator, wasmConnector } from \"@uwdata/mosaic-core\";\n\n/**\n * The page's one database: a DuckDB-WASM instance, the Mosaic `Coordinator` over it, and the file\n * registry, owned in one place.\n *\n * **One per document, because vgplot has one.** `MosaicProvider` registers its coordinator as\n * vgplot's process-wide active one, so a second coordinator on the page is a second database the\n * last-mounted chart wins. Every host wrote the same memoised boot to avoid it — three in these\n * docs, one in keasy, one in fossil's playground — and none of them owned the registry beside it.\n *\n * **The registry is the part that was missing.** DuckDB-WASM refuses to register a name a second\n * time under a different URL (`File already registered`), and answers only when the URL is\n * identical. A signed URL is different on every signing, so the second visit to the same data threw.\n * `lend` is the one place that knows what is behind a name: the same URL is a no-op, a new one drops\n * the old lease and registers the new. That indirection is also why the registry is kept rather than\n * reading `https://…` directly — a view names a stable file, and only the lease behind it rotates.\n *\n * It satisfies fossil's `Engine` structurally; this package does not depend on fossil, and does not\n * name `@duckdb/duckdb-wasm` either — the connector hands the database over untyped.\n */\nexport interface Engine {\n readonly coordinator: Coordinator;\n /** Rows as objects, uncached: a read after a `lend` must see the new lease. */\n query(sql: string): Promise<Record<string, unknown>[]>;\n /** name → URL. The same URL is a no-op; a different one replaces the lease. */\n lend(files: Record<string, string>): Promise<void>;\n /** Registers a copy of `bytes` under `name`, replacing whatever was there. */\n hold(name: string, bytes: Uint8Array): Promise<void>;\n /** Unregisters the names. A name the engine does not hold is not an error. */\n drop(names: readonly string[]): Promise<void>;\n}\n\n/** The slice of DuckDB-WASM's `AsyncDuckDB` the registry uses. */\ninterface Registry {\n registerFileURL(name: string, url: string, protocol: number, directIO: boolean): Promise<void>;\n registerFileBuffer(name: string, buffer: Uint8Array): Promise<void>;\n dropFile(name: string): Promise<void>;\n}\n\n/** `DuckDBDataProtocol.HTTP` — a numeric enum, spelled here so the type never has to be imported. */\nconst HTTP = 4;\n\n/**\n * DuckDB-WASM >= 1.30 downloads a lent file whole unless full reads are refused outright: its range\n * probe reads `Content-Length` where it meant `Content-Range` and falls through to a whole-file GET\n * (duckdb/duckdb-wasm#2228). Refused, it reaches its `HEAD` fallback and reads by range, which is the\n * whole reason a file is lent rather than fetched. The fallback needs a URL that answers `HEAD` — a\n * presigned GET does not; the host's redirect that signs per method does.\n */\nconst RANGE_READS = { filesystem: { forceFullHTTPReads: false } };\n\n/** A buffer is not a URL, so a held name never compares equal to a lent one. */\nconst HELD = Symbol(\"held\");\n\nasync function boot(): Promise<Engine> {\n const connector = wasmConnector({ config: RANGE_READS });\n const coordinator = new Coordinator(connector);\n const db = (await connector.getDuckDB()) as unknown as Registry;\n // Lent files are read lazily by range; caching their metadata is what keeps a pan from re-probing.\n await coordinator.exec(\"SET enable_http_metadata_cache = true\");\n\n const behind = new Map<string, string | typeof HELD>();\n\n // Registry changes run one at a time: two `lend`s of one name racing each other would each see the\n // old entry, and the second register would be refused.\n let tail: Promise<unknown> = Promise.resolve();\n const serial = (step: () => Promise<void>): Promise<void> => {\n const run = tail.then(step);\n tail = run.catch(() => {});\n return run;\n };\n\n const release = async (name: string) => {\n if (!behind.has(name)) return;\n await db.dropFile(name);\n behind.delete(name);\n };\n\n return {\n coordinator,\n query: (sql) =>\n coordinator.query(sql, { type: \"json\", cache: false }) as Promise<Record<string, unknown>[]>,\n lend: (files) =>\n serial(async () => {\n for (const [name, url] of Object.entries(files)) {\n if (behind.get(name) === url) continue;\n await release(name);\n await db.registerFileURL(name, url, HTTP, false);\n behind.set(name, url);\n }\n }),\n hold: (name, bytes) =>\n serial(async () => {\n await release(name);\n // A copy, because DuckDB-WASM transfers the buffer to its worker and detaches the caller's.\n await db.registerFileBuffer(name, bytes.slice());\n behind.set(name, HELD);\n }),\n drop: (names) =>\n serial(async () => {\n for (const name of names) await release(name);\n }),\n };\n}\n\n/**\n * Keyed on the document's global rather than on this module, so two copies of the package in one\n * bundle still share one database — which is the invariant, not the module identity.\n */\nconst KEY = Symbol.for(\"@kanzo-tech/mosaic/engine\");\n\n/** The page's one engine, booted on first ask. */\nexport function engine(): Promise<Engine> {\n const scope = globalThis as { [KEY]?: Promise<Engine> };\n scope[KEY] ??= boot();\n return scope[KEY];\n}\n"],"names":["HTTP","RANGE_READS","HELD","boot","connector","wasmConnector","coordinator","Coordinator","db","behind","tail","serial","step","run","release","name","sql","files","url","bytes","names","KEY","engine","scope"],"mappings":";AAyCA,MAAMA,IAAO,GASPC,IAAc,EAAE,YAAY,EAAE,oBAAoB,KAAM,GAGxDC,IAAO,OAAO,MAAM;AAE1B,eAAeC,IAAwB;AACrC,QAAMC,IAAYC,EAAc,EAAE,QAAQJ,GAAa,GACjDK,IAAc,IAAIC,EAAYH,CAAS,GACvCI,IAAM,MAAMJ,EAAU,UAAA;AAE5B,QAAME,EAAY,KAAK,uCAAuC;AAE9D,QAAMG,wBAAa,IAAA;AAInB,MAAIC,IAAyB,QAAQ,QAAA;AACrC,QAAMC,IAAS,CAACC,MAA6C;AAC3D,UAAMC,IAAMH,EAAK,KAAKE,CAAI;AAC1B,WAAAF,IAAOG,EAAI,MAAM,MAAM;AAAA,IAAC,CAAC,GAClBA;AAAA,EACT,GAEMC,IAAU,OAAOC,MAAiB;AACtC,IAAKN,EAAO,IAAIM,CAAI,MACpB,MAAMP,EAAG,SAASO,CAAI,GACtBN,EAAO,OAAOM,CAAI;AAAA,EACpB;AAEA,SAAO;AAAA,IACL,aAAAT;AAAA,IACA,OAAO,CAACU,MACNV,EAAY,MAAMU,GAAK,EAAE,MAAM,QAAQ,OAAO,IAAO;AAAA,IACvD,MAAM,CAACC,MACLN,EAAO,YAAY;AACjB,iBAAW,CAACI,GAAMG,CAAG,KAAK,OAAO,QAAQD,CAAK;AAC5C,QAAIR,EAAO,IAAIM,CAAI,MAAMG,MACzB,MAAMJ,EAAQC,CAAI,GAClB,MAAMP,EAAG,gBAAgBO,GAAMG,GAAKlB,GAAM,EAAK,GAC/CS,EAAO,IAAIM,GAAMG,CAAG;AAAA,IAExB,CAAC;AAAA,IACH,MAAM,CAACH,GAAMI,MACXR,EAAO,YAAY;AACjB,YAAMG,EAAQC,CAAI,GAElB,MAAMP,EAAG,mBAAmBO,GAAMI,EAAM,OAAO,GAC/CV,EAAO,IAAIM,GAAMb,CAAI;AAAA,IACvB,CAAC;AAAA,IACH,MAAM,CAACkB,MACLT,EAAO,YAAY;AACjB,iBAAWI,KAAQK,EAAO,OAAMN,EAAQC,CAAI;AAAA,IAC9C,CAAC;AAAA,EAAA;AAEP;AAMA,MAAMM,IAAM,OAAO,IAAI,2BAA2B;AAG3C,SAASC,IAA0B;AACxC,QAAMC,IAAQ;AACd,SAAAA,EAAAF,OAAAE,EAAAF,KAAelB,EAAA,IACRoB,EAAMF,CAAG;AAClB;"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { MosaicClient, Selection } from '@uwdata/mosaic-core';
2
- import { FilterExpr } from '@uwdata/mosaic-sql';
1
+ import { MosaicClient, type Selection } from "@uwdata/mosaic-core";
2
+ import { type FilterExpr } from "@uwdata/mosaic-sql";
3
3
  /**
4
4
  * The crossfilter adapter for a view whose positions are not in the database.
5
5
  *
@@ -34,7 +34,7 @@ export interface IdSetClientOptions {
34
34
  export declare class IdSetClient extends MosaicClient {
35
35
  #private;
36
36
  constructor(options: IdSetClientOptions);
37
- query(filter?: FilterExpr): import('@uwdata/mosaic-sql').SelectQuery;
37
+ query(filter?: FilterExpr): import("@uwdata/mosaic-sql").SelectQuery;
38
38
  queryResult(data: unknown): this;
39
39
  /** `null` retracts the selection: an empty clause has a null predicate, which the resolver drops. */
40
40
  publish(ids: readonly unknown[] | null): void;
@@ -1,43 +1,35 @@
1
- var o = (t) => {
2
- throw TypeError(t);
3
- };
4
- var n = (t, r, e) => r.has(t) || o("Cannot " + e);
5
- var i = (t, r, e) => (n(t, r, "read from private field"), e ? e.call(t) : r.get(t)), l = (t, r, e) => r.has(t) ? o("Cannot add the same private member more than once") : r instanceof WeakSet ? r.add(t) : r.set(t, e), u = (t, r, e, c) => (n(t, r, "write to private field"), c ? c.call(t, e) : r.set(t, e), e);
6
- import { MosaicClient as d, clausePoints as f } from "@uwdata/mosaic-core";
7
- import { Query as p } from "@uwdata/mosaic-sql";
8
- import { column as y } from "./arrow.js";
9
- var h, s, m, a;
10
- class w extends d {
11
- constructor(e) {
12
- super(e.filterBy);
13
- l(this, h);
14
- l(this, s);
15
- l(this, m);
16
- l(this, a);
17
- u(this, h, e.table), u(this, s, e.idField), u(this, m, e.as), u(this, a, e.onSurvivors);
18
- }
19
- query(e = []) {
20
- return p.from(i(this, h)).select({ id: i(this, s) }).where(e);
21
- }
22
- queryResult(e) {
23
- return i(this, a).call(this, y(e, "id")), this;
24
- }
25
- /** `null` retracts the selection: an empty clause has a null predicate, which the resolver drops. */
26
- publish(e) {
27
- i(this, m).update(
28
- f([i(this, s)], e == null ? void 0 : e.map((c) => [c]), {
29
- source: this,
30
- // A crossfilter normally exempts a client from its own clause, so a brush can be widened
31
- // after it has collapsed the chart under it. A view that FADES an excluded row instead of
32
- // removing it does not need the exemption: the row is still on screen and still
33
- // selectable. Declining it is what makes the selection visible — the fade is the brush.
34
- clients: /* @__PURE__ */ new Set()
35
- })
36
- );
37
- }
1
+ import { MosaicClient, clausePoints } from "@uwdata/mosaic-core";
2
+ import { Query } from "@uwdata/mosaic-sql";
3
+ import { column } from "./arrow.js";
4
+ export class IdSetClient extends MosaicClient {
5
+ #table;
6
+ #idField;
7
+ #as;
8
+ #emit;
9
+ constructor(options) {
10
+ super(options.filterBy);
11
+ this.#table = options.table;
12
+ this.#idField = options.idField;
13
+ this.#as = options.as;
14
+ this.#emit = options.onSurvivors;
15
+ }
16
+ query(filter = []) {
17
+ return Query.from(this.#table).select({ id: this.#idField }).where(filter);
18
+ }
19
+ queryResult(data) {
20
+ this.#emit(column(data, "id"));
21
+ return this;
22
+ }
23
+ /** `null` retracts the selection: an empty clause has a null predicate, which the resolver drops. */
24
+ publish(ids) {
25
+ this.#as.update(clausePoints([this.#idField], ids?.map((id) => [id]), {
26
+ source: this,
27
+ // A crossfilter normally exempts a client from its own clause, so a brush can be widened
28
+ // after it has collapsed the chart under it. A view that FADES an excluded row instead of
29
+ // removing it does not need the exemption: the row is still on screen and still
30
+ // selectable. Declining it is what makes the selection visible — the fade is the brush.
31
+ clients: new Set(),
32
+ }));
33
+ }
38
34
  }
39
- h = new WeakMap(), s = new WeakMap(), m = new WeakMap(), a = new WeakMap();
40
- export {
41
- w as IdSetClient
42
- };
43
- //# sourceMappingURL=id-set-client.js.map
35
+ //# sourceMappingURL=id-set-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"id-set-client.js","sources":["../src/id-set-client.ts"],"sourcesContent":["import { MosaicClient, clausePoints, type Selection } from \"@uwdata/mosaic-core\";\nimport { Query, type FilterExpr } from \"@uwdata/mosaic-sql\";\nimport { column } from \"./arrow.js\";\n\n/**\n * The crossfilter adapter for a view whose positions are not in the database.\n *\n * A `MosaicClient` asks for two things, and this is both:\n *\n * `query(filter)` — which rows survive the page's filters? The coordinator answers with a column\n * of ids, and the view fades everything else. The **observable** half.\n * `publish(ids)` — the user selected these. A points clause, `id IN (…)`, which every other chart\n * on the page filters by. The **controllable** half.\n *\n * The id list is not a shortcut, it is the only thing such a view can say. A Plot brush publishes\n * `weight BETWEEN …`, an interval in data space, because Plot's `x` IS a column. A GPU simulation's\n * `x` exists nowhere in the database, so no predicate over columns can describe the loop the user\n * drew. Enumerating what was hit is the honest translation — and its cost is the length of the `IN`\n * list, which is the real ceiling on the whole approach.\n *\n * Nothing here knows what draws. It is named for what it exchanges rather than for the renderer it\n * was first written against, because a canvas, a map and an imperative widget all reach the\n * crossfilter through exactly this shape.\n */\n\nexport interface IdSetClientOptions {\n /** The relation. Its `idField` column is what gets published and matched. */\n table: string;\n idField: string;\n /** What the view fades by. */\n filterBy: Selection;\n /** Where the selection publishes. */\n as: Selection;\n /** The surviving ids, in relation order, on every filter change. */\n onSurvivors: (ids: readonly unknown[]) => void;\n}\n\nexport class IdSetClient extends MosaicClient {\n #table: string;\n #idField: string;\n #as: Selection;\n #emit: (ids: readonly unknown[]) => void;\n\n constructor(options: IdSetClientOptions) {\n super(options.filterBy);\n this.#table = options.table;\n this.#idField = options.idField;\n this.#as = options.as;\n this.#emit = options.onSurvivors;\n }\n\n override query(filter: FilterExpr = []) {\n return Query.from(this.#table).select({ id: this.#idField }).where(filter);\n }\n\n override queryResult(data: unknown): this {\n this.#emit(column(data, \"id\"));\n return this;\n }\n\n /** `null` retracts the selection: an empty clause has a null predicate, which the resolver drops. */\n publish(ids: readonly unknown[] | null): void {\n this.#as.update(\n clausePoints([this.#idField], ids?.map((id) => [id]), {\n source: this,\n // A crossfilter normally exempts a client from its own clause, so a brush can be widened\n // after it has collapsed the chart under it. A view that FADES an excluded row instead of\n // removing it does not need the exemption: the row is still on screen and still\n // selectable. Declining it is what makes the selection visible — the fade is the brush.\n clients: new Set<MosaicClient>(),\n }),\n );\n }\n}\n"],"names":["IdSetClient","MosaicClient","options","__privateAdd","_table","_idField","_as","_emit","__privateSet","filter","Query","__privateGet","data","column","ids","clausePoints","id"],"mappings":";;;;;;;;;AAqCO,MAAMA,UAAoBC,EAAa;AAAA,EAM5C,YAAYC,GAA6B;AACvC,UAAMA,EAAQ,QAAQ;AANxB,IAAAC,EAAA,MAAAC;AACA,IAAAD,EAAA,MAAAE;AACA,IAAAF,EAAA,MAAAG;AACA,IAAAH,EAAA,MAAAI;AAIE,IAAAC,EAAA,MAAKJ,GAASF,EAAQ,QACtBM,EAAA,MAAKH,GAAWH,EAAQ,UACxBM,EAAA,MAAKF,GAAMJ,EAAQ,KACnBM,EAAA,MAAKD,GAAQL,EAAQ;AAAA,EACvB;AAAA,EAES,MAAMO,IAAqB,IAAI;AACtC,WAAOC,EAAM,KAAKC,EAAA,MAAKP,EAAM,EAAE,OAAO,EAAE,IAAIO,EAAA,MAAKN,GAAA,CAAU,EAAE,MAAMI,CAAM;AAAA,EAC3E;AAAA,EAES,YAAYG,GAAqB;AACxC,WAAAD,EAAA,MAAKJ,GAAL,WAAWM,EAAOD,GAAM,IAAI,IACrB;AAAA,EACT;AAAA;AAAA,EAGA,QAAQE,GAAsC;AAC5C,IAAAH,EAAA,MAAKL,GAAI;AAAA,MACPS,EAAa,CAACJ,EAAA,MAAKN,EAAQ,GAAGS,KAAA,gBAAAA,EAAK,IAAI,CAACE,MAAO,CAACA,CAAE,IAAI;AAAA,QACpD,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,QAKR,6BAAa,IAAA;AAAA,MAAkB,CAChC;AAAA,IAAA;AAAA,EAEL;AACF;AAnCEZ,IAAA,eACAC,IAAA,eACAC,IAAA,eACAC,IAAA;"}
1
+ {"version":3,"file":"id-set-client.js","sourceRoot":"","sources":["../src/id-set-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,YAAY,EAAkB,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAE,KAAK,EAAmB,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAmCpC,MAAM,OAAO,WAAY,SAAQ,YAAY;IAC3C,MAAM,CAAS;IACf,QAAQ,CAAS;IACjB,GAAG,CAAY;IACf,KAAK,CAAoC;IAEzC,YAAY,OAA2B;QACrC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;IACnC,CAAC;IAEQ,KAAK,CAAC,SAAqB,EAAE;QACpC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7E,CAAC;IAEQ,WAAW,CAAC,IAAa;QAChC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,qGAAqG;IACrG,OAAO,CAAC,GAA8B;QACpC,IAAI,CAAC,GAAG,CAAC,MAAM,CACb,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;YACpD,MAAM,EAAE,IAAI;YACZ,yFAAyF;YACzF,0FAA0F;YAC1F,gFAAgF;YAChF,wFAAwF;YACxF,OAAO,EAAE,IAAI,GAAG,EAAgB;SACjC,CAAC,CACH,CAAC;IACJ,CAAC;CACF"}
package/dist/index.d.ts CHANGED
@@ -20,16 +20,16 @@
20
20
  * `Selection` is what makes one copy of Mosaic the easy outcome, and two copies would be two
21
21
  * crossfilters that never hear each other.
22
22
  */
23
- export { Coordinator, MosaicClient, Selection, makeClient, clausePoint, clausePoints, clauseInterval, clauseIntervals, clauseMatch, type SelectionClause, } from '@uwdata/mosaic-core';
24
- export { Query, asc, desc, loadCSV, loadJSON, loadObjects, loadParquet, loadSpatial, loadExtension, type ExprValue, type FilterExpr, } from '@uwdata/mosaic-sql';
23
+ export { Coordinator, MosaicClient, Selection, makeClient, clausePoint, clausePoints, clauseInterval, clauseIntervals, clauseMatch, type SelectionClause, } from "@uwdata/mosaic-core";
24
+ export { Query, asc, desc, loadCSV, loadJSON, loadObjects, loadParquet, loadSpatial, loadExtension, type ExprValue, type FilterExpr, } from "@uwdata/mosaic-sql";
25
25
  /** Ours: turning an Arrow answer into values, which the client protocol does not do for you. */
26
- export { column, fillColumn, numbers, type NumericArray } from './arrow.js';
26
+ export { column, fillColumn, numbers, type NumericArray } from "./arrow.js";
27
27
  /** Ours: the crossfilter adapter for a view whose positions are not in the database. */
28
- export { IdSetClient, type IdSetClientOptions } from './id-set-client.js';
28
+ export { IdSetClient, type IdSetClientOptions } from "./id-set-client.js";
29
29
  /**
30
30
  * Ours: the page's one DuckDB-WASM engine — the coordinator and the file registry beside it. It is
31
31
  * the only door to `wasmConnector`, which is why that is no longer re-exported: a second boot is a
32
32
  * second database, and every host that wrote its own boot also wrote its own registry policy.
33
33
  */
34
- export { engine, type Engine } from './engine.js';
34
+ export { engine, type Engine } from "./engine.js";
35
35
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,31 +1,35 @@
1
- import { Coordinator as l, MosaicClient as a, Selection as t, clauseInterval as r, clauseIntervals as n, clauseMatch as s, clausePoint as c, clausePoints as i, makeClient as u } from "@uwdata/mosaic-core";
2
- import { Query as m, asc as f, desc as p, loadCSV as x, loadExtension as C, loadJSON as S, loadObjects as I, loadParquet as P, loadSpatial as b } from "@uwdata/mosaic-sql";
3
- import { column as M, fillColumn as O, numbers as g } from "./arrow.js";
4
- import { IdSetClient as j } from "./id-set-client.js";
5
- import { engine as q } from "./engine.js";
6
- export {
7
- l as Coordinator,
8
- j as IdSetClient,
9
- a as MosaicClient,
10
- m as Query,
11
- t as Selection,
12
- f as asc,
13
- r as clauseInterval,
14
- n as clauseIntervals,
15
- s as clauseMatch,
16
- c as clausePoint,
17
- i as clausePoints,
18
- M as column,
19
- p as desc,
20
- q as engine,
21
- O as fillColumn,
22
- x as loadCSV,
23
- C as loadExtension,
24
- S as loadJSON,
25
- I as loadObjects,
26
- P as loadParquet,
27
- b as loadSpatial,
28
- u as makeClient,
29
- g as numbers
30
- };
31
- //# sourceMappingURL=index.js.map
1
+ /**
2
+ * The Mosaic conversation, as one import.
3
+ *
4
+ * This package exists because of a bug that shipped. `@kanzo-tech/graph/duckdb` reached its
5
+ * coordinator, clients and clauses through `@kanzo-tech/ui/analytics`, whose barrel re-exports the
6
+ * React charts first — and `@uwdata/vgplot` with them. The import is static, so a host that
7
+ * installed the two peers the documentation asked for, `mosaic-core` and `mosaic-sql`, still could
8
+ * not open the subpath: vgplot came along, unasked and unused. `@kanzo-tech/graph` ended up
9
+ * declaring vgplot as an optional peer to paper over a dependency it never names.
10
+ *
11
+ * The cause was a layer in the wrong package rather than a missing entry in a list. Reading a
12
+ * column out of an Arrow answer and publishing a points clause are not user-interface concerns;
13
+ * they were only in a component library because that is where the first chart needed them.
14
+ *
15
+ * So: the data half lives here, with no React anywhere in it, and both consumers depend on this
16
+ * rather than on each other. `@kanzo-tech/ui/analytics` keeps every chart and re-exports these
17
+ * names unchanged, so nothing on that surface moves.
18
+ *
19
+ * The re-exports below are deliberate rather than lazy. A single import site for `Coordinator` and
20
+ * `Selection` is what makes one copy of Mosaic the easy outcome, and two copies would be two
21
+ * crossfilters that never hear each other.
22
+ */
23
+ export { Coordinator, MosaicClient, Selection, makeClient, clausePoint, clausePoints, clauseInterval, clauseIntervals, clauseMatch, } from "@uwdata/mosaic-core";
24
+ export { Query, asc, desc, loadCSV, loadJSON, loadObjects, loadParquet, loadSpatial, loadExtension, } from "@uwdata/mosaic-sql";
25
+ /** Ours: turning an Arrow answer into values, which the client protocol does not do for you. */
26
+ export { column, fillColumn, numbers } from "./arrow.js";
27
+ /** Ours: the crossfilter adapter for a view whose positions are not in the database. */
28
+ export { IdSetClient } from "./id-set-client.js";
29
+ /**
30
+ * Ours: the page's one DuckDB-WASM engine — the coordinator and the file registry beside it. It is
31
+ * the only door to `wasmConnector`, which is why that is no longer re-exported: a second boot is a
32
+ * second database, and every host that wrote its own boot also wrote its own registry policy.
33
+ */
34
+ export { engine } from "./engine.js";
35
+ //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,eAAe,EACf,WAAW,GAEZ,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,EACL,GAAG,EACH,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,WAAW,EACX,WAAW,EACX,WAAW,EACX,aAAa,GAGd,MAAM,oBAAoB,CAAC;AAE5B,gGAAgG;AAChG,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAqB,MAAM,YAAY,CAAC;AAE5E,wFAAwF;AACxF,OAAO,EAAE,WAAW,EAA2B,MAAM,oBAAoB,CAAC;AAE1E;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAe,MAAM,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@kanzo-tech/mosaic",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "The Mosaic conversation, without React — coordinator, clients and clauses re-exported as one surface, plus the Arrow column reader and the id-set client that every consumer of them was writing by hand.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "sideEffects": false,
8
8
  "files": [
9
9
  "dist",
10
+ "extensions",
10
11
  "README.md"
11
12
  ],
12
13
  "module": "./dist/index.js",
@@ -18,7 +19,12 @@
18
19
  },
19
20
  "./package.json": "./package.json"
20
21
  },
21
- "//peers": "mosaic-core and mosaic-sql are REQUIRED, not optional, and that is the whole point of this package: there is nothing here without them, so a host installs them once and both @kanzo-tech/ui/analytics and @kanzo-tech/graph/duckdb see the same coordinator. Two copies would be two crossfilters. @uwdata/vgplot is deliberately absent — it is a plotting library, it belongs to the charts in @kanzo-tech/ui, and its accidental presence on this path is exactly the bug this package exists to end. @duckdb/duckdb-wasm is absent for the same reason it is absent from @kanzo-tech/graph: nothing here names it, and a host that boots its own DuckDB depends on it directly. The two ranges differ — mosaic-core ^0.29.2, mosaic-sql ^0.29.0 — and that is not drift: mosaic-sql published exactly one release in the 0.29 line, so ^0.29.2 does not resolve. Each range names the newest patch that exists.",
22
+ "//dependencies": "@duckdb/duckdb-wasm is a dependency, exact, not a peer. `engine()` names its worker and module as `new URL(\"@duckdb/duckdb-wasm/dist/…\", import.meta.url)` — the spelling DuckDB-WASM documents for webpack — so the host's bundler emits them from this package's own copy, and they must be the release the JavaScript driving them comes from and the one the pinned httpfs was built for. A range, or a peer the host resolves, would let the three drift. It is also the release mosaic-core pins, so the tree holds one copy; `scripts/extensions.test.mjs` fails when either moves.",
23
+ "//peers": "mosaic-core and mosaic-sql are REQUIRED, not optional, and that is the whole point of this package: there is nothing here without them, so a host installs them once and both @kanzo-tech/ui/analytics and @kanzo-tech/graph/duckdb see the same coordinator. Two copies would be two crossfilters. @uwdata/vgplot is deliberately absent — it is a plotting library, it belongs to the charts in @kanzo-tech/ui, and its accidental presence on this path is exactly the bug this package exists to end. The two ranges differ — mosaic-core ^0.29.2, mosaic-sql ^0.29.0 — and that is not drift: mosaic-sql published exactly one release in the 0.29 line, so ^0.29.2 does not resolve. Each range names the newest patch that exists.",
24
+ "//build": "tsc, not Vite, and the reason is the `new URL(…, import.meta.url)` lines of src/engine.ts. Those expressions have to reach the HOST's bundler verbatim, which is what makes DuckDB's worker, module and httpfs assets of the host's page. Vite's library mode inlines every asset it resolves, and it resolved the extension into a 1,088 kB engine.js of base64; `?no-inline` emitted it instead, but under a root-absolute URL and renamed `httpfs2.…`, and DuckDB looks up an extension's entrypoint by the file name's first segment. Nothing here needed Vite: no JSX, no CSS, no directive, and nothing to bundle, since every import is a package the host resolves. `extensions/` sits beside `dist/` rather than inside it for the same line: `../extensions` resolves identically from `src/` and from `dist/`, so the test and the tarball read the same path.",
25
+ "dependencies": {
26
+ "@duckdb/duckdb-wasm": "1.33.1-dev57.0"
27
+ },
22
28
  "peerDependencies": {
23
29
  "@uwdata/mosaic-core": "^0.29.2",
24
30
  "@uwdata/mosaic-sql": "^0.29.0"
@@ -37,7 +43,7 @@
37
43
  "url": "https://github.com/Kanzo-Tech/ui/issues"
38
44
  },
39
45
  "scripts": {
40
- "build": "vite build",
46
+ "build": "node scripts/extensions.mjs && rm -rf dist && tsc -p tsconfig.build.json",
41
47
  "typecheck": "tsc --noEmit",
42
48
  "test": "vitest run"
43
49
  }