@cyanheads/mcp-ts-core 0.8.7 → 0.8.9

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 (77) hide show
  1. package/CLAUDE.md +2 -0
  2. package/README.md +1 -1
  3. package/biome.json +1 -1
  4. package/changelog/0.8.x/0.8.8.md +11 -0
  5. package/changelog/0.8.x/0.8.9.md +34 -0
  6. package/dist/canvas/core/CanvasInstance.d.ts +43 -0
  7. package/dist/canvas/core/CanvasInstance.d.ts.map +1 -0
  8. package/dist/canvas/core/CanvasInstance.js +63 -0
  9. package/dist/canvas/core/CanvasInstance.js.map +1 -0
  10. package/dist/canvas/core/CanvasRegistry.d.ts +96 -0
  11. package/dist/canvas/core/CanvasRegistry.d.ts.map +1 -0
  12. package/dist/canvas/core/CanvasRegistry.js +250 -0
  13. package/dist/canvas/core/CanvasRegistry.js.map +1 -0
  14. package/dist/canvas/core/DataCanvas.d.ts +49 -0
  15. package/dist/canvas/core/DataCanvas.d.ts.map +1 -0
  16. package/dist/canvas/core/DataCanvas.js +85 -0
  17. package/dist/canvas/core/DataCanvas.js.map +1 -0
  18. package/dist/canvas/core/IDataCanvasProvider.d.ts +47 -0
  19. package/dist/canvas/core/IDataCanvasProvider.d.ts.map +1 -0
  20. package/dist/canvas/core/IDataCanvasProvider.js +10 -0
  21. package/dist/canvas/core/IDataCanvasProvider.js.map +1 -0
  22. package/dist/canvas/core/canvasFactory.d.ts +26 -0
  23. package/dist/canvas/core/canvasFactory.d.ts.map +1 -0
  24. package/dist/canvas/core/canvasFactory.js +63 -0
  25. package/dist/canvas/core/canvasFactory.js.map +1 -0
  26. package/dist/canvas/core/sqlGate.d.ts +107 -0
  27. package/dist/canvas/core/sqlGate.d.ts.map +1 -0
  28. package/dist/canvas/core/sqlGate.js +267 -0
  29. package/dist/canvas/core/sqlGate.js.map +1 -0
  30. package/dist/canvas/index.d.ts +21 -0
  31. package/dist/canvas/index.d.ts.map +1 -0
  32. package/dist/canvas/index.js +19 -0
  33. package/dist/canvas/index.js.map +1 -0
  34. package/dist/canvas/providers/duckdb/DuckdbProvider.d.ts +56 -0
  35. package/dist/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -0
  36. package/dist/canvas/providers/duckdb/DuckdbProvider.js +600 -0
  37. package/dist/canvas/providers/duckdb/DuckdbProvider.js.map +1 -0
  38. package/dist/canvas/providers/duckdb/exportWriter.d.ts +48 -0
  39. package/dist/canvas/providers/duckdb/exportWriter.d.ts.map +1 -0
  40. package/dist/canvas/providers/duckdb/exportWriter.js +119 -0
  41. package/dist/canvas/providers/duckdb/exportWriter.js.map +1 -0
  42. package/dist/canvas/providers/duckdb/schemaSniffer.d.ts +44 -0
  43. package/dist/canvas/providers/duckdb/schemaSniffer.d.ts.map +1 -0
  44. package/dist/canvas/providers/duckdb/schemaSniffer.js +134 -0
  45. package/dist/canvas/providers/duckdb/schemaSniffer.js.map +1 -0
  46. package/dist/canvas/types.d.ts +134 -0
  47. package/dist/canvas/types.d.ts.map +1 -0
  48. package/dist/canvas/types.js +9 -0
  49. package/dist/canvas/types.js.map +1 -0
  50. package/dist/config/index.d.ts +51 -15
  51. package/dist/config/index.d.ts.map +1 -1
  52. package/dist/config/index.js +44 -0
  53. package/dist/config/index.js.map +1 -1
  54. package/dist/core/app.d.ts +8 -0
  55. package/dist/core/app.d.ts.map +1 -1
  56. package/dist/core/app.js +11 -0
  57. package/dist/core/app.js.map +1 -1
  58. package/dist/core/worker.d.ts +7 -0
  59. package/dist/core/worker.d.ts.map +1 -1
  60. package/dist/core/worker.js +1 -0
  61. package/dist/core/worker.js.map +1 -1
  62. package/dist/logs/combined.log +4 -4
  63. package/dist/logs/error.log +4 -4
  64. package/dist/storage/core/storageValidation.d.ts.map +1 -1
  65. package/dist/storage/core/storageValidation.js +7 -1
  66. package/dist/storage/core/storageValidation.js.map +1 -1
  67. package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
  68. package/dist/utils/internal/error-handler/errorHandler.js +2 -1
  69. package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
  70. package/package.json +18 -8
  71. package/skills/api-canvas/SKILL.md +260 -0
  72. package/skills/api-config/SKILL.md +18 -0
  73. package/skills/api-workers/SKILL.md +6 -0
  74. package/skills/report-issue-framework/SKILL.md +1 -0
  75. package/skills/report-issue-local/SKILL.md +2 -0
  76. package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +1 -0
  77. package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -0
package/CLAUDE.md CHANGED
@@ -34,6 +34,7 @@
34
34
  | `/auth` | `checkScopes` | Dynamic scope checking |
35
35
  | `/storage` | `StorageService` | Storage abstraction |
36
36
  | `/storage/types` | `IStorageProvider` | Provider interface |
37
+ | `/canvas` | `DataCanvas`, `CanvasInstance`, `CanvasRegistry`, `IDataCanvasProvider`, `DuckdbProvider`, `assertReadOnlyQuery`, `quoteIdentifier`, ... | DataCanvas primitive (Tier 3, optional peer dep `@duckdb/node-api`); SQL/analytical workspace |
37
38
  | `/utils` | formatting, encoding, network, pagination, logging, runtime, telemetry, token counting, parsers†, sanitization†, scheduling† | All utilities (†optional peer deps) |
38
39
  | `/services` | `OpenRouterProvider`, `SpeechService`, `createSpeechProvider`, `ElevenLabsProvider`, `WhisperProvider`, `GraphService`, provider interfaces and types | LLM, Speech (TTS/STT), Graph services |
39
40
  | `/linter` | `validateDefinitions`, `LintReport`, `LintDiagnostic`, `LintInput`, `LintSeverity` | Definition validation |
@@ -492,6 +493,7 @@ Detailed method signatures, options, and examples live in skill files. Read the
492
493
  | `api-config` | `skills/api-config/SKILL.md` | AppConfig, parseConfig, env vars |
493
494
  | `api-testing` | `skills/api-testing/SKILL.md` | createMockContext, test patterns, MockContextOptions |
494
495
  | `api-workers` | `skills/api-workers/SKILL.md` | createWorkerHandler, CloudflareBindings, Worker runtime |
496
+ | `api-canvas` | `skills/api-canvas/SKILL.md` | DataCanvas primitive: acquire/register/query/export, token-sharing model, SQL gate, lifecycle |
495
497
  | `api-linter` | `skills/api-linter/SKILL.md` | Definition lint rules (`format-parity`, `schema-*`, `name-*`, `server-json-*`, …) — look here when devcheck reports a lint diagnostic |
496
498
  | `add-tool` | `skills/add-tool/SKILL.md` | Scaffold a new MCP tool definition |
497
499
  | `add-app-tool` | `skills/add-app-tool/SKILL.md` | Scaffold an MCP App tool + UI resource pair |
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  <div align="center">
7
7
 
8
- [![Version](https://img.shields.io/badge/Version-0.8.7-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE)
8
+ [![Version](https://img.shields.io/badge/Version-0.8.9-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP Spec](https://img.shields.io/badge/MCP%20Spec-2025--11--25-8A2BE2.svg?style=flat-square)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-11-25/changelog.mdx) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE)
9
9
 
10
10
  [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3
3
  "vcs": {
4
4
  "enabled": true,
5
5
  "clientKind": "git",
@@ -0,0 +1,11 @@
1
+ ---
2
+ summary: "ErrorHandler stops double-writing ended OTel spans (recordException/setStatus); storage decodeCursor no longer leaks server stack traces through McpError.data on malformed cursors."
3
+ breaking: false
4
+ ---
5
+
6
+ # 0.8.8 — 2026-05-01
7
+
8
+ ## Fixed
9
+
10
+ - **`src/utils/internal/error-handler/errorHandler.ts`** ([#93](https://github.com/cyanheads/mcp-ts-core/issues/93)) — `handleError` now guards its OTel block with `activeSpan.isRecording()`, eliminating two `Cannot execute the operation on ended Span` warnings per tool failure under HTTP+OTel (`measureToolExecution` already records and ends the span before re-throwing). Same fix covers the prompt path; resource path was already clean (uses `classifyOnly`).
11
+ - **`src/storage/core/storageValidation.ts`** ([#71](https://github.com/cyanheads/mcp-ts-core/issues/71)) — `decodeCursor` no longer attaches `error.stack` to `McpError.data.rawError` — the stack goes to `logger.warning`, the thrown error carries only `operation` + request context. `McpError.data` is wire-visible (`structuredContent.error.data`/JSON-RPC `error.data`), so a forged cursor against `ctx.state.list(...)` previously leaked framework paths and version markers. Mirrors the pattern in `src/utils/pagination/pagination.ts`.
@@ -0,0 +1,34 @@
1
+ ---
2
+ summary: "DataCanvas primitive lands as a Tier 3 SQL/analytical workspace backed by DuckDB ([#97](https://github.com/cyanheads/mcp-ts-core/issues/97)) — opt-in via CANVAS_PROVIDER_TYPE, fails closed on Cloudflare Workers."
3
+ breaking: false
4
+ ---
5
+
6
+ # 0.8.9 — 2026-05-02
7
+
8
+ First landing of the DataCanvas primitive ([#97](https://github.com/cyanheads/mcp-ts-core/issues/97)) — register tabular data from upstream APIs, run SQL across multiple registered tables, export results. Tier 3, optional peer dep `@duckdb/node-api`, disabled by default.
9
+
10
+ ## Added
11
+
12
+ - **`@cyanheads/mcp-ts-core/canvas` subpath** (`src/canvas/`) — new public export. Surfaces `DataCanvas`, `CanvasInstance`, `CanvasRegistry`, `IDataCanvasProvider`, `DuckdbProvider`, `assertReadOnlyQuery`, `quoteIdentifier`, plus types (`ColumnSchema`, `RegisterTableOptions`, `QueryOptions`, `ExportOptions`, etc.).
13
+ - **`CoreServices.canvas?: DataCanvas`** (`src/core/app.ts`) — wired in `composeServices()` when `CANVAS_PROVIDER_TYPE !== 'none'`. `undefined` when disabled or running on Workers. `ServerHandle.shutdown()` automatically tears down active DuckDB instances and stops the sweeper.
14
+ - **Token-sharing model** — opaque 10-char URL-safe `canvasId` (~10¹⁸ keyspace). Tools accept an optional `canvas_id` input; framework mints on omit, resolves on match, throws `NotFound` on cross-tenant or unknown id (uniform to avoid existence leaks). Composite scope is `(tenantId, canvasId)`.
15
+ - **Lifecycle controls** — sliding TTL (default 24 h, extended on every op), absolute cap from creation (default 7 d), per-tenant active cap (default 100), background `unref`'d sweeper (default 60 s, set 0 to disable). In-memory only in v1; restart drops all canvases.
16
+ - **DuckDB provider** (`src/canvas/providers/duckdb/`) — one DuckDB instance per canvasId with `memory_limit` PRAGMA. SQL gate (`assertReadOnlyQuery`) blocks writes via AST inspection, not regex. Path-sandbox for file exports (CSV/Parquet/JSON) rejects absolute paths and `..` traversal; stream-based exports bypass. Schema sniffer materializes a sample (default 100 rows) when `schema` is omitted on `registerTable`.
17
+ - **Canvas env vars** (`src/config/index.ts`) — `CANVAS_PROVIDER_TYPE` (`none` | `duckdb`), `CANVAS_DEFAULT_MEMORY_LIMIT_MB` (1024), `CANVAS_EXPORT_PATH` (`./.canvas-exports`), `CANVAS_MAX_CANVASES_PER_TENANT` (100), `CANVAS_TTL_MS` (86400000), `CANVAS_ABSOLUTE_CAP_MS` (604800000), `CANVAS_SWEEPER_INTERVAL_MS` (60000), `CANVAS_DEFAULT_ROW_LIMIT` (10000), `CANVAS_SCHEMA_SNIFF_ROWS` (100). Worker bindings include `CANVAS_PROVIDER_TYPE` so explicit `duckdb` triggers the fail-closed factory path instead of silently no-opping.
18
+ - **Cloudflare Workers fail-closed** — `createCanvasService(config)` throws `ConfigurationError` when `CANVAS_PROVIDER_TYPE=duckdb` is set on a Worker (DuckDB has no V8-isolate build). Leave unset or `none` for Worker deployments.
19
+ - **Test coverage** — 7 unit suites (`tests/unit/canvas/`: `canvasFactory`, `CanvasRegistry`, `classifyDuckdbError`, `DataCanvas`, `exportWriter`, `schemaSniffer`, `sqlGate`) and a real-DuckDB smoke test (`tests/smoke/canvas-duckdb.test.ts`).
20
+ - **`surplus-token-idea` issue label** — new option in `.github/ISSUE_TEMPLATE/{bug_report,feature_request}.yml`, mirrored to `templates/.github/ISSUE_TEMPLATE/`. Added to the label tables in `skills/report-issue-framework/SKILL.md` and `skills/report-issue-local/SKILL.md`, plus a `gh label create` line in the latter (color `FF10F0`).
21
+
22
+ ## Docs
23
+
24
+ - **`skills/api-canvas/SKILL.md` v1.0** — new skill. Covers acquire → register → query → export flow, the token-sharing pattern for multi-agent collaboration, env config, lifecycle, SQL gate, and Cloudflare Workers fail-closed behavior.
25
+ - **`skills/api-config/SKILL.md`** — new Canvas env var table; platform support note (Linux/macOS/Windows × x64; Linux/macOS arm64; Windows arm64 unsupported per DuckDB upstream).
26
+ - **`skills/api-workers/SKILL.md`** — DataCanvas-unavailable callout with the exact `ConfigurationError` message and a `if (!ctx.core.canvas) { ... }` guard pattern.
27
+ - **`AGENTS.md` / `CLAUDE.md`** — `/canvas` row added to the exports table; `api-canvas` row added to the skills table.
28
+
29
+ ## Deps
30
+
31
+ - `@biomejs/biome` 2.4.13 → 2.4.14 (schema URL bumped in `biome.json`).
32
+ - `@cloudflare/workers-types` 4.20260501.1 → 4.20260502.1 (daily roll).
33
+ - `zod` 4.4.1 → 4.4.2 (resolutions + runtime dep + peer range).
34
+ - **Added** `@duckdb/node-api` ^1.5.2-r.1 as devDependency and optional peerDependency (Tier 3 — only installed when canvas is enabled).
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview Per-canvas handle returned by {@link DataCanvas.acquire}. Captures
3
+ * `(canvasId, tenantId)` once so callers don't repeat them on every op, and
4
+ * routes each call through the registry's TTL-touch + tenant validation gate
5
+ * before delegating to the provider.
6
+ * @module src/canvas/core/CanvasInstance
7
+ */
8
+ import type { RequestContext } from '../../utils/internal/requestContext.js';
9
+ import type { DescribeOptions, ExportOptions, ExportResult, ExportTarget, QueryOptions, QueryResult, RegisterRows, RegisterTableOptions, RegisterTableResult, TableInfo } from '../types.js';
10
+ import type { CanvasRegistry } from './CanvasRegistry.js';
11
+ import type { IDataCanvasProvider } from './IDataCanvasProvider.js';
12
+ /** Handle bound to a single canvas. Returned from {@link DataCanvas.acquire}. */
13
+ export declare class CanvasInstance {
14
+ /** Opaque canvas token. Surface this to callers; share it across agents. */
15
+ readonly canvasId: string;
16
+ /** Tenant the canvas is bound to. Resolved by the registry from `RequestContext`. */
17
+ readonly tenantId: string;
18
+ private readonly registry;
19
+ private readonly provider;
20
+ private readonly context;
21
+ /** True when {@link DataCanvas.acquire} created this canvas during the current call. */
22
+ readonly isNew: boolean;
23
+ /** ISO 8601 expiry after the most recent operation extended the sliding TTL. */
24
+ expiresAt: string;
25
+ constructor(
26
+ /** Opaque canvas token. Surface this to callers; share it across agents. */
27
+ canvasId: string,
28
+ /** Tenant the canvas is bound to. Resolved by the registry from `RequestContext`. */
29
+ tenantId: string, isNew: boolean, expiresAt: string, registry: CanvasRegistry, provider: IDataCanvasProvider, context: RequestContext);
30
+ /** Register a table on the canvas. */
31
+ registerTable(name: string, rows: RegisterRows, options?: RegisterTableOptions): Promise<RegisterTableResult>;
32
+ /** Run a SQL query against the canvas. */
33
+ query(sql: string, options?: QueryOptions): Promise<QueryResult>;
34
+ /** Export a canvas table to a path or stream target. */
35
+ export(tableName: string, target: ExportTarget, options?: ExportOptions): Promise<ExportResult>;
36
+ /** Describe one or all canvas tables. */
37
+ describe(options?: DescribeOptions): Promise<TableInfo[]>;
38
+ /** Drop a single canvas table. Returns `true` when found and removed. */
39
+ drop(name: string): Promise<boolean>;
40
+ /** Drop every table on the canvas. Returns the number dropped. */
41
+ clear(): Promise<number>;
42
+ }
43
+ //# sourceMappingURL=CanvasInstance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanvasInstance.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/CanvasInstance.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,EACV,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,iFAAiF;AACjF,qBAAa,cAAc;IAOvB,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAGzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAd1B,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;;IAGhB,4EAA4E;IACnE,QAAQ,EAAE,MAAM;IACzB,qFAAqF;IAC5E,QAAQ,EAAE,MAAM,EACzB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACA,QAAQ,EAAE,cAAc,EACxB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,cAAc;IAM1C,sCAAsC;IAChC,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC;IAK/B,0CAA0C;IACpC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;IAKtE,wDAAwD;IAClD,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC;IAKxB,yCAAyC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAK/D,yEAAyE;IACnE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK1C,kEAAkE;IAC5D,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;CAI/B"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @fileoverview Per-canvas handle returned by {@link DataCanvas.acquire}. Captures
3
+ * `(canvasId, tenantId)` once so callers don't repeat them on every op, and
4
+ * routes each call through the registry's TTL-touch + tenant validation gate
5
+ * before delegating to the provider.
6
+ * @module src/canvas/core/CanvasInstance
7
+ */
8
+ /** Handle bound to a single canvas. Returned from {@link DataCanvas.acquire}. */
9
+ export class CanvasInstance {
10
+ canvasId;
11
+ tenantId;
12
+ registry;
13
+ provider;
14
+ context;
15
+ /** True when {@link DataCanvas.acquire} created this canvas during the current call. */
16
+ isNew;
17
+ /** ISO 8601 expiry after the most recent operation extended the sliding TTL. */
18
+ expiresAt;
19
+ constructor(
20
+ /** Opaque canvas token. Surface this to callers; share it across agents. */
21
+ canvasId,
22
+ /** Tenant the canvas is bound to. Resolved by the registry from `RequestContext`. */
23
+ tenantId, isNew, expiresAt, registry, provider, context) {
24
+ this.canvasId = canvasId;
25
+ this.tenantId = tenantId;
26
+ this.registry = registry;
27
+ this.provider = provider;
28
+ this.context = context;
29
+ this.isNew = isNew;
30
+ this.expiresAt = expiresAt;
31
+ }
32
+ /** Register a table on the canvas. */
33
+ async registerTable(name, rows, options) {
34
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
35
+ return await this.provider.registerTable(this.canvasId, name, rows, this.context, options);
36
+ }
37
+ /** Run a SQL query against the canvas. */
38
+ async query(sql, options) {
39
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
40
+ return await this.provider.query(this.canvasId, sql, this.context, options);
41
+ }
42
+ /** Export a canvas table to a path or stream target. */
43
+ async export(tableName, target, options) {
44
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
45
+ return await this.provider.export(this.canvasId, tableName, target, this.context, options);
46
+ }
47
+ /** Describe one or all canvas tables. */
48
+ async describe(options) {
49
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
50
+ return await this.provider.describe(this.canvasId, this.context, options);
51
+ }
52
+ /** Drop a single canvas table. Returns `true` when found and removed. */
53
+ async drop(name) {
54
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
55
+ return await this.provider.drop(this.canvasId, name, this.context);
56
+ }
57
+ /** Drop every table on the canvas. Returns the number dropped. */
58
+ async clear() {
59
+ this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
60
+ return await this.provider.clear(this.canvasId, this.context);
61
+ }
62
+ }
63
+ //# sourceMappingURL=CanvasInstance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanvasInstance.js","sourceRoot":"","sources":["../../../src/canvas/core/CanvasInstance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Canvas lifecycle registry. Keys canvases by (tenantId, canvasId),
3
+ * generates crypto-secure 10-char URL-safe IDs, enforces a sliding 24h TTL with
4
+ * a 7-day absolute cap, and runs a periodic sweeper that destroys expired
5
+ * canvases via the underlying provider. Also enforces the per-tenant active
6
+ * canvas cap (default 100).
7
+ *
8
+ * Public access is gated by {@link DataCanvas} — callers never see this class
9
+ * directly. Tests construct it with a fake provider and a mock `Date.now` to
10
+ * exercise expiry/sweep logic.
11
+ * @module src/canvas/core/CanvasRegistry
12
+ */
13
+ import { type RequestContext } from '../../utils/internal/requestContext.js';
14
+ import type { IDataCanvasProvider } from './IDataCanvasProvider.js';
15
+ /** Tunable lifecycle constants for the registry. */
16
+ export interface CanvasRegistryOptions {
17
+ /** Absolute cap from creation in milliseconds. Default 7d. */
18
+ absoluteCapMs: number;
19
+ /** Maximum active canvases per tenant. Default 100. */
20
+ maxCanvasesPerTenant: number;
21
+ /** Sweeper interval in milliseconds. Default 60s. Set to 0 to disable. */
22
+ sweeperIntervalMs: number;
23
+ /** Sliding TTL in milliseconds. Default 24h. */
24
+ ttlMs: number;
25
+ }
26
+ /**
27
+ * Default lifecycle constants. Mirrored in the config schema; exported so
28
+ * tests and downstream tooling can reference the same numbers.
29
+ */
30
+ export declare const DEFAULT_CANVAS_REGISTRY_OPTIONS: CanvasRegistryOptions;
31
+ /** Result of {@link CanvasRegistry.acquire}. */
32
+ export interface AcquireResult {
33
+ canvasId: string;
34
+ /** Wall-clock expiry as ISO 8601, after the sliding extension. */
35
+ expiresAt: string;
36
+ /** True when the registry created the canvas during this acquire call. */
37
+ isNew: boolean;
38
+ tenantId: string;
39
+ }
40
+ /**
41
+ * Tracks the active canvases for a single process. Not multi-process safe —
42
+ * tokens issued by one process are not portable to another (matches v1 scope
43
+ * in the issue).
44
+ */
45
+ export declare class CanvasRegistry {
46
+ private readonly provider;
47
+ private readonly options;
48
+ /** Injected for tests. Defaults to `Date.now`. */
49
+ private readonly clock;
50
+ private readonly idGenerator;
51
+ private readonly canvases;
52
+ /** Per-tenant index for cap enforcement and listing. */
53
+ private readonly byTenant;
54
+ private sweeperTimer;
55
+ private isShuttingDown;
56
+ constructor(provider: IDataCanvasProvider, options?: CanvasRegistryOptions,
57
+ /** Injected for tests. Defaults to `Date.now`. */
58
+ clock?: () => number);
59
+ /**
60
+ * Resolve an existing canvas or create a new one when `maybeId` is omitted
61
+ * or the supplied id is unknown for the caller's tenant.
62
+ *
63
+ * - Omitted id → create fresh, return `isNew: true`.
64
+ * - Unknown id → throw `NotFound` (caller should retry without an id).
65
+ * - Known id under wrong tenant → throw `NotFound` (uniform with unknown
66
+ * to avoid leaking existence across tenants).
67
+ * - Known + own tenant → touch (extend TTL), return `isNew: false`.
68
+ */
69
+ acquire(maybeId: string | undefined, tenantId: string, context: RequestContext): Promise<AcquireResult>;
70
+ /**
71
+ * Validate that `canvasId` belongs to `tenantId` and is not expired, then
72
+ * extend its TTL and return the resolved expiry. Used by {@link CanvasInstance}
73
+ * before every op so individual operations slide the window.
74
+ */
75
+ touchOrThrow(canvasId: string, tenantId: string): string;
76
+ /**
77
+ * Drop a canvas explicitly (e.g. tenant-initiated cleanup). Returns true
78
+ * when the canvas existed and was destroyed.
79
+ */
80
+ drop(canvasId: string, tenantId: string, context: RequestContext): Promise<boolean>;
81
+ /** Active canvas count for a tenant (used by tests and metrics surfaces). */
82
+ countForTenant(tenantId: string): number;
83
+ /** Total active canvases (used by tests and metrics surfaces). */
84
+ totalActive(): number;
85
+ /** Stop the sweeper and tear down every active canvas. Idempotent. */
86
+ shutdown(context: RequestContext): Promise<void>;
87
+ /** @internal Test/diagnostic hook — runs one sweep pass synchronously. */
88
+ sweep(): Promise<void>;
89
+ private lookup;
90
+ private touch;
91
+ private enforceTenantCap;
92
+ private mintId;
93
+ private indexByTenant;
94
+ private destroy;
95
+ }
96
+ //# sourceMappingURL=CanvasRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanvasRegistry.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/CanvasRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,KAAK,cAAc,EAAyB,MAAM,oCAAoC,CAAC;AAEhG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAoBpE,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,qBAK7C,CAAC;AAEF,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,qBAAa,cAAc;IASvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,kDAAkD;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK;IAXxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,wDAAwD;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;gBAGZ,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,qBAAuD;IACjF,kDAAkD;IACjC,KAAK,GAAE,MAAM,MAAiB;IASjD;;;;;;;;;OASG;IACG,OAAO,CACX,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC;IAmDzB;;;;OAIG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IASxD;;;OAGG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAOzF,6EAA6E;IAC7E,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAIxC,kEAAkE;IAClE,WAAW,IAAI,MAAM;IAIrB,sEAAsE;IAChE,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAYtD,0EAA0E;IACpE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB5B,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,MAAM;IASd,OAAO,CAAC,aAAa;YASP,OAAO;CAiBtB"}
@@ -0,0 +1,250 @@
1
+ /**
2
+ * @fileoverview Canvas lifecycle registry. Keys canvases by (tenantId, canvasId),
3
+ * generates crypto-secure 10-char URL-safe IDs, enforces a sliding 24h TTL with
4
+ * a 7-day absolute cap, and runs a periodic sweeper that destroys expired
5
+ * canvases via the underlying provider. Also enforces the per-tenant active
6
+ * canvas cap (default 100).
7
+ *
8
+ * Public access is gated by {@link DataCanvas} — callers never see this class
9
+ * directly. Tests construct it with a fake provider and a mock `Date.now` to
10
+ * exercise expiry/sweep logic.
11
+ * @module src/canvas/core/CanvasRegistry
12
+ */
13
+ import { conflict, notFound, rateLimited } from '../../types-global/errors.js';
14
+ import { logger } from '../../utils/internal/logger.js';
15
+ import { requestContextService } from '../../utils/internal/requestContext.js';
16
+ import { IdGenerator } from '../../utils/security/idGenerator.js';
17
+ /**
18
+ * Canvas ID character set — URL-safe alphabet matching `nanoid`'s default
19
+ * (A-Z, a-z, 0-9, `-`, `_`). 10 chars × 64 alphabet ≈ 1.15 × 10^18 keyspace.
20
+ */
21
+ const CANVAS_ID_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
22
+ const CANVAS_ID_LENGTH = 10;
23
+ const CANVAS_ID_REGEX = /^[A-Za-z0-9_-]{10}$/;
24
+ /**
25
+ * Default lifecycle constants. Mirrored in the config schema; exported so
26
+ * tests and downstream tooling can reference the same numbers.
27
+ */
28
+ export const DEFAULT_CANVAS_REGISTRY_OPTIONS = {
29
+ ttlMs: 24 * 60 * 60 * 1000,
30
+ absoluteCapMs: 7 * 24 * 60 * 60 * 1000,
31
+ maxCanvasesPerTenant: 100,
32
+ sweeperIntervalMs: 60 * 1000,
33
+ };
34
+ /**
35
+ * Tracks the active canvases for a single process. Not multi-process safe —
36
+ * tokens issued by one process are not portable to another (matches v1 scope
37
+ * in the issue).
38
+ */
39
+ export class CanvasRegistry {
40
+ provider;
41
+ options;
42
+ clock;
43
+ idGenerator = new IdGenerator();
44
+ canvases = new Map();
45
+ /** Per-tenant index for cap enforcement and listing. */
46
+ byTenant = new Map();
47
+ sweeperTimer;
48
+ isShuttingDown = false;
49
+ constructor(provider, options = DEFAULT_CANVAS_REGISTRY_OPTIONS,
50
+ /** Injected for tests. Defaults to `Date.now`. */
51
+ clock = Date.now) {
52
+ this.provider = provider;
53
+ this.options = options;
54
+ this.clock = clock;
55
+ if (options.sweeperIntervalMs > 0) {
56
+ this.sweeperTimer = setInterval(() => void this.sweep(), options.sweeperIntervalMs);
57
+ // Don't keep the event loop alive solely for the sweeper.
58
+ this.sweeperTimer.unref?.();
59
+ }
60
+ }
61
+ /**
62
+ * Resolve an existing canvas or create a new one when `maybeId` is omitted
63
+ * or the supplied id is unknown for the caller's tenant.
64
+ *
65
+ * - Omitted id → create fresh, return `isNew: true`.
66
+ * - Unknown id → throw `NotFound` (caller should retry without an id).
67
+ * - Known id under wrong tenant → throw `NotFound` (uniform with unknown
68
+ * to avoid leaking existence across tenants).
69
+ * - Known + own tenant → touch (extend TTL), return `isNew: false`.
70
+ */
71
+ async acquire(maybeId, tenantId, context) {
72
+ if (this.isShuttingDown) {
73
+ throw notFound('Canvas registry is shutting down.', { tenantId });
74
+ }
75
+ if (maybeId !== undefined) {
76
+ const record = this.lookup(maybeId, tenantId);
77
+ if (!record) {
78
+ throw notFound('Canvas not found or expired. Omit canvas_id to start a new canvas.', {
79
+ canvasId: maybeId,
80
+ });
81
+ }
82
+ this.touch(record);
83
+ return {
84
+ canvasId: record.canvasId,
85
+ tenantId: record.tenantId,
86
+ isNew: false,
87
+ expiresAt: new Date(record.expiresAt).toISOString(),
88
+ };
89
+ }
90
+ this.enforceTenantCap(tenantId);
91
+ const canvasId = this.mintId();
92
+ const now = this.clock();
93
+ const record = {
94
+ canvasId,
95
+ tenantId,
96
+ createdAt: now,
97
+ lastAccessedAt: now,
98
+ expiresAt: now + this.options.ttlMs,
99
+ };
100
+ this.canvases.set(canvasId, record);
101
+ this.indexByTenant(tenantId, canvasId);
102
+ await this.provider.initCanvas(canvasId, context);
103
+ logger.debug('Canvas created.', {
104
+ ...context,
105
+ canvasId,
106
+ tenantId,
107
+ provider: this.provider.name,
108
+ });
109
+ return {
110
+ canvasId,
111
+ tenantId,
112
+ isNew: true,
113
+ expiresAt: new Date(record.expiresAt).toISOString(),
114
+ };
115
+ }
116
+ /**
117
+ * Validate that `canvasId` belongs to `tenantId` and is not expired, then
118
+ * extend its TTL and return the resolved expiry. Used by {@link CanvasInstance}
119
+ * before every op so individual operations slide the window.
120
+ */
121
+ touchOrThrow(canvasId, tenantId) {
122
+ const record = this.lookup(canvasId, tenantId);
123
+ if (!record) {
124
+ throw notFound('Canvas not found or expired.', { canvasId });
125
+ }
126
+ this.touch(record);
127
+ return new Date(record.expiresAt).toISOString();
128
+ }
129
+ /**
130
+ * Drop a canvas explicitly (e.g. tenant-initiated cleanup). Returns true
131
+ * when the canvas existed and was destroyed.
132
+ */
133
+ async drop(canvasId, tenantId, context) {
134
+ const record = this.lookup(canvasId, tenantId);
135
+ if (!record)
136
+ return false;
137
+ await this.destroy(record, context);
138
+ return true;
139
+ }
140
+ /** Active canvas count for a tenant (used by tests and metrics surfaces). */
141
+ countForTenant(tenantId) {
142
+ return this.byTenant.get(tenantId)?.size ?? 0;
143
+ }
144
+ /** Total active canvases (used by tests and metrics surfaces). */
145
+ totalActive() {
146
+ return this.canvases.size;
147
+ }
148
+ /** Stop the sweeper and tear down every active canvas. Idempotent. */
149
+ async shutdown(context) {
150
+ if (this.isShuttingDown)
151
+ return;
152
+ this.isShuttingDown = true;
153
+ if (this.sweeperTimer) {
154
+ clearInterval(this.sweeperTimer);
155
+ this.sweeperTimer = undefined;
156
+ }
157
+ const records = Array.from(this.canvases.values());
158
+ await Promise.allSettled(records.map((r) => this.destroy(r, context)));
159
+ await this.provider.shutdown();
160
+ }
161
+ /** @internal Test/diagnostic hook — runs one sweep pass synchronously. */
162
+ async sweep() {
163
+ if (this.isShuttingDown)
164
+ return;
165
+ const now = this.clock();
166
+ const expired = [];
167
+ for (const record of this.canvases.values()) {
168
+ if (now >= record.expiresAt || now - record.createdAt >= this.options.absoluteCapMs) {
169
+ expired.push(record);
170
+ }
171
+ }
172
+ if (expired.length === 0)
173
+ return;
174
+ const sweepContext = requestContextService.createRequestContext({
175
+ operation: 'CanvasRegistry.sweep',
176
+ });
177
+ await Promise.allSettled(expired.map((r) => this.destroy(r, sweepContext)));
178
+ logger.debug('Canvas sweeper destroyed expired canvases.', {
179
+ ...sweepContext,
180
+ destroyedCount: expired.length,
181
+ });
182
+ }
183
+ // ---------------------------------------------------------------------
184
+ // Internals
185
+ // ---------------------------------------------------------------------
186
+ lookup(canvasId, tenantId) {
187
+ if (!CANVAS_ID_REGEX.test(canvasId))
188
+ return;
189
+ const record = this.canvases.get(canvasId);
190
+ if (!record)
191
+ return;
192
+ if (record.tenantId !== tenantId)
193
+ return;
194
+ const now = this.clock();
195
+ if (now >= record.expiresAt || now - record.createdAt >= this.options.absoluteCapMs) {
196
+ return;
197
+ }
198
+ return record;
199
+ }
200
+ touch(record) {
201
+ const now = this.clock();
202
+ record.lastAccessedAt = now;
203
+ const slidingExpiry = now + this.options.ttlMs;
204
+ const absoluteExpiry = record.createdAt + this.options.absoluteCapMs;
205
+ record.expiresAt = Math.min(slidingExpiry, absoluteExpiry);
206
+ }
207
+ enforceTenantCap(tenantId) {
208
+ const count = this.byTenant.get(tenantId)?.size ?? 0;
209
+ if (count >= this.options.maxCanvasesPerTenant) {
210
+ throw rateLimited(`Tenant has reached the active canvas cap (${this.options.maxCanvasesPerTenant}). Drop unused canvases or wait for the sliding TTL to expire them.`, { tenantId, activeCount: count, cap: this.options.maxCanvasesPerTenant });
211
+ }
212
+ }
213
+ mintId() {
214
+ // Loop-mint to avoid the (vanishingly rare) collision case.
215
+ for (let i = 0; i < 5; i += 1) {
216
+ const id = this.idGenerator.generateRandomString(CANVAS_ID_LENGTH, CANVAS_ID_CHARSET);
217
+ if (!this.canvases.has(id))
218
+ return id;
219
+ }
220
+ throw conflict('Failed to generate a unique canvas ID after 5 attempts.');
221
+ }
222
+ indexByTenant(tenantId, canvasId) {
223
+ let set = this.byTenant.get(tenantId);
224
+ if (!set) {
225
+ set = new Set();
226
+ this.byTenant.set(tenantId, set);
227
+ }
228
+ set.add(canvasId);
229
+ }
230
+ async destroy(record, context) {
231
+ this.canvases.delete(record.canvasId);
232
+ const set = this.byTenant.get(record.tenantId);
233
+ if (set) {
234
+ set.delete(record.canvasId);
235
+ if (set.size === 0)
236
+ this.byTenant.delete(record.tenantId);
237
+ }
238
+ try {
239
+ await this.provider.destroyCanvas(record.canvasId, context);
240
+ }
241
+ catch (err) {
242
+ logger.warning('Provider destroyCanvas failed during sweep.', {
243
+ ...context,
244
+ canvasId: record.canvasId,
245
+ error: err instanceof Error ? err.message : String(err),
246
+ });
247
+ }
248
+ }
249
+ }
250
+ //# sourceMappingURL=CanvasRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanvasRegistry.js","sourceRoot":"","sources":["../../../src/canvas/core/CanvasRegistry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @fileoverview User-facing DataCanvas service. Wraps the provider and registry
3
+ * with debug logging and tenant-id resolution; the registry handles TTL,
4
+ * caps, and provider-keying. Mirrors the StorageService surface pattern but
5
+ * stays OTel-free in v1 (per issue #97 scope).
6
+ * @module src/canvas/core/DataCanvas
7
+ */
8
+ import type { RequestContext } from '../../utils/internal/requestContext.js';
9
+ import type { AcquireOptions } from '../types.js';
10
+ import { CanvasInstance } from './CanvasInstance.js';
11
+ import type { CanvasRegistry } from './CanvasRegistry.js';
12
+ import type { IDataCanvasProvider } from './IDataCanvasProvider.js';
13
+ /**
14
+ * Service entry point for the DataCanvas primitive. Returned from
15
+ * `core.canvas` on `CoreServices` when a canvas provider is configured.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const canvas = await core.canvas!.acquire(input.canvas_id, ctx);
20
+ * await canvas.registerTable('germplasm', rows);
21
+ * const result = await canvas.query('SELECT name FROM germplasm LIMIT 10');
22
+ * return { canvas_id: canvas.canvasId, expires_at: canvas.expiresAt, ... };
23
+ * ```
24
+ */
25
+ export declare class DataCanvas {
26
+ private readonly provider;
27
+ private readonly registry;
28
+ constructor(provider: IDataCanvasProvider, registry: CanvasRegistry);
29
+ /**
30
+ * Resolve an existing canvas or create a new one. The returned
31
+ * {@link CanvasInstance} captures `(canvasId, tenantId)` so subsequent
32
+ * operations don't need to repeat them.
33
+ */
34
+ acquire(maybeId: string | undefined, context: RequestContext, _options?: AcquireOptions): Promise<CanvasInstance>;
35
+ /**
36
+ * Drop a canvas explicitly. Returns true when the canvas existed and was
37
+ * destroyed.
38
+ */
39
+ drop(canvasId: string, context: RequestContext): Promise<boolean>;
40
+ /** Active canvas count for the calling tenant. */
41
+ countForTenant(context: RequestContext): number;
42
+ /** Liveness check on the underlying provider. */
43
+ healthCheck(): Promise<boolean>;
44
+ /** Tear down the registry and provider. Called from `ServerHandle.shutdown()`. */
45
+ shutdown(context: RequestContext): Promise<void>;
46
+ /** @internal Surface the underlying provider for advanced use cases. */
47
+ getProvider(): IDataCanvasProvider;
48
+ }
49
+ //# sourceMappingURL=DataCanvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DataCanvas.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/DataCanvas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAmBpE;;;;;;;;;;;GAWG;AACH,qBAAa,UAAU;IAEnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,cAAc;IAK3C;;;;OAIG;IACG,OAAO,CACX,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,cAAc,EACvB,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,cAAc,CAAC;IAoB1B;;;OAGG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAKvE,kDAAkD;IAClD,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM;IAK/C,iDAAiD;IACjD,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/B,kFAAkF;IAC5E,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,wEAAwE;IACxE,WAAW,IAAI,mBAAmB;CAGnC"}