@altertable/data-app 0.63.0 → 0.65.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 +3 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +1 -1
- package/dist/chunks/{contract-tkc552ze.js → contract-a061yj6r.js} +2 -2
- package/dist/chunks/contract-awa2d5b1.js +272 -0
- package/dist/chunks/contract-awa2d5b1.js.map +12 -0
- package/dist/chunks/{contract-zr3jd7mr.js → contract-g6hky7x6.js} +93 -72
- package/dist/chunks/contract-g6hky7x6.js.map +12 -0
- package/dist/chunks/{contract-8wdybxj7.js → contract-sm7j7k95.js} +110 -74
- package/dist/chunks/contract-sm7j7k95.js.map +14 -0
- package/dist/chunks/{contract-farfe948.js → contract-txjy0en2.js} +31 -31
- package/dist/chunks/contract-txjy0en2.js.map +10 -0
- package/dist/chunks/contract-wz59z8pq.js.map +2 -2
- package/dist/chunks/{contract-wd8qe3mt.js → contract-yxbjea23.js} +8 -6
- package/dist/chunks/contract-yxbjea23.js.map +12 -0
- package/dist/client/index.js +3 -3
- package/dist/core/appearance.js +4 -2
- package/dist/core/appearance.js.map +1 -1
- package/dist/core/contract.js +1 -1
- package/dist/embed/index.js +20 -10
- package/dist/embed/index.js.map +3 -3
- package/dist/react/embed/index.js +5 -2
- package/dist/react/embed/index.js.map +3 -3
- package/dist/react/index.js +726 -454
- package/dist/react/index.js.map +17 -14
- package/dist/types/client/iframe.d.ts +2 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/logger.d.ts +6 -0
- package/dist/types/core/appearance.d.ts +12 -1
- package/dist/types/core/bridge-endpoint.d.ts +21 -0
- package/dist/types/core/bridge.d.ts +137 -16
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/logger.d.ts +12 -0
- package/dist/types/embed/host.d.ts +6 -2
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/source.d.ts +1 -1
- package/dist/types/react/embed/bridge.d.ts +1 -1
- package/dist/types/react/index.d.ts +1 -0
- package/dist/types/react/ui/AppToolbar.d.ts +3 -1
- package/dist/types/react/ui/DataApp.d.ts +6 -1
- package/dist/types/react/ui/ExportControl.d.ts +4 -0
- package/dist/types/react/ui/Toast.d.ts +8 -0
- package/dist/types/react/ui/csv-export.d.ts +19 -0
- package/dist/types/react/ui/icons.d.ts +1 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +2 -2
- package/dist/worker.js +382 -79
- package/docs/app-authoring.md +30 -3
- package/docs/embed.md +1 -1
- package/docs/hosted-apps.md +2 -2
- package/docs/layout.md +31 -0
- package/docs/local-data-apps.md +1 -1
- package/docs/react.md +64 -2
- package/examples/starter-data-app/index.tsx +50 -32
- package/package.json +2 -1
- package/dist/chunks/contract-8q35dcyh.js +0 -9
- package/dist/chunks/contract-8q35dcyh.js.map +0 -10
- package/dist/chunks/contract-8wdybxj7.js.map +0 -13
- package/dist/chunks/contract-cxr9t12b.js +0 -18
- package/dist/chunks/contract-cxr9t12b.js.map +0 -10
- package/dist/chunks/contract-farfe948.js.map +0 -10
- package/dist/chunks/contract-wd8qe3mt.js.map +0 -11
- package/dist/chunks/contract-zr3jd7mr.js.map +0 -12
- /package/dist/chunks/{contract-tkc552ze.js.map → contract-a061yj6r.js.map} +0 -0
package/dist/worker.js
CHANGED
|
@@ -48,19 +48,273 @@ function trustedParent(config, searchParams) {
|
|
|
48
48
|
var TOKEN_RE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)+-app-[1-9][0-9]*$/;
|
|
49
49
|
var PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'";
|
|
50
50
|
var inlineBootstrap = `(() => {
|
|
51
|
+
// src/core/logger.ts
|
|
52
|
+
function isLogEntry(value) {
|
|
53
|
+
if (!value || typeof value !== "object")
|
|
54
|
+
return false;
|
|
55
|
+
const entry = value;
|
|
56
|
+
return (entry.method === "log" || entry.method === "info" || entry.method === "warn" || entry.method === "error") && Array.isArray(entry.args) && entry.args.length <= 128;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
// src/core/bridge.ts
|
|
52
60
|
var BRIDGE = "altertable:data-app";
|
|
53
61
|
var MAX_PENDING = 128;
|
|
54
62
|
var REQUEST_TIMEOUT_MS = 60000;
|
|
55
|
-
function isBridgeMessage(value) {
|
|
56
|
-
if (!value || typeof value !== "object")
|
|
57
|
-
return false;
|
|
58
|
-
const message = value;
|
|
59
|
-
return message.channel === BRIDGE && message.version === 1 && typeof message.type === "string" && typeof message.documentId === "string" && message.documentId.length > 0 && message.documentId.length <= 128;
|
|
60
|
-
}
|
|
61
63
|
function validId(value) {
|
|
62
64
|
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
|
|
63
65
|
}
|
|
66
|
+
function event({
|
|
67
|
+
wire,
|
|
68
|
+
from,
|
|
69
|
+
session,
|
|
70
|
+
parse
|
|
71
|
+
}) {
|
|
72
|
+
return { wire, from, session, parse };
|
|
73
|
+
}
|
|
74
|
+
function id(fields) {
|
|
75
|
+
if (!validId(fields.id))
|
|
76
|
+
throw new Error("Invalid request ID.");
|
|
77
|
+
return fields.id;
|
|
78
|
+
}
|
|
79
|
+
function string(fields, key) {
|
|
80
|
+
const value = fields[key];
|
|
81
|
+
if (typeof value !== "string")
|
|
82
|
+
throw new Error(\`Invalid \${key}.\`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function empty() {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
var bridgeProtocol = {
|
|
89
|
+
channel: BRIDGE,
|
|
90
|
+
version: 1,
|
|
91
|
+
events: {
|
|
92
|
+
connect: event({
|
|
93
|
+
wire: "bridge:connect",
|
|
94
|
+
from: "host",
|
|
95
|
+
session: "none",
|
|
96
|
+
parse: empty
|
|
97
|
+
}),
|
|
98
|
+
ready: event({
|
|
99
|
+
wire: "bridge:ready",
|
|
100
|
+
from: "app",
|
|
101
|
+
session: "none",
|
|
102
|
+
parse: empty
|
|
103
|
+
}),
|
|
104
|
+
initialize: event({
|
|
105
|
+
wire: "bridge:initialize",
|
|
106
|
+
from: "host",
|
|
107
|
+
session: "establish",
|
|
108
|
+
parse: (fields) => ({ state: fields.state })
|
|
109
|
+
}),
|
|
110
|
+
request: event({
|
|
111
|
+
wire: "bridge:request",
|
|
112
|
+
from: "app",
|
|
113
|
+
session: "current",
|
|
114
|
+
parse: (fields) => {
|
|
115
|
+
const route = string(fields, "route");
|
|
116
|
+
if (!route || route.length > 256)
|
|
117
|
+
throw new Error("Invalid message route.");
|
|
118
|
+
return { id: id(fields), route, payload: fields.payload };
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
result: event({
|
|
122
|
+
wire: "bridge:result",
|
|
123
|
+
from: "host",
|
|
124
|
+
session: "current",
|
|
125
|
+
parse: (fields) => {
|
|
126
|
+
if (!Object.hasOwn(fields, "response"))
|
|
127
|
+
throw new Error("Missing response.");
|
|
128
|
+
return { id: id(fields), response: fields.response };
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
error: event({
|
|
132
|
+
wire: "bridge:error",
|
|
133
|
+
from: "host",
|
|
134
|
+
session: "current",
|
|
135
|
+
parse: (fields) => {
|
|
136
|
+
if (fields.requestId !== undefined && typeof fields.requestId !== "string")
|
|
137
|
+
throw new Error("Invalid requestId.");
|
|
138
|
+
return {
|
|
139
|
+
id: id(fields),
|
|
140
|
+
code: string(fields, "code"),
|
|
141
|
+
message: string(fields, "message"),
|
|
142
|
+
...fields.requestId === undefined ? {} : { requestId: fields.requestId }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
cancel: event({
|
|
147
|
+
wire: "bridge:cancel",
|
|
148
|
+
from: "app",
|
|
149
|
+
session: "current",
|
|
150
|
+
parse: (fields) => ({ id: id(fields) })
|
|
151
|
+
}),
|
|
152
|
+
disconnect: event({
|
|
153
|
+
wire: "bridge:disconnect",
|
|
154
|
+
from: "app",
|
|
155
|
+
session: "current",
|
|
156
|
+
parse: empty
|
|
157
|
+
}),
|
|
158
|
+
scriptLoad: event({
|
|
159
|
+
wire: "script:load",
|
|
160
|
+
from: "host",
|
|
161
|
+
session: "current",
|
|
162
|
+
parse: (fields) => ({ javascript: string(fields, "javascript") })
|
|
163
|
+
}),
|
|
164
|
+
stateUpdate: event({
|
|
165
|
+
wire: "state:update",
|
|
166
|
+
from: "host",
|
|
167
|
+
session: "current",
|
|
168
|
+
parse: (fields) => ({ state: fields.state })
|
|
169
|
+
}),
|
|
170
|
+
runtimeLog: event({
|
|
171
|
+
wire: "runtime:log",
|
|
172
|
+
from: "app",
|
|
173
|
+
session: "current",
|
|
174
|
+
parse: (fields) => {
|
|
175
|
+
if (!isLogEntry(fields.payload))
|
|
176
|
+
throw new Error("Invalid log entry.");
|
|
177
|
+
return { payload: fields.payload };
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
runtimeReady: event({
|
|
181
|
+
wire: "runtime:ready",
|
|
182
|
+
from: "app",
|
|
183
|
+
session: "current",
|
|
184
|
+
parse: empty
|
|
185
|
+
}),
|
|
186
|
+
runtimeError: event({
|
|
187
|
+
wire: "runtime:error",
|
|
188
|
+
from: "app",
|
|
189
|
+
session: "current",
|
|
190
|
+
parse: empty
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var byWire = new Map;
|
|
195
|
+
for (const name of Object.keys(bridgeProtocol.events)) {
|
|
196
|
+
const wire = bridgeProtocol.events[name].wire;
|
|
197
|
+
if (byWire.has(wire))
|
|
198
|
+
throw new Error(\`Duplicate bridge event: \${wire}\`);
|
|
199
|
+
byWire.set(wire, name);
|
|
200
|
+
}
|
|
201
|
+
function bridgeEventName(wire) {
|
|
202
|
+
return byWire.get(wire);
|
|
203
|
+
}
|
|
204
|
+
function parseBridgeEnvelope(value) {
|
|
205
|
+
if (!value || typeof value !== "object")
|
|
206
|
+
return;
|
|
207
|
+
const fields = value;
|
|
208
|
+
if (fields.channel !== BRIDGE || fields.version !== bridgeProtocol.version || typeof fields.type !== "string" || typeof fields.documentId !== "string" || !fields.documentId || fields.documentId.length > 128 || fields.sessionId !== undefined && !validId(fields.sessionId) || fields.token !== undefined && typeof fields.token !== "string")
|
|
209
|
+
return;
|
|
210
|
+
return fields;
|
|
211
|
+
}
|
|
212
|
+
function parseBridgePayload(envelope, definition) {
|
|
213
|
+
try {
|
|
214
|
+
return {
|
|
215
|
+
channel: BRIDGE,
|
|
216
|
+
version: bridgeProtocol.version,
|
|
217
|
+
documentId: envelope.documentId,
|
|
218
|
+
...envelope.sessionId === undefined ? {} : { sessionId: envelope.sessionId },
|
|
219
|
+
...envelope.token === undefined ? {} : { token: envelope.token },
|
|
220
|
+
type: definition.wire,
|
|
221
|
+
...definition.parse(envelope)
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function parseBridgeMessage(value) {
|
|
228
|
+
const envelope = parseBridgeEnvelope(value);
|
|
229
|
+
if (!envelope)
|
|
230
|
+
return;
|
|
231
|
+
const name = bridgeEventName(envelope.type);
|
|
232
|
+
if (!name)
|
|
233
|
+
return;
|
|
234
|
+
const definition = bridgeProtocol.events[name];
|
|
235
|
+
if (definition.session !== "none" && envelope.sessionId === undefined)
|
|
236
|
+
return;
|
|
237
|
+
return parseBridgePayload(envelope, definition);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/core/bridge-endpoint.ts
|
|
241
|
+
function createBridgeEndpoint({
|
|
242
|
+
role,
|
|
243
|
+
origin,
|
|
244
|
+
source,
|
|
245
|
+
opaque,
|
|
246
|
+
context,
|
|
247
|
+
post,
|
|
248
|
+
diagnostic,
|
|
249
|
+
invalidRequest,
|
|
250
|
+
handlers
|
|
251
|
+
}) {
|
|
252
|
+
let disposed = false;
|
|
253
|
+
function send(name, payload) {
|
|
254
|
+
if (disposed)
|
|
255
|
+
throw new Error("The bridge endpoint has closed.");
|
|
256
|
+
const definition = bridgeProtocol.events[name];
|
|
257
|
+
const current = context();
|
|
258
|
+
const message = parseBridgeMessage({
|
|
259
|
+
...payload,
|
|
260
|
+
channel: bridgeProtocol.channel,
|
|
261
|
+
version: bridgeProtocol.version,
|
|
262
|
+
type: definition.wire,
|
|
263
|
+
documentId: name === "connect" ? "host" : current.documentId,
|
|
264
|
+
sessionId: current.sessionId,
|
|
265
|
+
token: opaque ? current.token : undefined
|
|
266
|
+
});
|
|
267
|
+
if (!message || opaque && !validId(current.token))
|
|
268
|
+
throw new Error("The bridge is not ready to send this event.");
|
|
269
|
+
diagnostic?.("send", definition.wire);
|
|
270
|
+
if (disposed)
|
|
271
|
+
return;
|
|
272
|
+
post(message);
|
|
273
|
+
}
|
|
274
|
+
function receive(event) {
|
|
275
|
+
const peer = source();
|
|
276
|
+
if (disposed || !peer || event.origin !== origin || event.source !== peer)
|
|
277
|
+
return;
|
|
278
|
+
const envelope = parseBridgeEnvelope(event.data);
|
|
279
|
+
if (!envelope)
|
|
280
|
+
return;
|
|
281
|
+
const current = context();
|
|
282
|
+
const connecting = role === "app" && envelope.type === bridgeProtocol.events.connect.wire;
|
|
283
|
+
if (opaque && (connecting ? !validId(envelope.token) : !current.token || envelope.token !== current.token))
|
|
284
|
+
return;
|
|
285
|
+
const name = bridgeEventName(envelope.type);
|
|
286
|
+
if (!name)
|
|
287
|
+
return;
|
|
288
|
+
const definition = bridgeProtocol.events[name];
|
|
289
|
+
if (definition.from === role)
|
|
290
|
+
return;
|
|
291
|
+
if (definition.session === "establish") {
|
|
292
|
+
if (envelope.documentId !== current.documentId || envelope.sessionId === undefined)
|
|
293
|
+
return;
|
|
294
|
+
} else if (definition.session === "current") {
|
|
295
|
+
if (!current.sessionId || envelope.sessionId !== current.sessionId || envelope.documentId !== current.documentId)
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const message = parseBridgePayload(envelope, definition);
|
|
299
|
+
if (!message) {
|
|
300
|
+
if (role === "host" && name === "request" && validId(envelope.id))
|
|
301
|
+
invalidRequest?.(envelope.id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
diagnostic?.("receive", definition.wire);
|
|
305
|
+
if (disposed)
|
|
306
|
+
return;
|
|
307
|
+
const handler = handlers[name];
|
|
308
|
+
handler(message);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
send,
|
|
312
|
+
receive,
|
|
313
|
+
dispose() {
|
|
314
|
+
disposed = true;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
64
318
|
|
|
65
319
|
// src/core/messages.ts
|
|
66
320
|
var errorBrand = Symbol.for("@altertable/data-app/MessageRoutingError");
|
|
@@ -193,6 +447,46 @@ var inlineBootstrap = `(() => {
|
|
|
193
447
|
}
|
|
194
448
|
}
|
|
195
449
|
|
|
450
|
+
// src/client/logger.ts
|
|
451
|
+
function createBridgeLogger(enabled, send) {
|
|
452
|
+
function resolve(args) {
|
|
453
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
454
|
+
const value = args[0]();
|
|
455
|
+
return Array.isArray(value) ? value : [value];
|
|
456
|
+
}
|
|
457
|
+
return args.map((arg) => typeof arg === "function" ? arg() : arg);
|
|
458
|
+
}
|
|
459
|
+
function write(method, args) {
|
|
460
|
+
if (!enabled())
|
|
461
|
+
return;
|
|
462
|
+
try {
|
|
463
|
+
send({ method, args: resolve(args) });
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
log: (...args) => write("log", args),
|
|
468
|
+
info: (...args) => write("info", args),
|
|
469
|
+
warn: (...args) => write("warn", args),
|
|
470
|
+
error: (...args) => write("error", args)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function logBridgeMessage(logger, message) {
|
|
474
|
+
if (message.type === "runtime:log")
|
|
475
|
+
return;
|
|
476
|
+
logger.log(() => {
|
|
477
|
+
const metadata = { type: message.type };
|
|
478
|
+
if ("id" in message)
|
|
479
|
+
metadata.id = message.id;
|
|
480
|
+
if (message.type === "bridge:request") {
|
|
481
|
+
metadata.route = message.route;
|
|
482
|
+
const payload = message.payload;
|
|
483
|
+
if (message.route === "data:query" && payload && typeof payload === "object" && "operation" in payload && typeof payload.operation === "string")
|
|
484
|
+
metadata.operation = payload.operation;
|
|
485
|
+
}
|
|
486
|
+
return ["Sending message to parent", metadata];
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
196
490
|
// src/client/iframe.ts
|
|
197
491
|
function rethrowDataMessageError(error) {
|
|
198
492
|
if (error instanceof MessageRoutingError)
|
|
@@ -215,67 +509,61 @@ var inlineBootstrap = `(() => {
|
|
|
215
509
|
const pending = new Map;
|
|
216
510
|
let hostState;
|
|
217
511
|
const stateListeners = new Set;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
512
|
+
const logger = createBridgeLogger(() => !disposed && !!sessionId && !!hostState && typeof hostState === "object" && ("logging" in hostState) && hostState.logging === true, (entry) => send("runtimeLog", { payload: entry }));
|
|
513
|
+
const endpoint = createBridgeEndpoint({
|
|
514
|
+
role: "app",
|
|
515
|
+
origin: parentOrigin,
|
|
516
|
+
source: () => frame.parent,
|
|
517
|
+
opaque: mode === "bundle",
|
|
518
|
+
context: () => ({ documentId, sessionId, token }),
|
|
519
|
+
post(message) {
|
|
520
|
+
frame.parent.postMessage(message, parentOrigin);
|
|
521
|
+
logBridgeMessage(logger, message);
|
|
522
|
+
},
|
|
523
|
+
handlers: {
|
|
524
|
+
connect(message) {
|
|
525
|
+
if (mode === "bundle")
|
|
526
|
+
token = message.token;
|
|
527
|
+
send("ready", {});
|
|
528
|
+
},
|
|
529
|
+
initialize(message) {
|
|
530
|
+
if (sessionId && sessionId !== message.sessionId) {
|
|
531
|
+
for (const entry of pending.values())
|
|
532
|
+
entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
|
|
533
|
+
}
|
|
534
|
+
const first = !sessionId;
|
|
535
|
+
sessionId = message.sessionId;
|
|
536
|
+
receiveState(message.state);
|
|
537
|
+
if (first)
|
|
538
|
+
for (const entry of pending.values())
|
|
539
|
+
entry.start();
|
|
540
|
+
if (disposed)
|
|
541
|
+
return;
|
|
542
|
+
if (mode === "url")
|
|
543
|
+
send("runtimeReady", {});
|
|
544
|
+
},
|
|
545
|
+
stateUpdate(message) {
|
|
546
|
+
receiveState(message.state);
|
|
547
|
+
},
|
|
548
|
+
scriptLoad(message) {
|
|
549
|
+
if (mode === "bundle")
|
|
550
|
+
loadScript?.(message.javascript);
|
|
551
|
+
},
|
|
552
|
+
result(message) {
|
|
553
|
+
pending.get(message.id)?.resolve(message.response);
|
|
554
|
+
},
|
|
555
|
+
error(message) {
|
|
556
|
+
pending.get(message.id)?.reject(new MessageRoutingError(message.code, message.message, message.requestId));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
const send = endpoint.send;
|
|
561
|
+
const receive = endpoint.receive;
|
|
221
562
|
function receiveState(state) {
|
|
222
563
|
hostState = state;
|
|
223
564
|
for (const listener of stateListeners)
|
|
224
565
|
listener(state);
|
|
225
566
|
}
|
|
226
|
-
function receive(event) {
|
|
227
|
-
if (event.origin !== parentOrigin || event.source !== frame.parent || !isBridgeMessage(event.data))
|
|
228
|
-
return;
|
|
229
|
-
const message = event.data;
|
|
230
|
-
if (message.type === "bridge:connect") {
|
|
231
|
-
if (mode === "bundle") {
|
|
232
|
-
if (!validId(message.token))
|
|
233
|
-
return;
|
|
234
|
-
token = message.token;
|
|
235
|
-
}
|
|
236
|
-
send({ type: "bridge:ready" });
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (mode === "bundle" && (!token || message.token !== token))
|
|
240
|
-
return;
|
|
241
|
-
if (message.documentId !== documentId)
|
|
242
|
-
return;
|
|
243
|
-
if (message.type === "bridge:initialize" && validId(message.sessionId)) {
|
|
244
|
-
if (sessionId && sessionId !== message.sessionId) {
|
|
245
|
-
for (const entry of pending.values())
|
|
246
|
-
entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
|
|
247
|
-
}
|
|
248
|
-
const first = !sessionId;
|
|
249
|
-
sessionId = message.sessionId;
|
|
250
|
-
if (first)
|
|
251
|
-
for (const entry of pending.values())
|
|
252
|
-
entry.start();
|
|
253
|
-
receiveState(message.state);
|
|
254
|
-
if (mode === "url")
|
|
255
|
-
send({ type: "runtime:ready" });
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (!sessionId || message.sessionId !== sessionId)
|
|
259
|
-
return;
|
|
260
|
-
if (message.type === "state:update") {
|
|
261
|
-
receiveState(message.state);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (message.type === "script:load" && mode === "bundle" && typeof message.javascript === "string") {
|
|
265
|
-
loadScript?.(message.javascript);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (typeof message.id !== "string")
|
|
269
|
-
return;
|
|
270
|
-
const entry = pending.get(message.id);
|
|
271
|
-
if (!entry)
|
|
272
|
-
return;
|
|
273
|
-
if (message.type === "bridge:result" && "response" in message) {
|
|
274
|
-
entry.resolve(message.response);
|
|
275
|
-
} else if (message.type === "bridge:error" && typeof message.code === "string" && typeof message.message === "string") {
|
|
276
|
-
entry.reject(new MessageRoutingError(message.code, message.message, typeof message.requestId === "string" ? message.requestId : undefined));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
567
|
frame.addEventListener("message", receive);
|
|
280
568
|
function requestMessage(message, signal) {
|
|
281
569
|
return new Promise((resolve, reject) => {
|
|
@@ -294,13 +582,13 @@ var inlineBootstrap = `(() => {
|
|
|
294
582
|
}
|
|
295
583
|
function abort() {
|
|
296
584
|
if (sent)
|
|
297
|
-
send(
|
|
585
|
+
send("cancel", { id });
|
|
298
586
|
cleanup();
|
|
299
587
|
reject(signal?.reason);
|
|
300
588
|
}
|
|
301
589
|
const timer = setTimeout(() => {
|
|
302
590
|
if (sent)
|
|
303
|
-
send(
|
|
591
|
+
send("cancel", { id });
|
|
304
592
|
cleanup();
|
|
305
593
|
reject(new DataAppError(sessionId ? "The data request timed out." : "The preview shell is unavailable. Reload the preview.", sessionId ? "timeout" : "bridge_unavailable"));
|
|
306
594
|
}, timeoutMs);
|
|
@@ -309,8 +597,7 @@ var inlineBootstrap = `(() => {
|
|
|
309
597
|
if (sent)
|
|
310
598
|
return;
|
|
311
599
|
try {
|
|
312
|
-
send({
|
|
313
|
-
type: "bridge:request",
|
|
600
|
+
send("request", {
|
|
314
601
|
id,
|
|
315
602
|
route: message.route,
|
|
316
603
|
payload: message.payload
|
|
@@ -334,8 +621,8 @@ var inlineBootstrap = `(() => {
|
|
|
334
621
|
signal?.addEventListener("abort", abort, { once: true });
|
|
335
622
|
if (sessionId)
|
|
336
623
|
entry.start();
|
|
337
|
-
else
|
|
338
|
-
send(
|
|
624
|
+
else if (mode === "url" || token)
|
|
625
|
+
send("ready", {});
|
|
339
626
|
});
|
|
340
627
|
}
|
|
341
628
|
const messages = createMessageClient({ "data:query": defineDataQueryRoute(), "data:sql": sqlQueryRoute }, requestMessage);
|
|
@@ -343,20 +630,22 @@ var inlineBootstrap = `(() => {
|
|
|
343
630
|
return messages.request("data:query", { operation, input }, { signal }).catch(rethrowDataMessageError);
|
|
344
631
|
}
|
|
345
632
|
function disconnect() {
|
|
346
|
-
|
|
633
|
+
if (sessionId)
|
|
634
|
+
send("disconnect", {});
|
|
347
635
|
sessionId = undefined;
|
|
348
636
|
for (const entry of pending.values())
|
|
349
637
|
entry.reject(new DataAppError("The preview has closed.", "bridge_closed"));
|
|
350
638
|
}
|
|
351
639
|
function resume(event) {
|
|
352
|
-
if (event.persisted)
|
|
353
|
-
send(
|
|
640
|
+
if (event.persisted && (mode === "url" || token))
|
|
641
|
+
send("ready", {});
|
|
354
642
|
}
|
|
355
643
|
function dispose() {
|
|
356
644
|
if (disposed)
|
|
357
645
|
return;
|
|
358
646
|
disconnect();
|
|
359
647
|
disposed = true;
|
|
648
|
+
endpoint.dispose();
|
|
360
649
|
frame.removeEventListener("message", receive);
|
|
361
650
|
frame.removeEventListener("pagehide", disconnect);
|
|
362
651
|
frame.removeEventListener("pageshow", resume);
|
|
@@ -365,8 +654,9 @@ var inlineBootstrap = `(() => {
|
|
|
365
654
|
frame.addEventListener("pagehide", disconnect);
|
|
366
655
|
frame.addEventListener("pageshow", resume);
|
|
367
656
|
if (mode === "url")
|
|
368
|
-
send(
|
|
657
|
+
send("ready", {});
|
|
369
658
|
return {
|
|
659
|
+
logger,
|
|
370
660
|
request: requestMessage,
|
|
371
661
|
transport: queryOperation,
|
|
372
662
|
lakehouse: {
|
|
@@ -386,10 +676,12 @@ var inlineBootstrap = `(() => {
|
|
|
386
676
|
};
|
|
387
677
|
},
|
|
388
678
|
ready() {
|
|
389
|
-
|
|
679
|
+
if (!disposed && sessionId)
|
|
680
|
+
send("runtimeReady", {});
|
|
390
681
|
},
|
|
391
682
|
fail() {
|
|
392
|
-
|
|
683
|
+
if (!disposed && sessionId)
|
|
684
|
+
send("runtimeError", {});
|
|
393
685
|
}
|
|
394
686
|
};
|
|
395
687
|
}
|
|
@@ -417,6 +709,7 @@ var inlineBootstrap = `(() => {
|
|
|
417
709
|
}) {
|
|
418
710
|
let loaded;
|
|
419
711
|
let failed = false;
|
|
712
|
+
const bundleSource = \`altertable-data-app-\${crypto.randomUUID()}.js\`;
|
|
420
713
|
const bridge = createIframeTransport({
|
|
421
714
|
parentOrigin,
|
|
422
715
|
window: frame,
|
|
@@ -435,7 +728,8 @@ var inlineBootstrap = `(() => {
|
|
|
435
728
|
loaded = javascript;
|
|
436
729
|
try {
|
|
437
730
|
const script = frame.document.createElement("script");
|
|
438
|
-
script.textContent = javascript
|
|
731
|
+
script.textContent = \`\${javascript}
|
|
732
|
+
//# sourceURL=\${bundleSource}\`;
|
|
439
733
|
frame.document.body.append(script);
|
|
440
734
|
if (!failed)
|
|
441
735
|
bridge.ready();
|
|
@@ -448,12 +742,20 @@ var inlineBootstrap = `(() => {
|
|
|
448
742
|
failed = true;
|
|
449
743
|
bridge.fail();
|
|
450
744
|
}
|
|
451
|
-
|
|
452
|
-
|
|
745
|
+
function reportBundleError(event) {
|
|
746
|
+
if (event.filename === bundleSource)
|
|
747
|
+
reportError();
|
|
748
|
+
}
|
|
749
|
+
function reportBundleRejection(event) {
|
|
750
|
+
if (event.reason instanceof Error && event.reason.stack?.includes(bundleSource))
|
|
751
|
+
reportError();
|
|
752
|
+
}
|
|
753
|
+
frame.addEventListener("error", reportBundleError);
|
|
754
|
+
frame.addEventListener("unhandledrejection", reportBundleRejection);
|
|
453
755
|
const uninstall = installDataAppTransport(bridge, frame);
|
|
454
756
|
return () => {
|
|
455
|
-
frame.removeEventListener("error",
|
|
456
|
-
frame.removeEventListener("unhandledrejection",
|
|
757
|
+
frame.removeEventListener("error", reportBundleError);
|
|
758
|
+
frame.removeEventListener("unhandledrejection", reportBundleRejection);
|
|
457
759
|
uninstall();
|
|
458
760
|
};
|
|
459
761
|
}
|
|
@@ -486,6 +788,7 @@ function isPreviewHost(hostname, domainName) {
|
|
|
486
788
|
return hostname.endsWith(suffix) && TOKEN_RE.test(hostname.slice(0, -suffix.length));
|
|
487
789
|
}
|
|
488
790
|
var worker_default = {
|
|
791
|
+
runtimeHtml,
|
|
489
792
|
fetch(request, env) {
|
|
490
793
|
const url = new URL(request.url);
|
|
491
794
|
if (!isPreviewHost(url.hostname, env.DOMAIN_NAME) || url.pathname !== "/") {
|
package/docs/app-authoring.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Author a data app
|
|
2
2
|
|
|
3
3
|
Build an exploration that answers the user's question and a story that presents
|
|
4
|
-
its strongest findings.
|
|
4
|
+
its strongest findings. Include CSV export so readers can take the displayed
|
|
5
|
+
results into their own tools. Exploration, export, and story use the same
|
|
6
|
+
queries, definitions, and evidence.
|
|
5
7
|
|
|
6
8
|
## Inspect the data
|
|
7
9
|
|
|
@@ -38,10 +40,31 @@ and failure.
|
|
|
38
40
|
| Register source names | [Source identifiers](react.md#register-source-identifiers) |
|
|
39
41
|
| Choose date and field filters | [Filter variables](react.md#time-views-and-field-filters) |
|
|
40
42
|
| Handle refresh and stale results | [Displayed results](react.md#preserve-displayed-results) |
|
|
43
|
+
| Export displayed data as CSV | [CSV export](react.md#export-displayed-data-as-csv) |
|
|
41
44
|
| Bind definitions and source evidence | [Data context](react.md#bind-evidence) |
|
|
42
45
|
|
|
43
46
|
Use the exported types for configuration, appearance, formatting, and component
|
|
44
|
-
options.
|
|
47
|
+
options. Declare configuration with `satisfies DataAppConfig` so appearance
|
|
48
|
+
fields and values are checked before bundling.
|
|
49
|
+
|
|
50
|
+
## Compose the layout
|
|
51
|
+
|
|
52
|
+
See the [layout contract](layout.md).
|
|
53
|
+
|
|
54
|
+
## Export the displayed results
|
|
55
|
+
|
|
56
|
+
Provide `DataApp.csvExport` in every analytical app. The request-backed API
|
|
57
|
+
requires a callback that selects an explicit filename and named datasets with ordered columns and raw
|
|
58
|
+
rows from the displayed snapshot. Follow [CSV export](react.md#export-displayed-data-as-csv)
|
|
59
|
+
and the [starter](../examples/starter-data-app/index.tsx); use the built-in toolbar
|
|
60
|
+
action rather than adding a custom download button.
|
|
61
|
+
|
|
62
|
+
Export every distinct analytical dataset at its displayed grain, including relevant
|
|
63
|
+
dimensions and measures. Reuse one dataset for charts or metrics derived from the
|
|
64
|
+
same rows. One dataset downloads as CSV; multiple datasets offer individual CSVs
|
|
65
|
+
and **Export all** as a ZIP archive. Use the displayed input for scope labels and filenames. Export the
|
|
66
|
+
bounded result the app already has; do not issue a different query or mix pending
|
|
67
|
+
filters into the visible result. Setup and static screens may omit export.
|
|
45
68
|
|
|
46
69
|
## Present the findings
|
|
47
70
|
|
|
@@ -54,7 +77,11 @@ row or chart into a step.
|
|
|
54
77
|
|
|
55
78
|
Verify findings against the source and the user's question. Distinguish measured
|
|
56
79
|
zero, unavailable values, and empty results. Check filters, refresh, loading,
|
|
57
|
-
empty, error, and stale states, then present the story.
|
|
80
|
+
empty, error, and stale states, then present the story. Download CSV from the
|
|
81
|
+
standalone and embedded toolbar and verify its filename, columns, raw values,
|
|
82
|
+
and filter scope against the displayed result. Export and Present story must be
|
|
83
|
+
available once analytical results are shown; initial loading, empty, and initial
|
|
84
|
+
errors have neither action. Inspect both experiences
|
|
58
85
|
at phone and desktop widths in light and dark themes.
|
|
59
86
|
|
|
60
87
|
The app owns its queries, result parsing, business definitions, configuration,
|
package/docs/embed.md
CHANGED
|
@@ -73,7 +73,7 @@ app bundle; non-React apps use `createDataAppNavigation()` from `/client`.
|
|
|
73
73
|
|
|
74
74
|
`attachDataAppBridge()` also supports connection mode for a host-owned iframe. Supply
|
|
75
75
|
`connection: { type: 'origin', origin }` or `{ type: 'opaque', token }`, and an
|
|
76
|
-
`onMessage` dispatcher. Both modes use the same transport. It returns `dispose()` and `
|
|
76
|
+
`onMessage` dispatcher. Both modes use the same transport. It returns `dispose()`, `setPresentation()`, and `setLogger()` methods and owns source/origin checks, request
|
|
77
77
|
correlation, cancellation, bounded pending requests, and reconnection. Use source mode for bundle loading and token rotation. The opaque destination requires
|
|
78
78
|
wildcard delivery, but incoming messages still require the exact iframe window,
|
|
79
79
|
null origin, token, document, and session to match.
|
package/docs/hosted-apps.md
CHANGED
|
@@ -4,7 +4,7 @@ Follow the [shared authoring flow](app-authoring.md) using [index.tsx](../exampl
|
|
|
4
4
|
one file with public package imports; omit server files, HTML, credentials, and
|
|
5
5
|
relative or app-alias imports.
|
|
6
6
|
|
|
7
|
-
Replace the sample SQL, parsers, filters, data context, story, and configuration with
|
|
7
|
+
Replace the sample SQL, parsers, filters, data context, CSV export, story, and configuration with
|
|
8
8
|
an exploration of the source data you inspected. The starter uses two SQL
|
|
9
9
|
`VALUES` rows, so it needs no production table.
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ For execution details, see [browser-owned operations](client.md#browser-owned-op
|
|
|
13
13
|
## Convert a local data app
|
|
14
14
|
|
|
15
15
|
1. Combine the app's operations and parsers, data context, views, story,
|
|
16
|
-
configuration, and browser entry into one `index.tsx`, following the
|
|
16
|
+
CSV export, configuration, and browser entry into one `index.tsx`, following the
|
|
17
17
|
[single-file starter](../examples/starter-data-app/index.tsx).
|
|
18
18
|
2. Replace the HTTP client with `createDataClient({ operations })`, using the
|
|
19
19
|
operation registry as a value. See [browser-owned operations](client.md#browser-owned-operations-for-bundle-apps)
|