@duckviz/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @duckviz/sdk
2
+
3
+ Node-only SDK for DuckViz AI endpoints. Composable classes for widget recommendations, log format detection, report generation, and credit management — with built-in retry, auth, and error handling.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @duckviz/sdk
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ### Next.js App Router (recommended)
14
+
15
+ The fastest way to connect your app to DuckViz AI. Four lines — token stays server-side.
16
+
17
+ ```ts
18
+ // app/api/duckviz/[...route]/route.ts
19
+ import { createDuckvizHandlers } from "@duckviz/sdk/next";
20
+
21
+ export const { POST, GET } = createDuckvizHandlers({
22
+ token: process.env.DUCKVIZ_TOKEN!,
23
+ });
24
+ ```
25
+
26
+ This creates a catch-all proxy that forwards `/api/duckviz/*` requests to DuckViz Cloud with your token attached. SSE responses stream through without buffering.
27
+
28
+ Wire it up with `@duckviz/explorer`:
29
+
30
+ ```tsx
31
+ // app/page.tsx
32
+ import { Explorer } from "@duckviz/explorer";
33
+
34
+ const customFetch = (input, init) => {
35
+ if (typeof input === "string" && input.startsWith("/api/")) {
36
+ input = input.replace(/^\/api\//, "/api/duckviz/");
37
+ }
38
+ return fetch(input, init);
39
+ };
40
+
41
+ <Explorer datasets={datasets} customFetch={customFetch} authenticated={true} />;
42
+ ```
43
+
44
+ ### Programmatic usage
45
+
46
+ ```ts
47
+ import { DuckvizWidgetFlow, DuckvizCredits } from "@duckviz/sdk";
48
+
49
+ // Generate widget recommendations
50
+ const flow = new DuckvizWidgetFlow({
51
+ token: process.env.DUCKVIZ_TOKEN!,
52
+ });
53
+
54
+ const widgets = await flow.invoke({
55
+ schema: [
56
+ { name: "date", type: "DATE" },
57
+ { name: "revenue", type: "DOUBLE" },
58
+ { name: "region", type: "VARCHAR" },
59
+ ],
60
+ tableName: "t_sales",
61
+ });
62
+
63
+ // Check credit balance
64
+ const credits = new DuckvizCredits({
65
+ token: process.env.DUCKVIZ_TOKEN!,
66
+ });
67
+ const { balance, plan } = await credits.invoke();
68
+ ```
69
+
70
+ ### Streaming (SSE)
71
+
72
+ ```ts
73
+ const flow = new DuckvizWidgetFlow({
74
+ token: process.env.DUCKVIZ_TOKEN!,
75
+ });
76
+
77
+ for await (const event of flow.stream({
78
+ schema,
79
+ tableName: "t_logs",
80
+ })) {
81
+ console.log(event.event, event.data);
82
+ }
83
+ ```
84
+
85
+ ## API
86
+
87
+ ### Next.js handler
88
+
89
+ | Export | Description |
90
+ | ------------------------- | ---------------------------------------------------------- |
91
+ | `createDuckvizHandlers()` | Creates `{ POST, GET }` handlers for a catch-all API route |
92
+
93
+ Options: `token` (required), `baseUrl` (override, defaults to `https://app.duckviz.com`), `onRequest` (optional hook).
94
+
95
+ ### Endpoint classes
96
+
97
+ | Class | Description |
98
+ | -------------------------- | ------------------------------------------- |
99
+ | `DuckvizWidgetFlow` | AI widget recommendations from table schema |
100
+ | `DuckvizLogFormatDetector` | Detect log format from sample lines |
101
+ | `DuckvizReports` | Generate structured reports from widgets |
102
+ | `DuckvizCredits` | Check credit balance |
103
+ | `DuckvizUsage` | Query usage history |
104
+
105
+ All classes accept `{ token, baseUrl?, maxRetries?, timeout?, fetch? }`.
106
+
107
+ ### Error types
108
+
109
+ | Error | Status | Description |
110
+ | --------------------------- | ------ | ------------------------ |
111
+ | `DuckvizAuthError` | 401 | Invalid or missing token |
112
+ | `DuckvizQuotaExceededError` | 402 | Insufficient credits |
113
+ | `DuckvizRateLimitError` | 429 | Rate limit exceeded |
114
+ | `DuckvizServerError` | 5xx | Server error |
115
+ | `DuckvizNetworkError` | — | Network/timeout failure |
116
+
117
+ ### Dashboard config validation
118
+
119
+ ```ts
120
+ import { validateDashboardConfig } from "@duckviz/sdk";
121
+
122
+ const config = validateDashboardConfig(untrustedInput);
123
+ // Throws DashboardConfigError with path info on invalid input
124
+ ```
125
+
126
+ ## Environment variables
127
+
128
+ | Variable | Required | Description |
129
+ | ------------------ | -------- | ---------------------------------------------- |
130
+ | `DUCKVIZ_TOKEN` | Yes | Personal access token (`dvz_live_...`) |
131
+ | `DUCKVIZ_BASE_URL` | No | Override API base URL (defaults to production) |
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,260 @@
1
+ 'use strict';
2
+
3
+ // src/errors.ts
4
+ var DuckvizError = class extends Error {
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ this.name = "DuckvizError";
9
+ }
10
+ };
11
+ var DuckvizAuthError = class extends DuckvizError {
12
+ constructor(message = "Invalid or missing DuckViz token") {
13
+ super(message, 401);
14
+ this.name = "DuckvizAuthError";
15
+ }
16
+ };
17
+ var DuckvizQuotaExceededError = class extends DuckvizError {
18
+ constructor(message = "Insufficient credits", required) {
19
+ super(message, 402);
20
+ this.required = required;
21
+ this.name = "DuckvizQuotaExceededError";
22
+ }
23
+ };
24
+ var DuckvizRateLimitError = class extends DuckvizError {
25
+ constructor(message = "Rate limit exceeded", retryAfterMs) {
26
+ super(message, 429);
27
+ this.retryAfterMs = retryAfterMs;
28
+ this.name = "DuckvizRateLimitError";
29
+ }
30
+ };
31
+ var DuckvizServerError = class extends DuckvizError {
32
+ constructor(message, status) {
33
+ super(message, status);
34
+ this.name = "DuckvizServerError";
35
+ }
36
+ };
37
+ var DuckvizNetworkError = class extends DuckvizError {
38
+ constructor(message, cause) {
39
+ super(message);
40
+ this.cause = cause;
41
+ this.name = "DuckvizNetworkError";
42
+ }
43
+ };
44
+
45
+ // src/base.ts
46
+ var DEFAULT_BASE_URL = "https://app.duckviz.com";
47
+ var DuckvizEndpoint = class {
48
+ token;
49
+ baseUrl;
50
+ fetchImpl;
51
+ maxRetries;
52
+ timeout;
53
+ constructor(fields) {
54
+ if (!fields.token) {
55
+ throw new DuckvizAuthError("token is required");
56
+ }
57
+ this.token = fields.token;
58
+ this.baseUrl = (fields.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
59
+ this.fetchImpl = fields.fetch ?? globalThis.fetch;
60
+ this.maxRetries = fields.maxRetries ?? 2;
61
+ this.timeout = fields.timeout ?? 6e4;
62
+ }
63
+ // ─── Shared transport helpers (used by subclasses) ──────────────────────
64
+ buildHeaders() {
65
+ return new Headers({
66
+ "Content-Type": "application/json",
67
+ Authorization: `Bearer ${this.token}`
68
+ });
69
+ }
70
+ /**
71
+ * POST a JSON body and parse a JSON response. Retries transient failures.
72
+ * Throws a typed `DuckvizError` subclass on non-2xx responses.
73
+ */
74
+ async postJson(path, body, options) {
75
+ const res = await this.requestWithRetry(path, body, options, "POST");
76
+ return await res.json();
77
+ }
78
+ /**
79
+ * GET a JSON response. Query params are passed as a plain object and
80
+ * serialized into the URL — values coerce to strings, arrays repeat the
81
+ * key, `undefined` / `null` are skipped.
82
+ */
83
+ async getJson(path, params, options) {
84
+ let finalPath = path;
85
+ if (params) {
86
+ const qs = new URLSearchParams();
87
+ for (const [k, v] of Object.entries(params)) {
88
+ if (v === void 0 || v === null) continue;
89
+ qs.append(k, String(v));
90
+ }
91
+ const q = qs.toString();
92
+ if (q) finalPath = `${path}?${q}`;
93
+ }
94
+ const res = await this.requestWithRetry(
95
+ finalPath,
96
+ void 0,
97
+ options,
98
+ "GET"
99
+ );
100
+ return await res.json();
101
+ }
102
+ /**
103
+ * POST a JSON body and stream back an SSE response, one parsed message at
104
+ * a time. The raw DuckViz event wire format is
105
+ * data: {"event":"...","data":...}\n\n
106
+ * — this helper yields the parsed `{event, data}` objects directly.
107
+ */
108
+ async *postSSE(path, body, options) {
109
+ const res = await this.requestWithRetry(path, body, options, "POST");
110
+ if (!res.body) return;
111
+ for await (const msg of parseSSEStream(res.body)) {
112
+ yield msg;
113
+ }
114
+ }
115
+ async requestWithRetry(path, body, options, method) {
116
+ const url = `${this.baseUrl}${path}`;
117
+ const payload = method === "POST" ? JSON.stringify(body) : void 0;
118
+ let attempt = 0;
119
+ while (true) {
120
+ const attemptController = new AbortController();
121
+ const timeoutId = setTimeout(
122
+ () => attemptController.abort(),
123
+ this.timeout
124
+ );
125
+ const mergedSignal = mergeSignals(
126
+ options?.signal,
127
+ attemptController.signal
128
+ );
129
+ let res;
130
+ try {
131
+ const init = {
132
+ method,
133
+ headers: this.buildHeaders(),
134
+ signal: mergedSignal
135
+ };
136
+ if (payload !== void 0) init.body = payload;
137
+ res = await this.fetchImpl(url, init);
138
+ } catch (err) {
139
+ clearTimeout(timeoutId);
140
+ if (options?.signal?.aborted) {
141
+ throw err;
142
+ }
143
+ if (attempt >= this.maxRetries) {
144
+ throw new DuckvizNetworkError(
145
+ err instanceof Error ? err.message : String(err),
146
+ err
147
+ );
148
+ }
149
+ await sleep(backoffMs(attempt));
150
+ attempt++;
151
+ continue;
152
+ }
153
+ clearTimeout(timeoutId);
154
+ if (res.ok) return res;
155
+ const errBody = await safeReadJson(res);
156
+ const message = (errBody && typeof errBody === "object" && "error" in errBody ? String(errBody.error) : null) ?? `Request failed (${res.status})`;
157
+ if (res.status === 401) throw new DuckvizAuthError(message);
158
+ if (res.status === 402) {
159
+ const required = errBody && typeof errBody === "object" && "required" in errBody ? Number(errBody.required) : void 0;
160
+ throw new DuckvizQuotaExceededError(
161
+ message,
162
+ Number.isFinite(required) ? required : void 0
163
+ );
164
+ }
165
+ if (res.status === 429) {
166
+ const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
167
+ if (attempt < this.maxRetries) {
168
+ await sleep(retryAfterMs ?? backoffMs(attempt));
169
+ attempt++;
170
+ continue;
171
+ }
172
+ throw new DuckvizRateLimitError(message, retryAfterMs);
173
+ }
174
+ if (res.status >= 500 && res.status < 600) {
175
+ if (attempt < this.maxRetries) {
176
+ await sleep(backoffMs(attempt));
177
+ attempt++;
178
+ continue;
179
+ }
180
+ throw new DuckvizServerError(message, res.status);
181
+ }
182
+ throw new DuckvizError(message, res.status);
183
+ }
184
+ }
185
+ };
186
+ function backoffMs(attempt) {
187
+ const base = 250 * Math.pow(2, attempt);
188
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
189
+ return Math.max(0, Math.round(base + jitter));
190
+ }
191
+ function sleep(ms) {
192
+ return new Promise((resolve) => setTimeout(resolve, ms));
193
+ }
194
+ function parseRetryAfter(header) {
195
+ if (!header) return void 0;
196
+ const seconds = Number(header);
197
+ if (Number.isFinite(seconds)) return seconds * 1e3;
198
+ const date = Date.parse(header);
199
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
200
+ return void 0;
201
+ }
202
+ async function safeReadJson(res) {
203
+ try {
204
+ return await res.json();
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ function mergeSignals(...signals) {
210
+ const real = signals.filter((s) => !!s);
211
+ if (real.length === 0) return void 0;
212
+ if (real.length === 1) return real[0];
213
+ const any = AbortSignal.any;
214
+ if (typeof any === "function") return any.call(AbortSignal, real);
215
+ const controller = new AbortController();
216
+ for (const s of real) {
217
+ if (s.aborted) {
218
+ controller.abort(s.reason);
219
+ break;
220
+ }
221
+ s.addEventListener("abort", () => controller.abort(s.reason), {
222
+ once: true
223
+ });
224
+ }
225
+ return controller.signal;
226
+ }
227
+ async function* parseSSEStream(body) {
228
+ const reader = body.getReader();
229
+ const decoder = new TextDecoder();
230
+ let buffer = "";
231
+ try {
232
+ while (true) {
233
+ const { done, value } = await reader.read();
234
+ if (done) break;
235
+ buffer += decoder.decode(value, { stream: true });
236
+ const lines = buffer.split("\n");
237
+ buffer = lines.pop() ?? "";
238
+ for (const line of lines) {
239
+ if (!line.startsWith("data: ")) continue;
240
+ const raw = line.slice(6).trim();
241
+ if (!raw) continue;
242
+ try {
243
+ yield JSON.parse(raw);
244
+ } catch {
245
+ }
246
+ }
247
+ }
248
+ } finally {
249
+ reader.releaseLock();
250
+ }
251
+ }
252
+
253
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
254
+ exports.DuckvizAuthError = DuckvizAuthError;
255
+ exports.DuckvizEndpoint = DuckvizEndpoint;
256
+ exports.DuckvizError = DuckvizError;
257
+ exports.DuckvizNetworkError = DuckvizNetworkError;
258
+ exports.DuckvizQuotaExceededError = DuckvizQuotaExceededError;
259
+ exports.DuckvizRateLimitError = DuckvizRateLimitError;
260
+ exports.DuckvizServerError = DuckvizServerError;
@@ -0,0 +1,251 @@
1
+ // src/errors.ts
2
+ var DuckvizError = class extends Error {
3
+ constructor(message, status) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = "DuckvizError";
7
+ }
8
+ };
9
+ var DuckvizAuthError = class extends DuckvizError {
10
+ constructor(message = "Invalid or missing DuckViz token") {
11
+ super(message, 401);
12
+ this.name = "DuckvizAuthError";
13
+ }
14
+ };
15
+ var DuckvizQuotaExceededError = class extends DuckvizError {
16
+ constructor(message = "Insufficient credits", required) {
17
+ super(message, 402);
18
+ this.required = required;
19
+ this.name = "DuckvizQuotaExceededError";
20
+ }
21
+ };
22
+ var DuckvizRateLimitError = class extends DuckvizError {
23
+ constructor(message = "Rate limit exceeded", retryAfterMs) {
24
+ super(message, 429);
25
+ this.retryAfterMs = retryAfterMs;
26
+ this.name = "DuckvizRateLimitError";
27
+ }
28
+ };
29
+ var DuckvizServerError = class extends DuckvizError {
30
+ constructor(message, status) {
31
+ super(message, status);
32
+ this.name = "DuckvizServerError";
33
+ }
34
+ };
35
+ var DuckvizNetworkError = class extends DuckvizError {
36
+ constructor(message, cause) {
37
+ super(message);
38
+ this.cause = cause;
39
+ this.name = "DuckvizNetworkError";
40
+ }
41
+ };
42
+
43
+ // src/base.ts
44
+ var DEFAULT_BASE_URL = "https://app.duckviz.com";
45
+ var DuckvizEndpoint = class {
46
+ token;
47
+ baseUrl;
48
+ fetchImpl;
49
+ maxRetries;
50
+ timeout;
51
+ constructor(fields) {
52
+ if (!fields.token) {
53
+ throw new DuckvizAuthError("token is required");
54
+ }
55
+ this.token = fields.token;
56
+ this.baseUrl = (fields.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
57
+ this.fetchImpl = fields.fetch ?? globalThis.fetch;
58
+ this.maxRetries = fields.maxRetries ?? 2;
59
+ this.timeout = fields.timeout ?? 6e4;
60
+ }
61
+ // ─── Shared transport helpers (used by subclasses) ──────────────────────
62
+ buildHeaders() {
63
+ return new Headers({
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${this.token}`
66
+ });
67
+ }
68
+ /**
69
+ * POST a JSON body and parse a JSON response. Retries transient failures.
70
+ * Throws a typed `DuckvizError` subclass on non-2xx responses.
71
+ */
72
+ async postJson(path, body, options) {
73
+ const res = await this.requestWithRetry(path, body, options, "POST");
74
+ return await res.json();
75
+ }
76
+ /**
77
+ * GET a JSON response. Query params are passed as a plain object and
78
+ * serialized into the URL — values coerce to strings, arrays repeat the
79
+ * key, `undefined` / `null` are skipped.
80
+ */
81
+ async getJson(path, params, options) {
82
+ let finalPath = path;
83
+ if (params) {
84
+ const qs = new URLSearchParams();
85
+ for (const [k, v] of Object.entries(params)) {
86
+ if (v === void 0 || v === null) continue;
87
+ qs.append(k, String(v));
88
+ }
89
+ const q = qs.toString();
90
+ if (q) finalPath = `${path}?${q}`;
91
+ }
92
+ const res = await this.requestWithRetry(
93
+ finalPath,
94
+ void 0,
95
+ options,
96
+ "GET"
97
+ );
98
+ return await res.json();
99
+ }
100
+ /**
101
+ * POST a JSON body and stream back an SSE response, one parsed message at
102
+ * a time. The raw DuckViz event wire format is
103
+ * data: {"event":"...","data":...}\n\n
104
+ * — this helper yields the parsed `{event, data}` objects directly.
105
+ */
106
+ async *postSSE(path, body, options) {
107
+ const res = await this.requestWithRetry(path, body, options, "POST");
108
+ if (!res.body) return;
109
+ for await (const msg of parseSSEStream(res.body)) {
110
+ yield msg;
111
+ }
112
+ }
113
+ async requestWithRetry(path, body, options, method) {
114
+ const url = `${this.baseUrl}${path}`;
115
+ const payload = method === "POST" ? JSON.stringify(body) : void 0;
116
+ let attempt = 0;
117
+ while (true) {
118
+ const attemptController = new AbortController();
119
+ const timeoutId = setTimeout(
120
+ () => attemptController.abort(),
121
+ this.timeout
122
+ );
123
+ const mergedSignal = mergeSignals(
124
+ options?.signal,
125
+ attemptController.signal
126
+ );
127
+ let res;
128
+ try {
129
+ const init = {
130
+ method,
131
+ headers: this.buildHeaders(),
132
+ signal: mergedSignal
133
+ };
134
+ if (payload !== void 0) init.body = payload;
135
+ res = await this.fetchImpl(url, init);
136
+ } catch (err) {
137
+ clearTimeout(timeoutId);
138
+ if (options?.signal?.aborted) {
139
+ throw err;
140
+ }
141
+ if (attempt >= this.maxRetries) {
142
+ throw new DuckvizNetworkError(
143
+ err instanceof Error ? err.message : String(err),
144
+ err
145
+ );
146
+ }
147
+ await sleep(backoffMs(attempt));
148
+ attempt++;
149
+ continue;
150
+ }
151
+ clearTimeout(timeoutId);
152
+ if (res.ok) return res;
153
+ const errBody = await safeReadJson(res);
154
+ const message = (errBody && typeof errBody === "object" && "error" in errBody ? String(errBody.error) : null) ?? `Request failed (${res.status})`;
155
+ if (res.status === 401) throw new DuckvizAuthError(message);
156
+ if (res.status === 402) {
157
+ const required = errBody && typeof errBody === "object" && "required" in errBody ? Number(errBody.required) : void 0;
158
+ throw new DuckvizQuotaExceededError(
159
+ message,
160
+ Number.isFinite(required) ? required : void 0
161
+ );
162
+ }
163
+ if (res.status === 429) {
164
+ const retryAfterMs = parseRetryAfter(res.headers.get("retry-after"));
165
+ if (attempt < this.maxRetries) {
166
+ await sleep(retryAfterMs ?? backoffMs(attempt));
167
+ attempt++;
168
+ continue;
169
+ }
170
+ throw new DuckvizRateLimitError(message, retryAfterMs);
171
+ }
172
+ if (res.status >= 500 && res.status < 600) {
173
+ if (attempt < this.maxRetries) {
174
+ await sleep(backoffMs(attempt));
175
+ attempt++;
176
+ continue;
177
+ }
178
+ throw new DuckvizServerError(message, res.status);
179
+ }
180
+ throw new DuckvizError(message, res.status);
181
+ }
182
+ }
183
+ };
184
+ function backoffMs(attempt) {
185
+ const base = 250 * Math.pow(2, attempt);
186
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
187
+ return Math.max(0, Math.round(base + jitter));
188
+ }
189
+ function sleep(ms) {
190
+ return new Promise((resolve) => setTimeout(resolve, ms));
191
+ }
192
+ function parseRetryAfter(header) {
193
+ if (!header) return void 0;
194
+ const seconds = Number(header);
195
+ if (Number.isFinite(seconds)) return seconds * 1e3;
196
+ const date = Date.parse(header);
197
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
198
+ return void 0;
199
+ }
200
+ async function safeReadJson(res) {
201
+ try {
202
+ return await res.json();
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+ function mergeSignals(...signals) {
208
+ const real = signals.filter((s) => !!s);
209
+ if (real.length === 0) return void 0;
210
+ if (real.length === 1) return real[0];
211
+ const any = AbortSignal.any;
212
+ if (typeof any === "function") return any.call(AbortSignal, real);
213
+ const controller = new AbortController();
214
+ for (const s of real) {
215
+ if (s.aborted) {
216
+ controller.abort(s.reason);
217
+ break;
218
+ }
219
+ s.addEventListener("abort", () => controller.abort(s.reason), {
220
+ once: true
221
+ });
222
+ }
223
+ return controller.signal;
224
+ }
225
+ async function* parseSSEStream(body) {
226
+ const reader = body.getReader();
227
+ const decoder = new TextDecoder();
228
+ let buffer = "";
229
+ try {
230
+ while (true) {
231
+ const { done, value } = await reader.read();
232
+ if (done) break;
233
+ buffer += decoder.decode(value, { stream: true });
234
+ const lines = buffer.split("\n");
235
+ buffer = lines.pop() ?? "";
236
+ for (const line of lines) {
237
+ if (!line.startsWith("data: ")) continue;
238
+ const raw = line.slice(6).trim();
239
+ if (!raw) continue;
240
+ try {
241
+ yield JSON.parse(raw);
242
+ } catch {
243
+ }
244
+ }
245
+ }
246
+ } finally {
247
+ reader.releaseLock();
248
+ }
249
+ }
250
+
251
+ export { DEFAULT_BASE_URL, DuckvizAuthError, DuckvizEndpoint, DuckvizError, DuckvizNetworkError, DuckvizQuotaExceededError, DuckvizRateLimitError, DuckvizServerError };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ // src/express.ts
4
+ function duckvizExpressMiddleware() {
5
+ throw new Error(
6
+ "@duckviz/sdk/express is not implemented yet. Use @duckviz/sdk/next for Next.js App Router, or open an issue if you need Express support."
7
+ );
8
+ }
9
+
10
+ exports.duckvizExpressMiddleware = duckvizExpressMiddleware;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @duckviz/sdk/express — placeholder.
3
+ *
4
+ * Deferred to a later session. The Next.js handler (`./next`) covers the
5
+ * main target use case (consumer platform plan Stream 5). Express support
6
+ * needs a different plumbing strategy because the Node http ServerResponse
7
+ * doesn't speak Web Streams natively, so SSE pass-through has to use
8
+ * `res.write` in a loop over the upstream body reader. Not hard, just not
9
+ * blocking Stream 5 v1.
10
+ *
11
+ * Importing this subpath throws so the failure is loud rather than a
12
+ * silently-broken `{}` export.
13
+ */
14
+ declare function duckvizExpressMiddleware(): never;
15
+
16
+ export { duckvizExpressMiddleware };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @duckviz/sdk/express — placeholder.
3
+ *
4
+ * Deferred to a later session. The Next.js handler (`./next`) covers the
5
+ * main target use case (consumer platform plan Stream 5). Express support
6
+ * needs a different plumbing strategy because the Node http ServerResponse
7
+ * doesn't speak Web Streams natively, so SSE pass-through has to use
8
+ * `res.write` in a loop over the upstream body reader. Not hard, just not
9
+ * blocking Stream 5 v1.
10
+ *
11
+ * Importing this subpath throws so the failure is loud rather than a
12
+ * silently-broken `{}` export.
13
+ */
14
+ declare function duckvizExpressMiddleware(): never;
15
+
16
+ export { duckvizExpressMiddleware };
@@ -0,0 +1,8 @@
1
+ // src/express.ts
2
+ function duckvizExpressMiddleware() {
3
+ throw new Error(
4
+ "@duckviz/sdk/express is not implemented yet. Use @duckviz/sdk/next for Next.js App Router, or open an issue if you need Express support."
5
+ );
6
+ }
7
+
8
+ export { duckvizExpressMiddleware };