@altertable/data-app 0.59.1 → 0.62.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/AGENTS.md +5 -3
- package/CONTRIBUTING.md +16 -4
- package/README.md +20 -14
- package/dist/chunks/{contract-jksbmt5q.js → contract-3bnrf5pf.js} +27 -5
- package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
- package/dist/chunks/{contract-tkkc28tg.js → contract-4vrw9zk9.js} +43 -13
- package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
- package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
- package/dist/chunks/contract-farfe948.js.map +10 -0
- package/dist/chunks/contract-nt819swq.js.map +1 -1
- package/dist/chunks/{contract-tqrf3ykr.js → contract-rrm7s5zp.js} +61 -32
- package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
- package/dist/chunks/{contract-mb5nfzwg.js → contract-xjv197ck.js} +178 -31
- package/dist/chunks/contract-xjv197ck.js.map +13 -0
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +5 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +42 -8
- package/dist/embed/index.js.map +5 -4
- package/dist/local.js +191 -79
- package/dist/local.js.map +8 -7
- package/dist/react/embed/index.js +84 -99
- package/dist/react/embed/index.js.map +4 -5
- package/dist/react/index.css +156 -6
- package/dist/react/index.js +313 -152
- package/dist/react/index.js.map +17 -13
- package/dist/server.js +172 -76
- package/dist/server.js.map +7 -6
- package/dist/types/client/iframe.d.ts +14 -0
- package/dist/types/client/index.d.ts +19 -13
- package/dist/types/core/appearance.d.ts +7 -6
- package/dist/types/core/contract.d.ts +3 -3
- package/dist/types/core/messages.d.ts +9 -3
- package/dist/types/core/operation.d.ts +27 -0
- package/dist/types/core/presentation.d.ts +7 -0
- package/dist/types/embed/bridge.d.ts +12 -0
- package/dist/types/embed/host.d.ts +10 -3
- package/dist/types/embed/index.d.ts +7 -4
- package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
- package/dist/types/embed/sql.d.ts +4 -0
- package/dist/types/react/embed/bridge.d.ts +19 -10
- package/dist/types/react/embed/index.d.ts +0 -2
- package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
- package/dist/types/react/ui/SearchInput.d.ts +1 -1
- package/dist/types/react/ui/index.d.ts +2 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
- package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
- package/dist/types/react/view-controls.d.ts +1 -1
- package/dist/{bootstrap.js → worker.js} +161 -29
- package/docs/app-authoring.md +24 -4
- package/docs/appearance.md +21 -8
- package/docs/client.md +40 -3
- package/docs/contract.md +9 -7
- package/docs/embed.md +114 -19
- package/docs/react-embed.md +84 -24
- package/docs/react.md +51 -0
- package/docs/releasing.md +5 -0
- package/docs/server-bun.md +5 -0
- package/docs/starter-agent-instructions.md +4 -2
- package/docs/worker.md +57 -0
- package/package.json +9 -5
- package/dist/chunks/contract-14vxdcrs.js.map +0 -10
- package/dist/chunks/contract-jksbmt5q.js.map +0 -10
- package/dist/chunks/contract-mb5nfzwg.js.map +0 -12
- package/dist/chunks/contract-tkkc28tg.js.map +0 -12
- package/dist/chunks/contract-tqrf3ykr.js.map +0 -11
- package/dist/types/embed/standalone.d.ts +0 -1
- package/dist/types/react/embed/shell.d.ts +0 -10
- package/docs/bootstrap.md +0 -76
package/AGENTS.md
CHANGED
|
@@ -13,12 +13,14 @@ boundaries. Read the documentation for the entry you use:
|
|
|
13
13
|
| Serve locally with Bun | [Bun server](docs/server-bun.md) |
|
|
14
14
|
| Compose views and request states | [React](docs/react.md), [stylesheet](docs/react-styles.md) |
|
|
15
15
|
| Embed apps in a host | [Embedding](docs/embed.md), [React embedding](docs/react-embed.md) |
|
|
16
|
-
|
|
|
16
|
+
| Deploy a Cloudflare runtime | [Worker asset](docs/worker.md) |
|
|
17
17
|
| Configure identity and theme | [Config](docs/config.md), [appearance](docs/appearance.md) |
|
|
18
18
|
| Format values | [Format](docs/format.md) |
|
|
19
19
|
|
|
20
|
-
Import public package entries.
|
|
21
|
-
use `import type` for operation types in browser code.
|
|
20
|
+
Import public package entries. For HTTP apps, keep SQL and execution on the server
|
|
21
|
+
and use `import type` for operation types in browser code. Bundle apps own their
|
|
22
|
+
operations in the browser and send SQL through the authorized host bridge. Keep
|
|
23
|
+
credentials and enforced access/query limits on the backend in both models. Import the React stylesheet
|
|
22
24
|
once in the browser entry. Edit app-owned files, not installed package files.
|
|
23
25
|
|
|
24
26
|
Inspect source data and time coverage before choosing an exploration. Build
|
package/CONTRIBUTING.md
CHANGED
|
@@ -17,14 +17,20 @@ bun run check
|
|
|
17
17
|
`src/server` owns request handling and the local Bun adapter, and `src/react` owns
|
|
18
18
|
React bindings; `src/react/embed` provides hosts without app UI dependencies.
|
|
19
19
|
Components and their styles live together in `src/react/ui`.
|
|
20
|
+
`examples/starter` is the runnable CLI starter port, using public package imports.
|
|
21
|
+
Its workspace dependency points to the built repository package; keep its app-owned
|
|
22
|
+
configuration and agent guidance usable when copied into another project.
|
|
20
23
|
`docs` describes each public entry. `scripts` builds and checks the published
|
|
21
24
|
artifact. Workflow validation lives in `scripts/check-workflows.sh`, and release
|
|
22
25
|
helpers live in `scripts/release`. Generated `dist` files are ignored; edit source
|
|
23
26
|
instead.
|
|
24
27
|
|
|
25
|
-
`src/embed/standalone.ts`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
The build compiles `src/embed/standalone.ts` into an in-memory classic script and
|
|
29
|
+
embeds it in `src/worker/index.ts` to emit the single-file `dist/worker.js`
|
|
30
|
+
deployment asset. The internal bootstrap reads its trusted parent origin from the
|
|
31
|
+
script element; keep app navigation and UI dependencies out of it. Worker request
|
|
32
|
+
policy and HTML live in the package; deployments supply domain and trusted-origin
|
|
33
|
+
bindings.
|
|
28
34
|
|
|
29
35
|
Keep core, client, server, and embed free of React and UI imports. Client and
|
|
30
36
|
server may depend on core; neither may import the other. Embed may depend on core
|
|
@@ -45,6 +51,11 @@ consumers.
|
|
|
45
51
|
|
|
46
52
|
## Checks
|
|
47
53
|
|
|
54
|
+
Run `bun run build` before individual typecheck, lint, or browser checks. Browser
|
|
55
|
+
fixtures import the public package exports and need the generated declarations
|
|
56
|
+
in `dist`. `bun run check` builds first, including on a fresh checkout.
|
|
57
|
+
Release Please owns `CHANGELOG.md`; formatting excludes its generated output.
|
|
58
|
+
|
|
48
59
|
| Command | Purpose |
|
|
49
60
|
| ------------------------- | ------------------------------------------------ |
|
|
50
61
|
| `bun run typecheck` | Validate source, tests, and scripts |
|
|
@@ -53,6 +64,7 @@ consumers.
|
|
|
53
64
|
| `bun run test` | Run contract, transport, and component tests |
|
|
54
65
|
| `bun run build` | Emit ESM, declarations, and the React stylesheet |
|
|
55
66
|
| `bun run test:package` | Check the built npm archive as a consumer |
|
|
67
|
+
| `bun run test:starter` | Typecheck, lint, and build the starter |
|
|
56
68
|
| `bun run test:browser` | Test embedding against built exports in Chromium |
|
|
57
69
|
| `bun run check:workflows` | Validate workflows and shell scripts |
|
|
58
70
|
| `bun run check` | Run all required checks |
|
|
@@ -70,7 +82,7 @@ Flag breaking API changes explicitly.
|
|
|
70
82
|
|
|
71
83
|
Build the package, then run `bash scripts/install-test-browser.sh` once and
|
|
72
84
|
`bun run test:browser` for iframe work. Browser fixtures import public built
|
|
73
|
-
exports;
|
|
85
|
+
exports; runtime HTML comes from the published Worker asset and app scripts are
|
|
74
86
|
bundled independently.
|
|
75
87
|
|
|
76
88
|
CI runs source and packed-package checks, validates workflow syntax, reviews
|
package/README.md
CHANGED
|
@@ -28,26 +28,32 @@ import { mountDataApp } from '@altertable/data-app/react';
|
|
|
28
28
|
|
|
29
29
|
## Documentation
|
|
30
30
|
|
|
31
|
-
| Entry | Use
|
|
32
|
-
| ----------------------------------------- |
|
|
33
|
-
| [/contract](docs/contract.md) | Define typed operations, parsers, and date ranges
|
|
34
|
-
| [/client](docs/client.md) | Call
|
|
35
|
-
| [/server](docs/server.md) | Authorize and execute requests on a hosted server
|
|
36
|
-
| [/server/bun](docs/server-bun.md) | Serve an app locally with Bun
|
|
37
|
-
| [/embed](docs/embed.md) | Host URL apps or sandboxed JavaScript bundles
|
|
38
|
-
| [/
|
|
39
|
-
| [/react/embed](docs/react-embed.md) | Embed apps with React
|
|
40
|
-
| [/react](docs/react.md) | Compose views, filters, widgets, and request states
|
|
41
|
-
| [/react/styles.css](docs/react-styles.md) | Load the React UI stylesheet
|
|
42
|
-
| [/config](docs/config.md) | Define app identity and scope
|
|
43
|
-
| [/appearance](docs/appearance.md) | Configure brand tokens and viewer theme
|
|
44
|
-
| [/format](docs/format.md) | Format dates, counts, ratios, and currencies
|
|
31
|
+
| Entry | Use |
|
|
32
|
+
| ----------------------------------------- | --------------------------------------------------- |
|
|
33
|
+
| [/contract](docs/contract.md) | Define typed operations, parsers, and date ranges |
|
|
34
|
+
| [/client](docs/client.md) | Call HTTP operations or run bundle operations |
|
|
35
|
+
| [/server](docs/server.md) | Authorize and execute requests on a hosted server |
|
|
36
|
+
| [/server/bun](docs/server-bun.md) | Serve an app locally with Bun |
|
|
37
|
+
| [/embed](docs/embed.md) | Host URL apps or sandboxed JavaScript bundles |
|
|
38
|
+
| [/worker](docs/worker.md) | Upload the self-contained Cloudflare Worker asset |
|
|
39
|
+
| [/react/embed](docs/react-embed.md) | Embed apps with a shared React iframe bridge |
|
|
40
|
+
| [/react](docs/react.md) | Compose views, filters, widgets, and request states |
|
|
41
|
+
| [/react/styles.css](docs/react-styles.md) | Load the React UI stylesheet |
|
|
42
|
+
| [/config](docs/config.md) | Define app identity and scope |
|
|
43
|
+
| [/appearance](docs/appearance.md) | Configure brand tokens and viewer theme |
|
|
44
|
+
| [/format](docs/format.md) | Format dates, counts, ratios, and currencies |
|
|
45
45
|
|
|
46
46
|
Start with [app authoring](docs/app-authoring.md) for the complete data flow.
|
|
47
47
|
The package ships these docs alongside its built JavaScript and declarations.
|
|
48
48
|
For agent-assisted work, direct your agent to
|
|
49
49
|
`node_modules/@altertable/data-app/AGENTS.md`.
|
|
50
50
|
|
|
51
|
+
## Starter example
|
|
52
|
+
|
|
53
|
+
The [runnable starter](https://github.com/altertable-ai/data-app/tree/main/examples/starter) includes app configuration,
|
|
54
|
+
server operations, a React connectivity screen, and agent instructions.
|
|
55
|
+
Use it as the starting point for an app that consumes the public package.
|
|
56
|
+
|
|
51
57
|
## Development
|
|
52
58
|
|
|
53
59
|
```fish
|
|
@@ -81,7 +81,7 @@ function defineDataQueryRoute2(operations) {
|
|
|
81
81
|
if (!value || typeof value !== "object")
|
|
82
82
|
throw new Error("Invalid data response.");
|
|
83
83
|
const response = value;
|
|
84
|
-
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object")
|
|
84
|
+
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object" || Array.isArray(response.body))
|
|
85
85
|
throw new Error("Invalid data response.");
|
|
86
86
|
const body = response.body;
|
|
87
87
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -123,11 +123,33 @@ var navigationUpdateRoute2 = /* @__PURE__ */ defineMessageRoute2({
|
|
|
123
123
|
return null;
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
|
+
var sqlQueryRoute2 = defineMessageRoute2({
|
|
127
|
+
input(value) {
|
|
128
|
+
if (!value || typeof value !== "object")
|
|
129
|
+
throw new Error("Invalid SQL query.");
|
|
130
|
+
const query = value;
|
|
131
|
+
if (typeof query.statement !== "string" || !query.statement.trim() || typeof query.limit !== "number" || !Number.isSafeInteger(query.limit) || query.limit < 1)
|
|
132
|
+
throw new Error("Invalid SQL query.");
|
|
133
|
+
return { statement: query.statement, limit: query.limit };
|
|
134
|
+
},
|
|
135
|
+
output(value, input) {
|
|
136
|
+
if (!value || typeof value !== "object")
|
|
137
|
+
throw new Error("Invalid query result.");
|
|
138
|
+
const result = value;
|
|
139
|
+
if (!Array.isArray(result.columns) || !result.columns.every((column) => column && typeof column.name === "string" && (column.type === undefined || typeof column.type === "string")) || !Array.isArray(result.rows) || result.rows.length > input.limit || !result.rows.every((row) => Array.isArray(row) && row.length === result.columns.length) || result.queryId !== undefined && typeof result.queryId !== "string")
|
|
140
|
+
throw new Error("Invalid query result.");
|
|
141
|
+
return {
|
|
142
|
+
columns: result.columns,
|
|
143
|
+
rows: result.rows,
|
|
144
|
+
...result.queryId === undefined ? {} : { queryId: result.queryId }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
126
148
|
var dataAppRoutes2 = {
|
|
127
|
-
"data
|
|
128
|
-
"navigation
|
|
149
|
+
"data:query": /* @__PURE__ */ defineDataQueryRoute2(),
|
|
150
|
+
"navigation:update": navigationUpdateRoute2
|
|
129
151
|
};
|
|
130
152
|
|
|
131
|
-
export { MessageRoutingError2, defineMessageRoute2, createMessageRouter2, defineDataQueryRoute2, navigationUpdateRoute2, dataAppRoutes2 };
|
|
153
|
+
export { MessageRoutingError2, defineMessageRoute2, createMessageRouter2, defineDataQueryRoute2, navigationUpdateRoute2, sqlQueryRoute2, dataAppRoutes2 };
|
|
132
154
|
|
|
133
|
-
//# debugId=
|
|
155
|
+
//# debugId=C13078B8337182CD64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/messages.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { TransportResponse } from '@/src/core/bridge';\nimport type { DisclosedQuery, QueryResult } from '@/src/core/contract';\n\nexport type MessageContext = { signal: AbortSignal };\nexport type RoutedMessage = { route: string; payload: unknown };\nexport type MessageDispatcher = (\n message: unknown,\n context: MessageContext\n) => Promise<unknown>;\nexport type MessageTransport = (\n message: RoutedMessage,\n signal?: AbortSignal\n) => Promise<unknown>;\nexport type MessageRoute<Input, Output> = {\n input: (value: unknown) => Input;\n output: (value: unknown, input: Input) => Output;\n};\nexport type MessageRoutes = Record<\n string,\n {\n input: (value: unknown) => unknown;\n output: (value: unknown, input: never) => unknown;\n }\n>;\nexport type MessageInput<Route extends MessageRoutes[string]> = ReturnType<\n Route['input']\n>;\nexport type MessageHandlers<Routes extends MessageRoutes> = {\n [Name in keyof Routes]: (\n payload: MessageInput<Routes[Name]>,\n context: MessageContext\n ) =>\n | ReturnType<Routes[Name]['output']>\n | Promise<ReturnType<Routes[Name]['output']>>;\n};\n\n// A bootstrap and its app may bundle independent copies in the same window.\nconst errorBrand = Symbol.for('@altertable/data-app/MessageRoutingError');\n\n/** Only these deliberate, public failures cross the message boundary. Other errors stay private. */\nexport class MessageRoutingError extends Error {\n readonly [errorBrand] = true;\n\n static [Symbol.hasInstance](value: unknown): boolean {\n return (\n value instanceof Error &&\n (value as { [errorBrand]?: unknown })[errorBrand] === true\n );\n }\n\n constructor(\n public readonly code: string,\n message: string,\n public readonly requestId?: string\n ) {\n super(message);\n this.name = 'MessageRoutingError';\n }\n}\n\nexport function defineMessageRoute<Input, Output>(\n route: MessageRoute<Input, Output>\n): MessageRoute<Input, Output> {\n return route;\n}\n\n/** Contracts are shared; handlers are supplied by the host and never belong in the app bundle. */\nexport function createMessageRouter<const Routes extends MessageRoutes>(\n routes: Routes,\n handlers: MessageHandlers<NoInfer<Routes>>\n) {\n const registered = new Map(\n Object.entries(routes).map(([name, contract]) => {\n const handler = handlers[name];\n if (!name || name.length > 256 || typeof handler !== 'function')\n throw new Error(\n 'Each message route requires a handler and a bounded name.'\n );\n\n return [name, { contract, handler }] as const;\n })\n );\n\n async function dispatch(\n value: unknown,\n context: MessageContext\n ): Promise<unknown> {\n if (\n !value ||\n typeof value !== 'object' ||\n typeof (value as RoutedMessage).route !== 'string'\n )\n throw new MessageRoutingError(\n 'invalid_message',\n 'Invalid routed message.'\n );\n const message = value as RoutedMessage;\n const entry = registered.get(message.route);\n if (!entry)\n throw new MessageRoutingError('unknown_route', 'Unknown message route.');\n let payload: unknown;\n try {\n payload = entry.contract.input(message.payload);\n } catch {\n throw new MessageRoutingError(\n 'invalid_payload',\n 'Invalid message payload.'\n );\n }\n context.signal.throwIfAborted();\n let result: unknown;\n try {\n result = await entry.handler(payload as never, context);\n } catch (error) {\n if (context.signal.aborted) throw context.signal.reason;\n if (error instanceof MessageRoutingError) throw error;\n throw new MessageRoutingError(\n 'request_failed',\n 'The message request failed. Retry the request.'\n );\n }\n context.signal.throwIfAborted();\n try {\n return entry.contract.output(result, payload as never);\n } catch (error) {\n if (error instanceof MessageRoutingError) throw error;\n throw new MessageRoutingError(\n 'invalid_response',\n 'Invalid message response.'\n );\n }\n }\n\n return { dispatch };\n}\n\nexport type OperationContracts = Record<\n string,\n {\n input: (value: unknown) => unknown;\n output: (value: unknown) => unknown;\n }\n>;\nexport type DataQueryInput<Operations extends OperationContracts> = {\n [Name in keyof Operations & string]: {\n operation: Name;\n input: ReturnType<Operations[Name]['input']>;\n };\n}[keyof Operations & string];\nexport type DataQueryBody<Output> = {\n data: Output;\n requestId: string;\n queriedAt: string;\n queryIds: string[];\n queries?: DisclosedQuery[];\n};\nexport type DataQueryRoute<Operations extends OperationContracts> =\n MessageRoute<DataQueryInput<Operations>, TransportResponse> & {\n operations: Operations | undefined;\n };\nexport type MessageOutput<Route extends MessageRoutes[string], Input> =\n Route extends DataQueryRoute<infer Operations>\n ? Input extends { operation: infer Name extends keyof Operations }\n ? {\n status: number;\n body: DataQueryBody<ReturnType<Operations[Name]['output']>>;\n }\n : never\n : ReturnType<Route['output']>;\n\n/** Omit contracts only in generic hosts such as the CLI shell, where Bun validates operations. */\nexport function defineDataQueryRoute<\n Operations extends OperationContracts = OperationContracts,\n>(operations?: Operations): DataQueryRoute<Operations> {\n return {\n operations,\n input(value) {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid query.');\n const request = value as { operation?: unknown; input?: unknown };\n if (\n typeof request.operation !== 'string' ||\n !request.operation ||\n request.operation.length > 256 ||\n request.operation.includes('/')\n )\n throw new Error('Invalid operation.');\n const operation =\n operations && Object.hasOwn(operations, request.operation)\n ? operations[request.operation]\n : undefined;\n if (operations && !operation) throw new Error('Unknown operation.');\n\n return {\n operation: request.operation,\n input: operation ? operation.input(request.input) : request.input,\n } as DataQueryInput<Operations>;\n },\n output(value, input) {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid data response.');\n const response = value as TransportResponse;\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599 ||\n !response.body ||\n typeof response.body !== 'object' ||\n Array.isArray(response.body)\n )\n throw new Error('Invalid data response.');\n const body = response.body as DataQueryBody<unknown> & {\n error?: { code?: unknown; message?: unknown; requestId?: unknown };\n };\n if (response.status < 200 || response.status >= 300) {\n const failure = body.error;\n throw new MessageRoutingError(\n typeof failure?.code === 'string' ? failure.code : 'request_failed',\n typeof failure?.message === 'string'\n ? failure.message\n : 'Could not load data.',\n typeof failure?.requestId === 'string' ? failure.requestId : undefined\n );\n }\n if (\n !('data' in body) ||\n typeof body.requestId !== 'string' ||\n typeof body.queriedAt !== 'string' ||\n !Array.isArray(body.queryIds) ||\n !body.queryIds.every(id => typeof id === 'string')\n )\n throw new Error('Invalid data envelope.');\n if (\n body.queries !== undefined &&\n (!Array.isArray(body.queries) ||\n !body.queries.every(\n query =>\n query &&\n typeof query === 'object' &&\n typeof query.name === 'string' &&\n typeof query.statement === 'string' &&\n (query.queryId === undefined || typeof query.queryId === 'string')\n ))\n )\n throw new Error('Invalid query evidence.');\n const operation = operations?.[input.operation];\n\n return {\n status: response.status,\n body: {\n ...body,\n data: operation ? operation.output(body.data) : body.data,\n },\n };\n },\n };\n}\n\nexport type NavigationUpdate = {\n search: string;\n hash: string;\n mode: 'push' | 'replace';\n title?: string;\n};\nexport const navigationUpdateRoute = /* @__PURE__ */ defineMessageRoute({\n input(value: unknown): NavigationUpdate {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid navigation.');\n const input = value as NavigationUpdate;\n if (\n typeof input.search !== 'string' ||\n input.search.length > 16_384 ||\n (input.search !== '' && !input.search.startsWith('?')) ||\n typeof input.hash !== 'string' ||\n input.hash.length > 4096 ||\n (input.hash !== '' && !input.hash.startsWith('#')) ||\n (input.mode !== 'push' && input.mode !== 'replace') ||\n (input.title !== undefined &&\n (typeof input.title !== 'string' || input.title.length > 512))\n )\n throw new Error('Invalid navigation.');\n\n return {\n search: input.search,\n hash: input.hash,\n mode: input.mode,\n ...(input.title === undefined ? {} : { title: input.title }),\n };\n },\n output(value: unknown): null {\n if (value !== null) throw new Error('Invalid navigation response.');\n\n return null;\n },\n});\nexport type SqlQueryInput = { statement: string; limit: number };\n\n/** SQL delivery for browser-owned operations. Hosts must enforce backend access and resource limits. */\nexport const sqlQueryRoute = defineMessageRoute({\n input(value: unknown): SqlQueryInput {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid SQL query.');\n const query = value as { statement?: unknown; limit?: unknown };\n if (\n typeof query.statement !== 'string' ||\n !query.statement.trim() ||\n typeof query.limit !== 'number' ||\n !Number.isSafeInteger(query.limit) ||\n query.limit < 1\n )\n throw new Error('Invalid SQL query.');\n\n return { statement: query.statement, limit: query.limit };\n },\n output(value: unknown, input: SqlQueryInput): QueryResult {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid query result.');\n const result = value as QueryResult;\n if (\n !Array.isArray(result.columns) ||\n !result.columns.every(\n column =>\n column &&\n typeof column.name === 'string' &&\n (column.type === undefined || typeof column.type === 'string')\n ) ||\n !Array.isArray(result.rows) ||\n result.rows.length > input.limit ||\n !result.rows.every(\n row => Array.isArray(row) && row.length === result.columns.length\n ) ||\n (result.queryId !== undefined && typeof result.queryId !== 'string')\n )\n throw new Error('Invalid query result.');\n\n return {\n columns: result.columns,\n rows: result.rows,\n ...(result.queryId === undefined ? {} : { queryId: result.queryId }),\n };\n },\n});\n\nexport const dataAppRoutes = {\n 'data:query': /* @__PURE__ */ defineDataQueryRoute(),\n 'navigation:update': navigationUpdateRoute,\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAqCA,IAAM,aAAa,OAAO,IAAI,0CAA0C;AAAA;AAGjE,MAAM,6BAA4B,MAAM;AAAA,EAW3B;AAAA,EAEA;AAAA,GAZR,cAAc;AAAA,UAEhB,OAAO,YAAY,CAAC,OAAyB;AAAA,IACnD,OACE,iBAAiB,SAChB,MAAqC,gBAAgB;AAAA;AAAA,EAI1D,WAAW,CACO,MAChB,SACgB,WAChB;AAAA,IACA,MAAM,OAAO;AAAA,IAJG;AAAA,IAEA;AAAA,IAGhB,KAAK,OAAO;AAAA;AAEhB;AAEO,SAAS,mBAAiC,CAC/C,OAC6B;AAAA,EAC7B,OAAO;AAAA;AAIF,SAAS,oBAAuD,CACrE,QACA,UACA;AAAA,EACA,MAAM,aAAa,IAAI,IACrB,OAAO,QAAQ,MAAM,EAAE,IAAI,EAAE,MAAM,cAAc;AAAA,IAC/C,MAAM,UAAU,SAAS;AAAA,IACzB,IAAI,CAAC,QAAQ,KAAK,SAAS,OAAO,OAAO,YAAY;AAAA,MACnD,MAAM,IAAI,MACR,2DACF;AAAA,IAEF,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ,CAAC;AAAA,GACpC,CACH;AAAA,EAEA,eAAe,QAAQ,CACrB,OACA,SACkB;AAAA,IAClB,IACE,CAAC,SACD,OAAO,UAAU,YACjB,OAAQ,MAAwB,UAAU;AAAA,MAE1C,MAAM,IAAI,qBACR,mBACA,yBACF;AAAA,IACF,MAAM,UAAU;AAAA,IAChB,MAAM,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,IAC1C,IAAI,CAAC;AAAA,MACH,MAAM,IAAI,qBAAoB,iBAAiB,wBAAwB;AAAA,IACzE,IAAI;AAAA,IACJ,IAAI;AAAA,MACF,UAAU,MAAM,SAAS,MAAM,QAAQ,OAAO;AAAA,MAC9C,MAAM;AAAA,MACN,MAAM,IAAI,qBACR,mBACA,0BACF;AAAA;AAAA,IAEF,QAAQ,OAAO,eAAe;AAAA,IAC9B,IAAI;AAAA,IACJ,IAAI;AAAA,MACF,SAAS,MAAM,MAAM,QAAQ,SAAkB,OAAO;AAAA,MACtD,OAAO,OAAO;AAAA,MACd,IAAI,QAAQ,OAAO;AAAA,QAAS,MAAM,QAAQ,OAAO;AAAA,MACjD,IAAI,iBAAiB;AAAA,QAAqB,MAAM;AAAA,MAChD,MAAM,IAAI,qBACR,kBACA,gDACF;AAAA;AAAA,IAEF,QAAQ,OAAO,eAAe;AAAA,IAC9B,IAAI;AAAA,MACF,OAAO,MAAM,SAAS,OAAO,QAAQ,OAAgB;AAAA,MACrD,OAAO,OAAO;AAAA,MACd,IAAI,iBAAiB;AAAA,QAAqB,MAAM;AAAA,MAChD,MAAM,IAAI,qBACR,oBACA,2BACF;AAAA;AAAA;AAAA,EAIJ,OAAO,EAAE,SAAS;AAAA;AAsCb,SAAS,qBAEf,CAAC,YAAqD;AAAA,EACrD,OAAO;AAAA,IACL;AAAA,IACA,KAAK,CAAC,OAAO;AAAA,MACX,IAAI,CAAC,SAAS,OAAO,UAAU;AAAA,QAC7B,MAAM,IAAI,MAAM,gBAAgB;AAAA,MAClC,MAAM,UAAU;AAAA,MAChB,IACE,OAAO,QAAQ,cAAc,YAC7B,CAAC,QAAQ,aACT,QAAQ,UAAU,SAAS,OAC3B,QAAQ,UAAU,SAAS,GAAG;AAAA,QAE9B,MAAM,IAAI,MAAM,oBAAoB;AAAA,MACtC,MAAM,YACJ,cAAc,OAAO,OAAO,YAAY,QAAQ,SAAS,IACrD,WAAW,QAAQ,aACnB;AAAA,MACN,IAAI,cAAc,CAAC;AAAA,QAAW,MAAM,IAAI,MAAM,oBAAoB;AAAA,MAElE,OAAO;AAAA,QACL,WAAW,QAAQ;AAAA,QACnB,OAAO,YAAY,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ;AAAA,MAC9D;AAAA;AAAA,IAEF,MAAM,CAAC,OAAO,OAAO;AAAA,MACnB,IAAI,CAAC,SAAS,OAAO,UAAU;AAAA,QAC7B,MAAM,IAAI,MAAM,wBAAwB;AAAA,MAC1C,MAAM,WAAW;AAAA,MACjB,IACE,CAAC,OAAO,UAAU,SAAS,MAAM,KACjC,SAAS,SAAS,OAClB,SAAS,SAAS,OAClB,CAAC,SAAS,QACV,OAAO,SAAS,SAAS,YACzB,MAAM,QAAQ,SAAS,IAAI;AAAA,QAE3B,MAAM,IAAI,MAAM,wBAAwB;AAAA,MAC1C,MAAM,OAAO,SAAS;AAAA,MAGtB,IAAI,SAAS,SAAS,OAAO,SAAS,UAAU,KAAK;AAAA,QACnD,MAAM,UAAU,KAAK;AAAA,QACrB,MAAM,IAAI,qBACR,OAAO,SAAS,SAAS,WAAW,QAAQ,OAAO,kBACnD,OAAO,SAAS,YAAY,WACxB,QAAQ,UACR,wBACJ,OAAO,SAAS,cAAc,WAAW,QAAQ,YAAY,SAC/D;AAAA,MACF;AAAA,MACA,IACE,EAAE,UAAU,SACZ,OAAO,KAAK,cAAc,YAC1B,OAAO,KAAK,cAAc,YAC1B,CAAC,MAAM,QAAQ,KAAK,QAAQ,KAC5B,CAAC,KAAK,SAAS,MAAM,QAAM,OAAO,OAAO,QAAQ;AAAA,QAEjD,MAAM,IAAI,MAAM,wBAAwB;AAAA,MAC1C,IACE,KAAK,YAAY,cAChB,CAAC,MAAM,QAAQ,KAAK,OAAO,KAC1B,CAAC,KAAK,QAAQ,MACZ,WACE,SACA,OAAO,UAAU,YACjB,OAAO,MAAM,SAAS,YACtB,OAAO,MAAM,cAAc,aAC1B,MAAM,YAAY,aAAa,OAAO,MAAM,YAAY,SAC7D;AAAA,QAEF,MAAM,IAAI,MAAM,yBAAyB;AAAA,MAC3C,MAAM,YAAY,aAAa,MAAM;AAAA,MAErC,OAAO;AAAA,QACL,QAAQ,SAAS;AAAA,QACjB,MAAM;AAAA,aACD;AAAA,UACH,MAAM,YAAY,UAAU,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,QACvD;AAAA,MACF;AAAA;AAAA,EAEJ;AAAA;AASK,IAAM,yCAAwC,oBAAmB;AAAA,EACtE,KAAK,CAAC,OAAkC;AAAA,IACtC,IAAI,CAAC,SAAS,OAAO,UAAU;AAAA,MAC7B,MAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC,MAAM,QAAQ;AAAA,IACd,IACE,OAAO,MAAM,WAAW,YACxB,MAAM,OAAO,SAAS,SACrB,MAAM,WAAW,MAAM,CAAC,MAAM,OAAO,WAAW,GAAG,KACpD,OAAO,MAAM,SAAS,YACtB,MAAM,KAAK,SAAS,QACnB,MAAM,SAAS,MAAM,CAAC,MAAM,KAAK,WAAW,GAAG,KAC/C,MAAM,SAAS,UAAU,MAAM,SAAS,aACxC,MAAM,UAAU,cACd,OAAO,MAAM,UAAU,YAAY,MAAM,MAAM,SAAS;AAAA,MAE3D,MAAM,IAAI,MAAM,qBAAqB;AAAA,IAEvC,OAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM,MAAM;AAAA,MACZ,MAAM,MAAM;AAAA,SACR,MAAM,UAAU,YAAY,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM;AAAA,IAC5D;AAAA;AAAA,EAEF,MAAM,CAAC,OAAsB;AAAA,IAC3B,IAAI,UAAU;AAAA,MAAM,MAAM,IAAI,MAAM,8BAA8B;AAAA,IAElE,OAAO;AAAA;AAEX,CAAC;AAIM,IAAM,iBAAgB,oBAAmB;AAAA,EAC9C,KAAK,CAAC,OAA+B;AAAA,IACnC,IAAI,CAAC,SAAS,OAAO,UAAU;AAAA,MAC7B,MAAM,IAAI,MAAM,oBAAoB;AAAA,IACtC,MAAM,QAAQ;AAAA,IACd,IACE,OAAO,MAAM,cAAc,YAC3B,CAAC,MAAM,UAAU,KAAK,KACtB,OAAO,MAAM,UAAU,YACvB,CAAC,OAAO,cAAc,MAAM,KAAK,KACjC,MAAM,QAAQ;AAAA,MAEd,MAAM,IAAI,MAAM,oBAAoB;AAAA,IAEtC,OAAO,EAAE,WAAW,MAAM,WAAW,OAAO,MAAM,MAAM;AAAA;AAAA,EAE1D,MAAM,CAAC,OAAgB,OAAmC;AAAA,IACxD,IAAI,CAAC,SAAS,OAAO,UAAU;AAAA,MAC7B,MAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC,MAAM,SAAS;AAAA,IACf,IACE,CAAC,MAAM,QAAQ,OAAO,OAAO,KAC7B,CAAC,OAAO,QAAQ,MACd,YACE,UACA,OAAO,OAAO,SAAS,aACtB,OAAO,SAAS,aAAa,OAAO,OAAO,SAAS,SACzD,KACA,CAAC,MAAM,QAAQ,OAAO,IAAI,KAC1B,OAAO,KAAK,SAAS,MAAM,SAC3B,CAAC,OAAO,KAAK,MACX,SAAO,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW,OAAO,QAAQ,MAC7D,KACC,OAAO,YAAY,aAAa,OAAO,OAAO,YAAY;AAAA,MAE3D,MAAM,IAAI,MAAM,uBAAuB;AAAA,IAEzC,OAAO;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO;AAAA,SACT,OAAO,YAAY,YAAY,CAAC,IAAI,EAAE,SAAS,OAAO,QAAQ;AAAA,IACpE;AAAA;AAEJ,CAAC;AAEM,IAAM,iBAAgB;AAAA,EAC3B,8BAA8B,sBAAqB;AAAA,EACnD,qBAAqB;AACvB;",
|
|
8
|
+
"debugId": "C13078B8337182CD64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -3,15 +3,20 @@ import {
|
|
|
3
3
|
createHttpTransport2,
|
|
4
4
|
createMessageClient2,
|
|
5
5
|
localFrameBridge,
|
|
6
|
-
getDataAppTransport2
|
|
7
|
-
|
|
6
|
+
getDataAppTransport2,
|
|
7
|
+
executeDataOperation,
|
|
8
|
+
toDataOperationFailure
|
|
9
|
+
} from "./contract-xjv197ck.js";
|
|
8
10
|
import {
|
|
11
|
+
MessageRoutingError2,
|
|
12
|
+
defineDataQueryRoute2,
|
|
9
13
|
navigationUpdateRoute2
|
|
10
|
-
} from "./contract-
|
|
14
|
+
} from "./contract-3bnrf5pf.js";
|
|
11
15
|
import {
|
|
12
16
|
PARENT_PARAM,
|
|
13
17
|
validLocation
|
|
14
18
|
} from "./contract-ryyf6dme.js";
|
|
19
|
+
|
|
15
20
|
// src/client/location.ts
|
|
16
21
|
function createAppLocation(frame, virtual, publish) {
|
|
17
22
|
let state = { search: "", hash: "" };
|
|
@@ -105,9 +110,9 @@ function attachNavigation(frame, virtual, connection) {
|
|
|
105
110
|
if (disposed)
|
|
106
111
|
return;
|
|
107
112
|
unsubscribe?.();
|
|
108
|
-
const messages = createMessageClient2({ "navigation
|
|
113
|
+
const messages = createMessageClient2({ "navigation:update": navigationUpdateRoute2 }, bridge.request);
|
|
109
114
|
function sendUpdate(update) {
|
|
110
|
-
messages.request("navigation
|
|
115
|
+
messages.request("navigation:update", update).catch(() => {});
|
|
111
116
|
}
|
|
112
117
|
function receiveState(state) {
|
|
113
118
|
if (!state || typeof state !== "object" || !validLocation(state))
|
|
@@ -183,26 +188,51 @@ function getDataAppNavigation2(frame = window) {
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
// src/client/index.ts
|
|
191
|
+
var dataQueryRoute = defineDataQueryRoute2();
|
|
186
192
|
function createDataClient2(options = {}) {
|
|
187
193
|
const http = createHttpTransport2(options);
|
|
194
|
+
const useHttp = options.endpoint !== undefined || options.fetch !== undefined;
|
|
188
195
|
return {
|
|
189
196
|
async query(name, input, { signal } = {}) {
|
|
190
|
-
|
|
197
|
+
if (options.operations) {
|
|
198
|
+
signal?.throwIfAborted();
|
|
199
|
+
const requestId = crypto.randomUUID();
|
|
200
|
+
if (!Object.hasOwn(options.operations, name))
|
|
201
|
+
throw new DataAppError2("Unknown data operation.", "not_found", requestId);
|
|
202
|
+
const lakehouse = options.lakehouse ?? (typeof window === "undefined" ? undefined : getDataAppTransport2()?.lakehouse);
|
|
203
|
+
if (!lakehouse)
|
|
204
|
+
throw new DataAppError2("An authorized SQL bridge must be installed first.", "bridge_unavailable", requestId);
|
|
205
|
+
try {
|
|
206
|
+
const { body } = await executeDataOperation(options.operations[name], input, {
|
|
207
|
+
lakehouse,
|
|
208
|
+
signal: signal ?? new AbortController().signal,
|
|
209
|
+
includeSql: true,
|
|
210
|
+
requestId,
|
|
211
|
+
operationName: name
|
|
212
|
+
});
|
|
213
|
+
return { ...body, input };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (signal?.aborted)
|
|
216
|
+
throw signal.reason;
|
|
217
|
+
if (error instanceof DataAppError2)
|
|
218
|
+
throw error;
|
|
219
|
+
const failure = toDataOperationFailure(error);
|
|
220
|
+
throw new DataAppError2(failure.message, failure.code, requestId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const transport = options.transport ?? (useHttp ? http : localFrameBridge()?.transport ?? http);
|
|
191
224
|
const response = await transport(name, input, signal);
|
|
192
225
|
const ok = response.status >= 200 && response.status < 300;
|
|
193
226
|
let body;
|
|
194
227
|
try {
|
|
195
|
-
|
|
196
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
197
|
-
throw new Error("Invalid response envelope.");
|
|
198
|
-
body = parsed;
|
|
228
|
+
body = dataQueryRoute.output(response, { operation: name, input }).body;
|
|
199
229
|
} catch (error) {
|
|
200
230
|
if (signal?.aborted)
|
|
201
231
|
throw error;
|
|
232
|
+
if (error instanceof MessageRoutingError2)
|
|
233
|
+
throw new DataAppError2(error.message, error.code, error.requestId);
|
|
202
234
|
throw new DataAppError2(ok ? "The data response was invalid." : "Could not load data.", ok ? "invalid_response" : "request_failed");
|
|
203
235
|
}
|
|
204
|
-
if (!ok)
|
|
205
|
-
throw new DataAppError2(body.error?.message ?? "Could not load data.", body.error?.code ?? "request_failed", body.error?.requestId);
|
|
206
236
|
return { ...body, input };
|
|
207
237
|
}
|
|
208
238
|
};
|
|
@@ -210,4 +240,4 @@ function createDataClient2(options = {}) {
|
|
|
210
240
|
|
|
211
241
|
export { createDataAppNavigation2, getDataAppNavigation2, createDataClient2 };
|
|
212
242
|
|
|
213
|
-
//# debugId=
|
|
243
|
+
//# debugId=A9C5A1657EDE5C5C64756E2164756E21
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/client/location.ts", "../src/client/navigation.ts", "../src/client/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { PARENT_PARAM, validLocation } from '@/src/core/bridge';\n\nexport type AppLocation = { search: string; hash: string };\nexport type DataAppLocation = {\n snapshot(): AppLocation;\n subscribe(listener: () => void): () => void;\n update(location: AppLocation, mode?: 'push' | 'replace'): void;\n};\n\n/** Opaque sandboxes keep app state in memory; URL frames retain their real URL for HMR. */\nexport function createAppLocation(\n frame: Window,\n virtual: boolean,\n publish: (location: AppLocation, mode: 'push' | 'replace') => void\n) {\n let state: AppLocation = { search: '', hash: '' };\n let disposed = false;\n const listeners = new Set<() => void>();\n\n function notify() {\n for (const listener of listeners) listener();\n }\n\n function snapshot(): AppLocation {\n if (virtual) return state;\n const url = new URL(frame.location.href);\n url.searchParams.delete(PARENT_PARAM);\n if (state.search !== url.search || state.hash !== url.hash)\n state = { search: url.search, hash: url.hash };\n\n return state;\n }\n\n function apply(next: AppLocation) {\n const previous = snapshot();\n if (previous.search === next.search && previous.hash === next.hash) return;\n if (virtual) state = next;\n else {\n const url = new URL(frame.location.href);\n const parent = url.searchParams.get(PARENT_PARAM);\n url.search = next.search;\n if (parent) url.searchParams.set(PARENT_PARAM, parent);\n url.hash = next.hash;\n frame.history.replaceState(null, '', url);\n frame.dispatchEvent(new Event('popstate'));\n }\n if (virtual) notify();\n }\n\n const location: DataAppLocation = {\n snapshot,\n subscribe(listener) {\n listeners.add(listener);\n if (!virtual) frame.addEventListener('popstate', listener);\n\n return () => {\n listeners.delete(listener);\n frame.removeEventListener('popstate', listener);\n };\n },\n update(next, mode = 'replace') {\n if (disposed) return;\n if (!validLocation(next)) throw new Error('Invalid app location.');\n const before = snapshot();\n if (before.search === next.search && before.hash === next.hash) return;\n apply(next);\n publish(next, mode);\n },\n };\n\n function dispose() {\n disposed = true;\n for (const listener of listeners)\n frame.removeEventListener('popstate', listener);\n listeners.clear();\n }\n\n return { location, apply, dispose };\n}\n",
|
|
6
|
+
"import { createAppLocation, type DataAppLocation } from '@/src/client/location';\nimport { validLocation } from '@/src/core/bridge';\nimport {\n navigationUpdateRoute,\n type NavigationUpdate,\n} from '@/src/core/messages';\nimport { createMessageClient } from '@/src/client/messages';\nimport {\n getDataAppTransport,\n localFrameBridge,\n type IframeTransport,\n} from '@/src/client/iframe';\n\nexport type DataAppNavigation = DataAppLocation & {\n /** Publish URL changes made outside the adapter, including the document title. */\n publish(mode?: 'push' | 'replace'): void;\n dispose(): void;\n};\n\n/** Attach app URL behavior to an already authenticated bridge. */\nexport function createDataAppNavigation({\n bridge,\n window: frame = window,\n}: {\n bridge: IframeTransport;\n window?: Window;\n}): DataAppNavigation {\n return attachNavigation(frame, bridge.mode === 'bundle', bridge).navigation;\n}\n\nfunction attachNavigation(\n frame: Window,\n virtual: boolean,\n connection: IframeTransport | Promise<IframeTransport>\n) {\n let disposed = false;\n let unsubscribe: (() => void) | undefined;\n let send: ((update: NavigationUpdate) => void) | undefined;\n let pendingUpdate: NavigationUpdate | undefined;\n const { location, apply, dispose } = createAppLocation(\n frame,\n virtual,\n publish\n );\n\n function publish(\n location: { search: string; hash: string },\n mode: 'push' | 'replace'\n ) {\n const update = { ...location, mode, title: frame.document.title };\n if (send) send(update);\n else pendingUpdate = update;\n }\n\n function attach(bridge: IframeTransport) {\n if (disposed) return;\n unsubscribe?.();\n const messages = createMessageClient(\n { 'navigation:update': navigationUpdateRoute },\n bridge.request\n );\n\n function sendUpdate(update: NavigationUpdate) {\n void messages.request('navigation:update', update).catch(() => {});\n }\n\n function receiveState(state: unknown) {\n if (!state || typeof state !== 'object' || !validLocation(state)) return;\n pendingUpdate = undefined;\n apply(state as { search: string; hash: string });\n }\n\n send = sendUpdate;\n unsubscribe = bridge.subscribe(receiveState);\n receiveState(bridge.snapshot());\n if (pendingUpdate) {\n sendUpdate(pendingUpdate);\n pendingUpdate = undefined;\n }\n }\n\n if ('then' in connection) void connection.then(attach).catch(() => {});\n else attach(connection);\n\n const navigation: DataAppNavigation = {\n ...location,\n publish(mode = 'replace') {\n if (!disposed) publish(location.snapshot(), mode);\n },\n dispose() {\n disposed = true;\n unsubscribe?.();\n dispose();\n send = undefined;\n pendingUpdate = undefined;\n },\n };\n\n return {\n navigation,\n attach,\n isDisposed() {\n return disposed;\n },\n };\n}\n\nconst navigationKey = Symbol.for('altertable.appNavigation');\ntype NavigationWindow = Window & {\n [navigationKey]?: ReturnType<typeof attachNavigation> & {\n bridge?: IframeTransport;\n };\n};\n\n/** URL controls attach navigation on demand; bootstrap and data-only clients do not. */\nexport function getDataAppNavigation(\n frame: Window = window\n): DataAppNavigation | undefined {\n const target = frame as NavigationWindow;\n const bridge = getDataAppTransport(frame);\n const cached = target[navigationKey]?.isDisposed()\n ? undefined\n : target[navigationKey];\n if (cached) {\n if (bridge && cached.bridge !== bridge) {\n cached.attach(bridge);\n cached.bridge = bridge;\n }\n\n return cached.navigation;\n }\n if (bridge) {\n const adapter = attachNavigation(frame, bridge.mode === 'bundle', bridge);\n target[navigationKey] = { bridge, ...adapter };\n\n return adapter.navigation;\n }\n const preview = localFrameBridge(frame);\n if (!preview?.connect) return undefined;\n const connection = preview.connect();\n const adapter = attachNavigation(frame, false, connection);\n const entry = {\n ...adapter,\n bridge: undefined as IframeTransport | undefined,\n };\n target[navigationKey] = entry;\n void connection\n .then(bridge => {\n entry.bridge = bridge;\n })\n .catch(() => {});\n\n return adapter.navigation;\n}\n",
|
|
7
|
+
"/**\n * Typed clients for HTTP and iframe delivery; safe to import in the browser.\n * @module @altertable/data-app/client\n * @see https://github.com/altertable-ai/data-app/blob/main/docs/client.md\n */\nimport { createHttpTransport, DataAppError } from '@/src/client/transport';\nimport { getDataAppTransport, localFrameBridge } from '@/src/client/iframe';\nimport {\n executeDataOperation,\n toDataOperationFailure,\n} from '@/src/core/operation';\nimport type {\n DataOperations,\n DisclosedQuery,\n Lakehouse,\n} from '@/src/core/contract';\nimport { defineDataQueryRoute, MessageRoutingError } from '@/src/core/messages';\nimport type { DataTransport } from '@/src/client/transport';\n\nexport type InputOf<T> = T extends { input: (value: unknown) => infer Input }\n ? Input\n : never;\nexport type OutputOf<T> = T extends { output: (value: unknown) => infer Output }\n ? Output\n : never;\n\n/**\n * Parsed operation data and query evidence. `queries` is present only when SQL disclosure is\n * allowed.\n */\nexport type DataResponse<Output, Input = unknown> = {\n data: Output;\n /** The exact browser input that produced this response. Never infer it from current controls. */\n input: Input;\n requestId: string;\n queriedAt: string;\n queryIds: string[];\n queries?: DisclosedQuery[];\n};\n\nexport type DataClient<Operations extends DataOperations> = {\n query<Name extends keyof Operations & string>(\n name: Name,\n input: InputOf<Operations[Name]>,\n options?: { signal?: AbortSignal }\n ): Promise<\n DataResponse<OutputOf<Operations[Name]>, InputOf<Operations[Name]>>\n >;\n};\n\n/** Browser operations and named-operation delivery are mutually exclusive configurations. */\nexport type DataClientOptions<Operations extends DataOperations> =\n | {\n operations: Operations;\n lakehouse?: Lakehouse;\n transport?: never;\n endpoint?: never;\n fetch?: never;\n }\n | {\n operations?: never;\n lakehouse?: never;\n transport?: DataTransport;\n endpoint?: string;\n fetch?: typeof fetch;\n };\n\nconst dataQueryRoute = defineDataQueryRoute();\n\n/** Named HTTP operations, or browser-owned operations executed through an authorized SQL bridge. */\nexport function createDataClient<Operations extends DataOperations>(\n options: DataClientOptions<Operations> = {}\n): DataClient<Operations> {\n const http = createHttpTransport(options);\n const useHttp = options.endpoint !== undefined || options.fetch !== undefined;\n\n return {\n async query(name, input, { signal } = {}) {\n if (options.operations) {\n signal?.throwIfAborted();\n const requestId = crypto.randomUUID();\n if (!Object.hasOwn(options.operations, name))\n throw new DataAppError(\n 'Unknown data operation.',\n 'not_found',\n requestId\n );\n const lakehouse =\n options.lakehouse ??\n (typeof window === 'undefined'\n ? undefined\n : getDataAppTransport()?.lakehouse);\n if (!lakehouse)\n throw new DataAppError(\n 'An authorized SQL bridge must be installed first.',\n 'bridge_unavailable',\n requestId\n );\n\n try {\n const { body } = await executeDataOperation(\n options.operations[name]!,\n input,\n {\n lakehouse,\n signal: signal ?? new AbortController().signal,\n includeSql: true,\n requestId,\n operationName: name,\n }\n );\n\n return { ...body, input } as DataResponse<\n OutputOf<Operations[typeof name]>,\n InputOf<Operations[typeof name]>\n >;\n } catch (error) {\n if (signal?.aborted) throw signal.reason;\n if (error instanceof DataAppError) throw error;\n const failure = toDataOperationFailure(error);\n throw new DataAppError(failure.message, failure.code, requestId);\n }\n }\n\n const transport =\n options.transport ??\n (useHttp ? http : (localFrameBridge()?.transport ?? http));\n const response = await transport(name, input, signal);\n const ok = response.status >= 200 && response.status < 300;\n let body: Omit<DataResponse<OutputOf<Operations[typeof name]>>, 'input'>;\n try {\n body = dataQueryRoute.output(response, { operation: name, input })\n .body as typeof body;\n } catch (error) {\n if (signal?.aborted) throw error;\n if (error instanceof MessageRoutingError)\n throw new DataAppError(error.message, error.code, error.requestId);\n throw new DataAppError(\n ok ? 'The data response was invalid.' : 'Could not load data.',\n ok ? 'invalid_response' : 'request_failed'\n );\n }\n\n return { ...body, input };\n },\n };\n}\n\nexport type { DataAppLocation, AppLocation } from '@/src/client/location';\n\nexport {\n createDataAppNavigation,\n getDataAppNavigation,\n type DataAppNavigation,\n} from '@/src/client/navigation';\n\nexport {\n createHttpTransport,\n DataAppError,\n type DataTransport,\n} from '@/src/client/transport';\nexport {\n createIframeTransport,\n installDataAppTransport,\n getDataAppTransport,\n type IframeTransport,\n} from '@/src/client/iframe';\nexport { createMessageClient } from '@/src/client/messages';\n\nexport type { DataAppPresentation } from '@/src/core/presentation';\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "A9C5A1657EDE5C5C64756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/core/appearance.ts
|
|
6
6
|
var defaults = {
|
|
7
|
-
|
|
7
|
+
theme: "light",
|
|
8
8
|
baseColor: "neutral",
|
|
9
9
|
accentColor: "#405d47",
|
|
10
10
|
darkAccentColor: "#a6c4ad",
|
|
@@ -41,7 +41,7 @@ function parseAppearance2(value) {
|
|
|
41
41
|
typography: { ...defaults.typography }
|
|
42
42
|
};
|
|
43
43
|
invariant(record(value) && Object.keys(value).every((key) => [
|
|
44
|
-
"
|
|
44
|
+
"theme",
|
|
45
45
|
"baseColor",
|
|
46
46
|
"accentColor",
|
|
47
47
|
"darkAccentColor",
|
|
@@ -52,10 +52,10 @@ function parseAppearance2(value) {
|
|
|
52
52
|
"typography"
|
|
53
53
|
].includes(key)), "Invalid appearance settings.");
|
|
54
54
|
const typography = value.typography;
|
|
55
|
-
const validFields = (value.
|
|
55
|
+
const validFields = (value.theme === undefined || oneOf(value.theme, ["light", "dark", "system"])) && (value.baseColor === undefined || oneOf(value.baseColor, ["neutral", "slate", "warm"])) && (value.accentColor === undefined || color(value.accentColor)) && (value.darkAccentColor === undefined || color(value.darkAccentColor)) && (value.chartColors === undefined || Array.isArray(value.chartColors) && value.chartColors.length >= 1 && value.chartColors.length <= 8 && value.chartColors.every(color)) && (value.density === undefined || oneOf(value.density, ["compact", "comfortable", "spacious"])) && (value.cornerRadius === undefined || oneOf(value.cornerRadius, ["none", "small", "medium", "large"])) && (value.elevation === undefined || oneOf(value.elevation, ["flat", "subtle", "raised"])) && (typography === undefined || record(typography) && Object.keys(typography).every((key) => key === "body" || key === "heading") && (typography.body === undefined || font(typography.body)) && (typography.heading === undefined || font(typography.heading)));
|
|
56
56
|
invariant(validFields, "Invalid appearance settings.");
|
|
57
57
|
return {
|
|
58
|
-
|
|
58
|
+
theme: value.theme ?? defaults.theme,
|
|
59
59
|
baseColor: value.baseColor ?? defaults.baseColor,
|
|
60
60
|
accentColor: value.accentColor ?? defaults.accentColor,
|
|
61
61
|
darkAccentColor: value.darkAccentColor ?? (value.accentColor === undefined ? defaults.darkAccentColor : undefined),
|
|
@@ -119,7 +119,7 @@ function applyAppearance2(value) {
|
|
|
119
119
|
"--at-shadow-surface": settings.elevation === "flat" ? "none" : settings.elevation === "raised" ? "0 8px 28px rgb(20 28 40 / 10%)" : "0 3px 16px rgb(20 28 40 / 5%)"
|
|
120
120
|
};
|
|
121
121
|
function applyColors() {
|
|
122
|
-
const dark = settings.
|
|
122
|
+
const dark = settings.theme === "dark" || settings.theme === "system" && preference.matches;
|
|
123
123
|
const [background, surfaceColor, subtle, text, muted, border] = palettes[settings.baseColor][dark ? "dark" : "light"];
|
|
124
124
|
const accent = dark ? settings.darkAccentColor ?? `color-mix(in srgb, ${settings.accentColor} 50%, white)` : settings.accentColor;
|
|
125
125
|
settings.chartColors.forEach((chartColor, index) => {
|
|
@@ -148,35 +148,31 @@ function applyAppearance2(value) {
|
|
|
148
148
|
root.style.setProperty(name, token);
|
|
149
149
|
}
|
|
150
150
|
applyColors();
|
|
151
|
-
if (settings.
|
|
151
|
+
if (settings.theme === "system")
|
|
152
152
|
preference.addEventListener("change", applyColors);
|
|
153
153
|
return () => {
|
|
154
154
|
preference.removeEventListener("change", applyColors);
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
-
function createThemeController2(
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
let mode = settings.mode;
|
|
157
|
+
function createThemeController2(initialTheme = "light") {
|
|
158
|
+
const storageKey = "altertable.data-app.theme";
|
|
159
|
+
let theme = initialTheme;
|
|
161
160
|
try {
|
|
162
161
|
const saved = window.localStorage.getItem(storageKey);
|
|
163
162
|
if (oneOf(saved, ["light", "dark", "system"]))
|
|
164
|
-
|
|
163
|
+
theme = saved;
|
|
165
164
|
} catch {}
|
|
166
|
-
let stopAppearance = applyAppearance2({ ...settings, mode });
|
|
167
165
|
const listeners = new Set;
|
|
168
166
|
return {
|
|
169
|
-
|
|
170
|
-
return
|
|
167
|
+
getTheme() {
|
|
168
|
+
return theme;
|
|
171
169
|
},
|
|
172
|
-
|
|
173
|
-
if (
|
|
170
|
+
setTheme(nextTheme) {
|
|
171
|
+
if (nextTheme === theme)
|
|
174
172
|
return;
|
|
175
|
-
|
|
176
|
-
mode = nextMode;
|
|
177
|
-
stopAppearance = applyAppearance2({ ...settings, mode });
|
|
173
|
+
theme = nextTheme;
|
|
178
174
|
try {
|
|
179
|
-
window.localStorage.setItem(storageKey,
|
|
175
|
+
window.localStorage.setItem(storageKey, theme);
|
|
180
176
|
} catch {}
|
|
181
177
|
listeners.forEach((listener) => listener());
|
|
182
178
|
},
|
|
@@ -189,4 +185,4 @@ function createThemeController2(value) {
|
|
|
189
185
|
|
|
190
186
|
export { parseAppearance2, applyAppearance2, createThemeController2 };
|
|
191
187
|
|
|
192
|
-
//# debugId=
|
|
188
|
+
//# debugId=90CD7598768B570C64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/appearance.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Brand settings; parsing is portable, while theme application requires the browser.\n * @module @altertable/data-app/appearance\n * @see https://github.com/altertable-ai/data-app/blob/main/docs/appearance.md\n */\nimport { invariant } from '@/src/core/invariant';\n\nexport type Theme = 'light' | 'dark';\nexport type ThemePreference = Theme | 'system';\n\nexport type AppearanceSettings = {\n theme: ThemePreference;\n baseColor: 'neutral' | 'slate' | 'warm';\n accentColor: string;\n darkAccentColor?: string;\n chartColors: string[];\n density: 'compact' | 'comfortable' | 'spacious';\n cornerRadius: 'none' | 'small' | 'medium' | 'large';\n elevation: 'flat' | 'subtle' | 'raised';\n typography: { body: string; heading: string };\n};\n\nexport type ThemeController = {\n getTheme: () => ThemePreference;\n setTheme: (theme: ThemePreference) => void;\n subscribe: (listener: () => void) => () => void;\n};\n\nconst defaults: AppearanceSettings = {\n theme: 'light',\n baseColor: 'neutral',\n accentColor: '#405d47',\n darkAccentColor: '#a6c4ad',\n chartColors: [\n '#285fc0',\n '#a95319',\n '#147862',\n '#7243aa',\n '#aa3958',\n '#475569',\n ],\n density: 'comfortable',\n cornerRadius: 'medium',\n elevation: 'subtle',\n typography: { body: 'system', heading: 'system' },\n};\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction oneOf<T extends string>(\n value: unknown,\n options: readonly T[]\n): value is T {\n return typeof value === 'string' && options.includes(value as T);\n}\n\nfunction color(value: unknown): value is string {\n return typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value);\n}\n\nfunction font(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= 120 &&\n !/[;{}()]/.test(value)\n );\n}\n\nexport function parseAppearance(value: unknown): AppearanceSettings {\n if (value === undefined)\n return {\n ...defaults,\n chartColors: [...defaults.chartColors],\n typography: { ...defaults.typography },\n };\n invariant(\n record(value) &&\n Object.keys(value).every(key =>\n [\n 'theme',\n 'baseColor',\n 'accentColor',\n 'darkAccentColor',\n 'chartColors',\n 'density',\n 'cornerRadius',\n 'elevation',\n 'typography',\n ].includes(key)\n ),\n 'Invalid appearance settings.'\n );\n const typography = value.typography;\n const validFields =\n (value.theme === undefined ||\n oneOf(value.theme, ['light', 'dark', 'system'])) &&\n (value.baseColor === undefined ||\n oneOf(value.baseColor, ['neutral', 'slate', 'warm'])) &&\n (value.accentColor === undefined || color(value.accentColor)) &&\n (value.darkAccentColor === undefined || color(value.darkAccentColor)) &&\n (value.chartColors === undefined ||\n (Array.isArray(value.chartColors) &&\n value.chartColors.length >= 1 &&\n value.chartColors.length <= 8 &&\n value.chartColors.every(color))) &&\n (value.density === undefined ||\n oneOf(value.density, ['compact', 'comfortable', 'spacious'])) &&\n (value.cornerRadius === undefined ||\n oneOf(value.cornerRadius, ['none', 'small', 'medium', 'large'])) &&\n (value.elevation === undefined ||\n oneOf(value.elevation, ['flat', 'subtle', 'raised'])) &&\n (typography === undefined ||\n (record(typography) &&\n Object.keys(typography).every(\n key => key === 'body' || key === 'heading'\n ) &&\n (typography.body === undefined || font(typography.body)) &&\n (typography.heading === undefined || font(typography.heading))));\n invariant(validFields, 'Invalid appearance settings.');\n\n return {\n theme: (value.theme ?? defaults.theme) as AppearanceSettings['theme'],\n baseColor: (value.baseColor ??\n defaults.baseColor) as AppearanceSettings['baseColor'],\n accentColor: (value.accentColor ?? defaults.accentColor) as string,\n darkAccentColor: (value.darkAccentColor ??\n (value.accentColor === undefined\n ? defaults.darkAccentColor\n : undefined)) as string | undefined,\n chartColors: (value.chartColors ?? defaults.chartColors) as string[],\n density: (value.density ??\n defaults.density) as AppearanceSettings['density'],\n cornerRadius: (value.cornerRadius ??\n defaults.cornerRadius) as AppearanceSettings['cornerRadius'],\n elevation: (value.elevation ??\n defaults.elevation) as AppearanceSettings['elevation'],\n typography: {\n body:\n record(typography) && typeof typography.body === 'string'\n ? typography.body\n : defaults.typography.body,\n heading:\n record(typography) && typeof typography.heading === 'string'\n ? typography.heading\n : defaults.typography.heading,\n },\n };\n}\n\nconst palettes = {\n neutral: {\n light: ['#ffffff', '#ffffff', '#f6f7f7', '#202124', '#687078', '#e3e6e8'],\n dark: ['#151719', '#202326', '#292d31', '#f2f3f4', '#aeb5bc', '#3b4248'],\n },\n slate: {\n light: ['#f8fafc', '#ffffff', '#f1f5f9', '#17212f', '#607083', '#dce3ea'],\n dark: ['#111820', '#1b2530', '#263340', '#f0f4f8', '#a8b5c3', '#3a4857'],\n },\n warm: {\n light: ['#fbfaf8', '#ffffff', '#f5f2ed', '#292723', '#736e66', '#e8e2da'],\n dark: ['#1b1916', '#25221e', '#302c27', '#f5f1eb', '#bcb3a8', '#494239'],\n },\n} as const;\n\nconst spaces = {\n compact: ['4px', '8px', '12px', '18px', '24px'],\n comfortable: ['5px', '10px', '16px', '24px', '32px'],\n spacious: ['6px', '12px', '20px', '30px', '40px'],\n} as const;\n\nconst radii = {\n none: ['0px', '0px', '0px'],\n small: ['4px', '7px', '9px'],\n medium: ['7px', '12px', '16px'],\n large: ['10px', '18px', '24px'],\n} as const;\n\nfunction fontStack(family: string): string {\n return family === 'system'\n ? \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ui-sans-serif, sans-serif\"\n : `${JSON.stringify(family)}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, ui-sans-serif, sans-serif`;\n}\n\n/**\n * Install semantic tokens on the document root, including portaled UI. Returns a cleanup for\n * system-theme listening.\n */\nexport function applyAppearance(value: unknown): () => void {\n const settings = parseAppearance(value);\n const root = document.documentElement;\n const preference = window.matchMedia('(prefers-color-scheme: dark)');\n const [xs, sm, md, lg, xl] = spaces[settings.density];\n const [control, surface, overlay] = radii[settings.cornerRadius];\n const tokens: Record<string, string> = {\n '--at-font': fontStack(settings.typography.body),\n '--at-font-heading': fontStack(settings.typography.heading),\n '--at-space-xs': xs,\n '--at-space-sm': sm,\n '--at-space-md': md,\n '--at-space-lg': lg,\n '--at-space-xl': xl,\n '--at-layout-gap': `clamp(${md}, 2.5vw, ${lg})`,\n '--at-radius-control': control,\n '--at-radius-surface': surface,\n '--at-radius-overlay': overlay,\n '--at-shadow-overlay':\n settings.elevation === 'flat'\n ? 'none'\n : settings.elevation === 'raised'\n ? '0 24px 72px rgb(0 0 0 / 24%)'\n : '0 18px 50px rgb(0 0 0 / 16%)',\n '--at-shadow-surface':\n settings.elevation === 'flat'\n ? 'none'\n : settings.elevation === 'raised'\n ? '0 8px 28px rgb(20 28 40 / 10%)'\n : '0 3px 16px rgb(20 28 40 / 5%)',\n };\n\n function applyColors(): void {\n const dark =\n settings.theme === 'dark' ||\n (settings.theme === 'system' && preference.matches);\n const [background, surfaceColor, subtle, text, muted, border] =\n palettes[settings.baseColor][dark ? 'dark' : 'light'];\n const accent = dark\n ? (settings.darkAccentColor ??\n `color-mix(in srgb, ${settings.accentColor} 50%, white)`)\n : settings.accentColor;\n settings.chartColors.forEach((chartColor, index) => {\n tokens[`--at-chart-${index + 1}`] = dark\n ? `color-mix(in srgb, ${chartColor} 60%, white)`\n : chartColor;\n });\n Object.assign(tokens, {\n '--at-background': background,\n '--at-surface': surfaceColor,\n '--at-subtle': subtle,\n '--at-text': text,\n '--at-muted': muted,\n '--at-border': border,\n '--at-accent': accent,\n '--at-focus-color': muted,\n '--at-control-hover-border': `color-mix(in srgb, ${muted} 45%, ${border})`,\n '--at-accent-hover': `color-mix(in srgb, ${accent} 80%, ${dark ? 'white' : 'black'})`,\n '--at-accent-subtle': `color-mix(in srgb, ${accent} ${dark ? 22 : 12}%, ${surfaceColor})`,\n '--at-on-accent': dark ? background : '#ffffff',\n '--at-danger': dark ? '#f97066' : '#b42318',\n '--at-backdrop': dark ? 'rgb(0 0 0 / 55%)' : 'rgb(15 23 30 / 22%)',\n '--at-code-surface': dark ? '#242a31' : '#f4f6f9',\n '--at-code-text': dark ? '#e5e9ef' : '#273242',\n });\n root.style.colorScheme = dark ? 'dark' : 'light';\n for (const [name, token] of Object.entries(tokens))\n root.style.setProperty(name, token);\n }\n applyColors();\n if (settings.theme === 'system')\n preference.addEventListener('change', applyColors);\n\n return () => {\n preference.removeEventListener('change', applyColors);\n };\n}\n\n/** Persist a viewer theme preference. Applying appearance belongs to the UI owner. */\nexport function createThemeController(\n initialTheme: ThemePreference = 'light'\n): ThemeController {\n const storageKey = 'altertable.data-app.theme';\n let theme = initialTheme;\n try {\n const saved = window.localStorage.getItem(storageKey);\n if (oneOf(saved, ['light', 'dark', 'system'])) theme = saved;\n } catch {\n // Storage can be unavailable in private browsing or embedded contexts.\n }\n const listeners = new Set<() => void>();\n\n return {\n getTheme() {\n return theme;\n },\n setTheme(nextTheme) {\n if (nextTheme === theme) return;\n theme = nextTheme;\n try {\n window.localStorage.setItem(storageKey, theme);\n } catch {\n // The selection still applies to this page.\n }\n listeners.forEach(listener => listener());\n },\n subscribe(listener) {\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n },\n };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "90CD7598768B570C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|