@wavehouse/sdk 0.0.0-dev.0f8826c
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/LICENSE +202 -0
- package/README.md +136 -0
- package/dist/index.cjs +1107 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +461 -0
- package/dist/index.d.ts +461 -0
- package/dist/index.js +1069 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
async function parseErrorResponse(res) {
|
|
3
|
+
let body;
|
|
4
|
+
try {
|
|
5
|
+
body = await res.json();
|
|
6
|
+
} catch {
|
|
7
|
+
body = void 0;
|
|
8
|
+
}
|
|
9
|
+
const message = typeof body?.error === "string" ? body.error : typeof body?.message === "string" ? body.message : res.statusText;
|
|
10
|
+
const retryable = res.status === 503 || res.status >= 500;
|
|
11
|
+
return {
|
|
12
|
+
status: res.status,
|
|
13
|
+
code: `HTTP_${res.status}`,
|
|
14
|
+
message,
|
|
15
|
+
details: body,
|
|
16
|
+
retryable
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function networkError(cause) {
|
|
20
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
21
|
+
return {
|
|
22
|
+
status: 0,
|
|
23
|
+
code: "NETWORK_ERROR",
|
|
24
|
+
message,
|
|
25
|
+
retryable: true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function ok(data) {
|
|
29
|
+
return { ok: true, data, error: null };
|
|
30
|
+
}
|
|
31
|
+
function okPage(data, hasMore, next) {
|
|
32
|
+
return { ok: true, data, error: null, hasMore, next };
|
|
33
|
+
}
|
|
34
|
+
function err(error) {
|
|
35
|
+
return { ok: false, data: null, error };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/http.ts
|
|
39
|
+
async function request(ctx, opts) {
|
|
40
|
+
const url = buildURL(ctx.baseURL, opts.path, opts.params);
|
|
41
|
+
const headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Accept: "application/json"
|
|
44
|
+
};
|
|
45
|
+
if (ctx.auth) {
|
|
46
|
+
const token = await ctx.auth();
|
|
47
|
+
if (token) {
|
|
48
|
+
headers.Authorization = `Bearer ${token}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let lastError = null;
|
|
52
|
+
const maxAttempts = ctx.options.maxRetries + 1;
|
|
53
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: opts.method,
|
|
57
|
+
headers,
|
|
58
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
59
|
+
signal: opts.signal
|
|
60
|
+
});
|
|
61
|
+
if (res.ok) {
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
const data = text ? JSON.parse(text) : void 0;
|
|
64
|
+
return { data, error: null, headers: res.headers };
|
|
65
|
+
}
|
|
66
|
+
const error = await parseErrorResponse(res);
|
|
67
|
+
if (res.status === 503) {
|
|
68
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
69
|
+
if (retryAfter && attempt < maxAttempts - 1) {
|
|
70
|
+
const delay = parseInt(retryAfter, 10) * 1e3 || 3e4;
|
|
71
|
+
await sleep(delay, opts.signal);
|
|
72
|
+
lastError = error;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (error.retryable && attempt < maxAttempts - 1) {
|
|
77
|
+
await sleep(backoff(attempt), opts.signal);
|
|
78
|
+
lastError = error;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
return { data: null, error, headers: res.headers };
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
84
|
+
return {
|
|
85
|
+
data: null,
|
|
86
|
+
error: { status: 0, code: "ABORTED", message: "Request aborted", retryable: false },
|
|
87
|
+
headers: new Headers()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
lastError = networkError(e);
|
|
91
|
+
if (attempt < maxAttempts - 1) {
|
|
92
|
+
await sleep(backoff(attempt), opts.signal);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { data: null, error: lastError, headers: new Headers() };
|
|
97
|
+
}
|
|
98
|
+
function buildURL(base, path, params) {
|
|
99
|
+
const url = new URL(path, base.endsWith("/") ? base : `${base}/`);
|
|
100
|
+
if (params) {
|
|
101
|
+
for (const [k, v] of Object.entries(params)) {
|
|
102
|
+
url.searchParams.set(k, v);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return url.toString();
|
|
106
|
+
}
|
|
107
|
+
function backoff(attempt) {
|
|
108
|
+
return Math.min(1e3 * 2 ** attempt, 3e4);
|
|
109
|
+
}
|
|
110
|
+
function sleep(ms, signal) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
if (signal?.aborted) {
|
|
113
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const timer = setTimeout(resolve, ms);
|
|
117
|
+
signal?.addEventListener(
|
|
118
|
+
"abort",
|
|
119
|
+
() => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
122
|
+
},
|
|
123
|
+
{ once: true }
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/dlq.ts
|
|
129
|
+
var DLQNamespace = class {
|
|
130
|
+
_ctx;
|
|
131
|
+
_createStream;
|
|
132
|
+
constructor(ctx, createStream) {
|
|
133
|
+
this._ctx = ctx;
|
|
134
|
+
this._createStream = createStream;
|
|
135
|
+
}
|
|
136
|
+
/** Get DLQ statistics (message counts per table). */
|
|
137
|
+
async list(opts) {
|
|
138
|
+
const { data, error } = await request(this._ctx, {
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: "/v1/dlq/stats",
|
|
141
|
+
signal: opts?.signal
|
|
142
|
+
});
|
|
143
|
+
if (error) return err(error);
|
|
144
|
+
return ok(data);
|
|
145
|
+
}
|
|
146
|
+
/** Get DLQ stats filtered by table name. */
|
|
147
|
+
async table(name, opts) {
|
|
148
|
+
const { data, error } = await request(this._ctx, {
|
|
149
|
+
method: "GET",
|
|
150
|
+
path: "/v1/dlq/stats",
|
|
151
|
+
params: { table: name },
|
|
152
|
+
signal: opts?.signal
|
|
153
|
+
});
|
|
154
|
+
if (error) return err(error);
|
|
155
|
+
return ok(data);
|
|
156
|
+
}
|
|
157
|
+
/** Subscribe to live DLQ events. */
|
|
158
|
+
stream(opts) {
|
|
159
|
+
return this._createStream("dlq", opts);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/pipes.ts
|
|
164
|
+
var PipeRef = class {
|
|
165
|
+
_ctx;
|
|
166
|
+
_name;
|
|
167
|
+
_params;
|
|
168
|
+
_createStream;
|
|
169
|
+
constructor(ctx, name, params, createStream) {
|
|
170
|
+
this._ctx = ctx;
|
|
171
|
+
this._name = name;
|
|
172
|
+
this._params = params;
|
|
173
|
+
this._createStream = createStream;
|
|
174
|
+
}
|
|
175
|
+
/** Execute the pipe and return results. */
|
|
176
|
+
async fetch(opts) {
|
|
177
|
+
const { data, error } = await request(this._ctx, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
path: `/v1/pipes/${encodeURIComponent(this._name)}`,
|
|
180
|
+
body: this._params ?? {},
|
|
181
|
+
signal: opts?.signal
|
|
182
|
+
});
|
|
183
|
+
if (error) return err(error);
|
|
184
|
+
return ok(data);
|
|
185
|
+
}
|
|
186
|
+
/** Subscribe to live events from the pipe's underlying query. */
|
|
187
|
+
stream(opts) {
|
|
188
|
+
return this._createStream(this._name, opts);
|
|
189
|
+
}
|
|
190
|
+
then(onfulfilled, onrejected) {
|
|
191
|
+
return this.fetch().then(onfulfilled, onrejected);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var PipesNamespace = class {
|
|
195
|
+
_ctx;
|
|
196
|
+
constructor(ctx) {
|
|
197
|
+
this._ctx = ctx;
|
|
198
|
+
}
|
|
199
|
+
/** List all registered pipes. */
|
|
200
|
+
async list(opts) {
|
|
201
|
+
const { data, error } = await request(this._ctx, {
|
|
202
|
+
method: "GET",
|
|
203
|
+
path: "/v1/admin/pipes",
|
|
204
|
+
signal: opts?.signal
|
|
205
|
+
});
|
|
206
|
+
if (error) return err(error);
|
|
207
|
+
return ok(data);
|
|
208
|
+
}
|
|
209
|
+
/** Get a single pipe definition by name. */
|
|
210
|
+
async get(name, opts) {
|
|
211
|
+
const { data, error } = await request(this._ctx, {
|
|
212
|
+
method: "GET",
|
|
213
|
+
path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
|
|
214
|
+
signal: opts?.signal
|
|
215
|
+
});
|
|
216
|
+
if (error) return err(error);
|
|
217
|
+
return ok(data);
|
|
218
|
+
}
|
|
219
|
+
/** Create or update a pipe. */
|
|
220
|
+
async set(name, def, opts) {
|
|
221
|
+
const { error } = await request(this._ctx, {
|
|
222
|
+
method: "PUT",
|
|
223
|
+
path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
|
|
224
|
+
body: def,
|
|
225
|
+
signal: opts?.signal
|
|
226
|
+
});
|
|
227
|
+
if (error) return err(error);
|
|
228
|
+
return ok(void 0);
|
|
229
|
+
}
|
|
230
|
+
/** Delete a pipe by name. */
|
|
231
|
+
async delete(name, opts) {
|
|
232
|
+
const { error } = await request(this._ctx, {
|
|
233
|
+
method: "DELETE",
|
|
234
|
+
path: `/v1/admin/pipes/${encodeURIComponent(name)}`,
|
|
235
|
+
signal: opts?.signal
|
|
236
|
+
});
|
|
237
|
+
if (error) return err(error);
|
|
238
|
+
return ok(void 0);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/policy.ts
|
|
243
|
+
var PolicyNamespace = class {
|
|
244
|
+
_ctx;
|
|
245
|
+
constructor(ctx) {
|
|
246
|
+
this._ctx = ctx;
|
|
247
|
+
}
|
|
248
|
+
/** Get the current access control policy. */
|
|
249
|
+
async get(opts) {
|
|
250
|
+
const { data, error } = await request(this._ctx, {
|
|
251
|
+
method: "GET",
|
|
252
|
+
path: "/v1/admin/policy",
|
|
253
|
+
signal: opts?.signal
|
|
254
|
+
});
|
|
255
|
+
if (error) return err(error);
|
|
256
|
+
return ok(data);
|
|
257
|
+
}
|
|
258
|
+
/** Replace the entire access control policy. */
|
|
259
|
+
async set(policy, opts) {
|
|
260
|
+
const { error } = await request(this._ctx, {
|
|
261
|
+
method: "PUT",
|
|
262
|
+
path: "/v1/admin/policy",
|
|
263
|
+
body: policy,
|
|
264
|
+
signal: opts?.signal
|
|
265
|
+
});
|
|
266
|
+
if (error) return err(error);
|
|
267
|
+
return ok(void 0);
|
|
268
|
+
}
|
|
269
|
+
/** Validate a policy without applying it (dry run). */
|
|
270
|
+
async validate(policy, opts) {
|
|
271
|
+
const { data, error } = await request(this._ctx, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
path: "/v1/admin/policy/validate",
|
|
274
|
+
body: policy,
|
|
275
|
+
signal: opts?.signal
|
|
276
|
+
});
|
|
277
|
+
if (error) return err(error);
|
|
278
|
+
return ok(data);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/schema.ts
|
|
283
|
+
var SchemaNamespace = class {
|
|
284
|
+
_ctx;
|
|
285
|
+
constructor(ctx) {
|
|
286
|
+
this._ctx = ctx;
|
|
287
|
+
}
|
|
288
|
+
/** List all table schemas discovered from ClickHouse. */
|
|
289
|
+
async list(opts) {
|
|
290
|
+
const { data, error } = await request(this._ctx, {
|
|
291
|
+
method: "GET",
|
|
292
|
+
path: "/v1/schema",
|
|
293
|
+
signal: opts?.signal
|
|
294
|
+
});
|
|
295
|
+
if (error) return err(error);
|
|
296
|
+
let schemas;
|
|
297
|
+
if (Array.isArray(data)) {
|
|
298
|
+
schemas = {};
|
|
299
|
+
for (const table of data) {
|
|
300
|
+
if (table && typeof table === "object" && "name" in table) {
|
|
301
|
+
schemas[table.name] = table;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
schemas = data;
|
|
306
|
+
}
|
|
307
|
+
return ok(schemas);
|
|
308
|
+
}
|
|
309
|
+
/** Force a schema refresh from ClickHouse system.columns. */
|
|
310
|
+
async refresh(opts) {
|
|
311
|
+
const { error } = await request(this._ctx, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
path: "/v1/schema/refresh",
|
|
314
|
+
signal: opts?.signal
|
|
315
|
+
});
|
|
316
|
+
if (error) return err(error);
|
|
317
|
+
return ok(void 0);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/sql.ts
|
|
322
|
+
async function sql(ctx, query, opts) {
|
|
323
|
+
const { data, error } = await request(ctx, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
path: "/v1/admin/query",
|
|
326
|
+
body: { sql: query },
|
|
327
|
+
signal: opts?.signal
|
|
328
|
+
});
|
|
329
|
+
if (error) return err(error);
|
|
330
|
+
return ok(data);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/stream/controller.ts
|
|
334
|
+
var StreamController = class {
|
|
335
|
+
_transport;
|
|
336
|
+
_subscribers = /* @__PURE__ */ new Set();
|
|
337
|
+
_status = "connecting";
|
|
338
|
+
_buffer = [];
|
|
339
|
+
_waiters = [];
|
|
340
|
+
_done = false;
|
|
341
|
+
constructor(transport) {
|
|
342
|
+
this._transport = transport;
|
|
343
|
+
this._transport.onEvent = (event) => {
|
|
344
|
+
for (const sub of this._subscribers) {
|
|
345
|
+
sub.next(event);
|
|
346
|
+
}
|
|
347
|
+
const waiter = this._waiters.shift();
|
|
348
|
+
if (waiter) {
|
|
349
|
+
waiter.resolve({ value: event, done: false });
|
|
350
|
+
} else {
|
|
351
|
+
this._buffer.push(event);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
this._transport.onStatus = (status) => {
|
|
355
|
+
if (status === this._status) return;
|
|
356
|
+
this._status = status;
|
|
357
|
+
for (const sub of this._subscribers) {
|
|
358
|
+
sub.status?.(status);
|
|
359
|
+
}
|
|
360
|
+
if (status === "closed") {
|
|
361
|
+
this._done = true;
|
|
362
|
+
for (const w of this._waiters) {
|
|
363
|
+
w.resolve({ value: void 0, done: true });
|
|
364
|
+
}
|
|
365
|
+
this._waiters = [];
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
this._transport.onError = (error) => {
|
|
369
|
+
for (const sub of this._subscribers) {
|
|
370
|
+
sub.error?.(error);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
this._transport.connect();
|
|
374
|
+
}
|
|
375
|
+
/** Current connection status. */
|
|
376
|
+
get status() {
|
|
377
|
+
return this._status;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Returns a promise that resolves when the stream status reaches `'live'`,
|
|
381
|
+
* rejects immediately if the stream is already `'closed'`, or rejects after
|
|
382
|
+
* `timeoutMs` milliseconds (default: 5 000) if it never connects.
|
|
383
|
+
*
|
|
384
|
+
* Safe to call before `.subscribe()` — does not trigger auto-close when
|
|
385
|
+
* the internal waiter is removed.
|
|
386
|
+
*
|
|
387
|
+
* `@example`
|
|
388
|
+
* const stream = client.from('events').stream();
|
|
389
|
+
* const unsub = stream.subscribe({ next: (e) => console.log(e) });
|
|
390
|
+
* await stream.connected(); // waits until the transport is live
|
|
391
|
+
* await client.from('events').insert({ ... });
|
|
392
|
+
*/
|
|
393
|
+
connected(timeoutMs = 5e3) {
|
|
394
|
+
if (this._status === "live") return Promise.resolve();
|
|
395
|
+
if (this._status === "closed") return Promise.reject(new Error("Stream is closed"));
|
|
396
|
+
if (this._done) return Promise.reject(new Error("Stream closed before connecting"));
|
|
397
|
+
return new Promise((resolve, reject) => {
|
|
398
|
+
let settled = false;
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
if (settled) return;
|
|
401
|
+
settled = true;
|
|
402
|
+
this._subscribers.delete(watcher);
|
|
403
|
+
reject(new Error(`Stream did not connect within ${timeoutMs}ms`));
|
|
404
|
+
}, timeoutMs);
|
|
405
|
+
const watcher = {
|
|
406
|
+
next: () => {
|
|
407
|
+
},
|
|
408
|
+
status: (s) => {
|
|
409
|
+
if (s === "live") {
|
|
410
|
+
if (settled) return;
|
|
411
|
+
settled = true;
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
this._subscribers.delete(watcher);
|
|
414
|
+
resolve();
|
|
415
|
+
} else if (s === "closed") {
|
|
416
|
+
if (settled) return;
|
|
417
|
+
settled = true;
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
this._subscribers.delete(watcher);
|
|
420
|
+
reject(new Error("Stream closed before connecting"));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
this._subscribers.add(watcher);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
/** Subscribe to stream events via callbacks. Returns an unsubscribe function. */
|
|
428
|
+
subscribe(subscriber) {
|
|
429
|
+
this._subscribers.add(subscriber);
|
|
430
|
+
subscriber.status?.(this._status);
|
|
431
|
+
return () => {
|
|
432
|
+
this._subscribers.delete(subscriber);
|
|
433
|
+
if (this._subscribers.size === 0 && this._waiters.length === 0) {
|
|
434
|
+
this.close();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Attach an AbortSignal — when aborted, the stream is closed. */
|
|
439
|
+
attachSignal(signal) {
|
|
440
|
+
if (signal.aborted) {
|
|
441
|
+
this.close();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
signal.addEventListener("abort", () => this.close(), { once: true });
|
|
445
|
+
}
|
|
446
|
+
/** Close the stream and release resources. */
|
|
447
|
+
close() {
|
|
448
|
+
this._transport.disconnect();
|
|
449
|
+
if (this._status !== "closed") {
|
|
450
|
+
this._status = "closed";
|
|
451
|
+
for (const sub of this._subscribers) {
|
|
452
|
+
sub.status?.("closed");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
this._done = true;
|
|
456
|
+
for (const w of this._waiters) {
|
|
457
|
+
w.resolve({ value: void 0, done: true });
|
|
458
|
+
}
|
|
459
|
+
this._waiters = [];
|
|
460
|
+
}
|
|
461
|
+
/** Async iterator protocol — enables `for await (const event of stream)`. */
|
|
462
|
+
[Symbol.asyncIterator]() {
|
|
463
|
+
const self = this;
|
|
464
|
+
return {
|
|
465
|
+
next() {
|
|
466
|
+
if (self._buffer.length > 0) {
|
|
467
|
+
return Promise.resolve({ value: self._buffer.shift(), done: false });
|
|
468
|
+
}
|
|
469
|
+
if (self._done) {
|
|
470
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
471
|
+
}
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
self._waiters.push({ resolve });
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
return() {
|
|
477
|
+
self.close();
|
|
478
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
479
|
+
},
|
|
480
|
+
[Symbol.asyncIterator]() {
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/stream/sse.ts
|
|
488
|
+
var activeSSEConnections = 0;
|
|
489
|
+
var SSE_WARN_THRESHOLD = 5;
|
|
490
|
+
var SSETransport = class {
|
|
491
|
+
_opts;
|
|
492
|
+
_es = null;
|
|
493
|
+
onEvent = null;
|
|
494
|
+
onStatus = null;
|
|
495
|
+
onError = null;
|
|
496
|
+
constructor(opts) {
|
|
497
|
+
this._opts = opts;
|
|
498
|
+
}
|
|
499
|
+
connect() {
|
|
500
|
+
if (typeof EventSource === "undefined") {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"[wavehouse] EventSource is not available in this environment. Please provide a global polyfill (e.g., `globalThis.EventSource = require('eventsource')`)."
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
this._doConnect().catch((err2) => {
|
|
506
|
+
this.onError?.({
|
|
507
|
+
status: 0,
|
|
508
|
+
code: "SSE_CONNECT_ERROR",
|
|
509
|
+
message: err2 instanceof Error ? err2.message : String(err2),
|
|
510
|
+
retryable: true
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
disconnect() {
|
|
515
|
+
if (this._es) {
|
|
516
|
+
this._es.close();
|
|
517
|
+
this._es = null;
|
|
518
|
+
activeSSEConnections = Math.max(0, activeSSEConnections - 1);
|
|
519
|
+
}
|
|
520
|
+
this.onStatus?.("closed");
|
|
521
|
+
}
|
|
522
|
+
async _doConnect() {
|
|
523
|
+
const url = new URL("/v1/stream", this._opts.baseURL);
|
|
524
|
+
url.searchParams.set("table", this._opts.table);
|
|
525
|
+
if (this._opts.since) {
|
|
526
|
+
url.searchParams.set("since", this._opts.since);
|
|
527
|
+
}
|
|
528
|
+
if (this._opts.auth) {
|
|
529
|
+
const token = await this._opts.auth();
|
|
530
|
+
if (token) {
|
|
531
|
+
url.searchParams.set("token", token);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
activeSSEConnections++;
|
|
535
|
+
if (activeSSEConnections > SSE_WARN_THRESHOLD) {
|
|
536
|
+
console.warn(
|
|
537
|
+
`[wavehouse] ${activeSSEConnections} SSE connections open. Browsers limit HTTP/1.1 to 6 connections per domain.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
this._es = new EventSource(url.toString());
|
|
541
|
+
this._es.onopen = () => {
|
|
542
|
+
this.onStatus?.("live");
|
|
543
|
+
};
|
|
544
|
+
this._es.onmessage = (e) => {
|
|
545
|
+
try {
|
|
546
|
+
const msg = JSON.parse(e.data);
|
|
547
|
+
const event = {
|
|
548
|
+
table: msg.table_name,
|
|
549
|
+
timestamp: msg.received_timestamp,
|
|
550
|
+
data: msg.data
|
|
551
|
+
};
|
|
552
|
+
this.onEvent?.(event);
|
|
553
|
+
} catch {
|
|
554
|
+
console.warn("[wavehouse] SSE received malformed message:", e.data);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
this._es.onerror = () => {
|
|
558
|
+
if (this._es?.readyState === EventSource.CONNECTING) {
|
|
559
|
+
this.onStatus?.("reconnecting");
|
|
560
|
+
} else if (this._es?.readyState === EventSource.CLOSED) {
|
|
561
|
+
this.onStatus?.("closed");
|
|
562
|
+
} else if (this._es?.readyState === EventSource.OPEN) {
|
|
563
|
+
this.onStatus?.("live");
|
|
564
|
+
} else {
|
|
565
|
+
this.onError?.({
|
|
566
|
+
status: 0,
|
|
567
|
+
code: "SSE_ERROR",
|
|
568
|
+
message: "SSE connection error",
|
|
569
|
+
retryable: true
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/sys.ts
|
|
577
|
+
var SysNamespace = class {
|
|
578
|
+
_ctx;
|
|
579
|
+
constructor(ctx) {
|
|
580
|
+
this._ctx = ctx;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Liveness ping — resolves with no error when the server is reachable and
|
|
584
|
+
* past boot. Hits the public, content-free `/v1/health` route (200/503, no
|
|
585
|
+
* body), kept intentionally distinct from the `/livez` Kubernetes probe so
|
|
586
|
+
* it stays reachable even in deployments that filter probe paths at the
|
|
587
|
+
* reverse proxy. Use it to check a server is online before sending data, or
|
|
588
|
+
* to pick among servers in a distributed setup.
|
|
589
|
+
*/
|
|
590
|
+
async health(opts) {
|
|
591
|
+
const { error } = await request(this._ctx, {
|
|
592
|
+
method: "GET",
|
|
593
|
+
path: "/v1/health",
|
|
594
|
+
signal: opts?.signal
|
|
595
|
+
});
|
|
596
|
+
if (error) return err(error);
|
|
597
|
+
return ok(void 0);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/stream/live-query.ts
|
|
602
|
+
var LiveQuery = class {
|
|
603
|
+
_stream;
|
|
604
|
+
_subscriber;
|
|
605
|
+
_buffer = [];
|
|
606
|
+
_buffering = true;
|
|
607
|
+
_unsubStream = null;
|
|
608
|
+
_closed = false;
|
|
609
|
+
constructor(stream, fetchFn, subscriber, _filters) {
|
|
610
|
+
this._stream = stream;
|
|
611
|
+
this._subscriber = subscriber;
|
|
612
|
+
this._unsubStream = stream.subscribe({
|
|
613
|
+
next: (event) => {
|
|
614
|
+
if (this._closed) return;
|
|
615
|
+
if (this._buffering) {
|
|
616
|
+
this._buffer.push(event);
|
|
617
|
+
} else {
|
|
618
|
+
subscriber.next(event);
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
status: (s) => subscriber.status?.(s),
|
|
622
|
+
error: (e) => subscriber.error?.(e)
|
|
623
|
+
});
|
|
624
|
+
this._runBackfill(fetchFn);
|
|
625
|
+
}
|
|
626
|
+
async _runBackfill(fetchFn) {
|
|
627
|
+
try {
|
|
628
|
+
const result = await fetchFn();
|
|
629
|
+
if (this._closed) return;
|
|
630
|
+
this._subscriber.initial?.(result);
|
|
631
|
+
if (result.error) {
|
|
632
|
+
this._buffering = false;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const rows = result.data;
|
|
636
|
+
let lastTimestamp;
|
|
637
|
+
if (rows.length > 0) {
|
|
638
|
+
const lastRow = rows[rows.length - 1];
|
|
639
|
+
lastTimestamp = lastRow?.received_timestamp;
|
|
640
|
+
}
|
|
641
|
+
this._buffering = false;
|
|
642
|
+
for (const event of this._buffer) {
|
|
643
|
+
if (this._closed) break;
|
|
644
|
+
if (lastTimestamp && event.timestamp <= lastTimestamp) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
this._subscriber.next(event);
|
|
648
|
+
}
|
|
649
|
+
this._buffer = [];
|
|
650
|
+
} catch {
|
|
651
|
+
this._buffering = false;
|
|
652
|
+
this._buffer = [];
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** Close the live query and the underlying stream. */
|
|
656
|
+
close() {
|
|
657
|
+
this._closed = true;
|
|
658
|
+
this._buffering = false;
|
|
659
|
+
this._buffer = [];
|
|
660
|
+
this._unsubStream?.();
|
|
661
|
+
this._stream.close();
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/query-builder.ts
|
|
666
|
+
var OP_MAP = {
|
|
667
|
+
"=": "eq",
|
|
668
|
+
"!=": "neq",
|
|
669
|
+
">": "gt",
|
|
670
|
+
">=": "gte",
|
|
671
|
+
"<": "lt",
|
|
672
|
+
"<=": "lte",
|
|
673
|
+
in: "in",
|
|
674
|
+
like: "like",
|
|
675
|
+
not_like: "not_like"
|
|
676
|
+
};
|
|
677
|
+
var QueryBuilder = class _QueryBuilder {
|
|
678
|
+
/** @internal */
|
|
679
|
+
_state;
|
|
680
|
+
/** @internal */
|
|
681
|
+
_ctx;
|
|
682
|
+
/** @internal */
|
|
683
|
+
_createStream;
|
|
684
|
+
constructor(ctx, state, createStream) {
|
|
685
|
+
this._ctx = ctx;
|
|
686
|
+
this._state = Object.freeze({ ...state });
|
|
687
|
+
this._createStream = createStream;
|
|
688
|
+
}
|
|
689
|
+
// --- Builder methods (each returns a new QueryBuilder) ---
|
|
690
|
+
select(...columns) {
|
|
691
|
+
return this._clone({ columns: [...this._state.columns, ...columns] });
|
|
692
|
+
}
|
|
693
|
+
where(column, op, value) {
|
|
694
|
+
const filter = { column, op: OP_MAP[op], value };
|
|
695
|
+
return this._clone({ filters: [...this._state.filters, filter] });
|
|
696
|
+
}
|
|
697
|
+
count(column = "*", alias = "count") {
|
|
698
|
+
return this._addAgg("count", column, alias);
|
|
699
|
+
}
|
|
700
|
+
sum(column, alias = `sum_${column}`) {
|
|
701
|
+
return this._addAgg("sum", column, alias);
|
|
702
|
+
}
|
|
703
|
+
avg(column, alias = `avg_${column}`) {
|
|
704
|
+
return this._addAgg("avg", column, alias);
|
|
705
|
+
}
|
|
706
|
+
min(column, alias = `min_${column}`) {
|
|
707
|
+
return this._addAgg("min", column, alias);
|
|
708
|
+
}
|
|
709
|
+
max(column, alias = `max_${column}`) {
|
|
710
|
+
return this._addAgg("max", column, alias);
|
|
711
|
+
}
|
|
712
|
+
countDistinct(column, alias = `count_distinct_${column}`) {
|
|
713
|
+
return this._addAgg("countDistinct", column, alias);
|
|
714
|
+
}
|
|
715
|
+
aggregate(fn, column, alias) {
|
|
716
|
+
return this._addAgg(fn, column, alias);
|
|
717
|
+
}
|
|
718
|
+
groupBy(...columns) {
|
|
719
|
+
return this._clone({ groupBy: [...this._state.groupBy, ...columns] });
|
|
720
|
+
}
|
|
721
|
+
orderBy(column, dir = "asc") {
|
|
722
|
+
return this._clone({ orderBy: [...this._state.orderBy, { column, dir }] });
|
|
723
|
+
}
|
|
724
|
+
limit(n) {
|
|
725
|
+
return this._clone({ limit: n });
|
|
726
|
+
}
|
|
727
|
+
timeRange(column, since, until) {
|
|
728
|
+
return this._clone({ timeRange: { column, since, until } });
|
|
729
|
+
}
|
|
730
|
+
cacheTTL(seconds) {
|
|
731
|
+
return this._clone({ cacheTTL: seconds });
|
|
732
|
+
}
|
|
733
|
+
// --- Execution ---
|
|
734
|
+
/** Default row limit when none is specified. Matches backend DefaultMaxRows. */
|
|
735
|
+
static DEFAULT_LIMIT = 1e3;
|
|
736
|
+
async fetch(opts) {
|
|
737
|
+
const effectiveLimit = opts?.limit ?? this._state.limit ?? _QueryBuilder.DEFAULT_LIMIT;
|
|
738
|
+
const ast = this._buildAST(effectiveLimit);
|
|
739
|
+
const { data, error } = await request(this._ctx, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
path: `/v1/query?table=${encodeURIComponent(this._state.table)}`,
|
|
742
|
+
body: ast,
|
|
743
|
+
signal: opts?.signal
|
|
744
|
+
});
|
|
745
|
+
if (error) return err(error);
|
|
746
|
+
const rows = data;
|
|
747
|
+
const hasMore = effectiveLimit != null && rows.length >= effectiveLimit;
|
|
748
|
+
if (hasMore) {
|
|
749
|
+
const nextFn = () => this._fetchNext(rows, effectiveLimit, opts);
|
|
750
|
+
return okPage(rows, true, nextFn);
|
|
751
|
+
}
|
|
752
|
+
return okPage(rows, false);
|
|
753
|
+
}
|
|
754
|
+
stream(opts) {
|
|
755
|
+
const raw = this._createStream(this._state.table, opts);
|
|
756
|
+
const filters = this._state.filters;
|
|
757
|
+
const columns = this._state.columns;
|
|
758
|
+
if (filters.length === 0 && columns.length === 0) {
|
|
759
|
+
return raw;
|
|
760
|
+
}
|
|
761
|
+
return new FilteredStreamController(raw, filters, columns);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Start a live query: fetches historical data, then streams live updates.
|
|
765
|
+
*
|
|
766
|
+
* The subscriber's `initial()` is called once with the fetch result, then
|
|
767
|
+
* `next()` fires for each live event. Events that arrived during the fetch
|
|
768
|
+
* are deduplicated and flushed automatically.
|
|
769
|
+
*
|
|
770
|
+
* Returns a LiveQuery handle with a `.close()` method.
|
|
771
|
+
*/
|
|
772
|
+
liveQuery(subscriber, opts) {
|
|
773
|
+
const stream = this.stream(opts);
|
|
774
|
+
const fetchFn = () => this.fetch();
|
|
775
|
+
return new LiveQuery(stream, fetchFn, subscriber, this._state.filters);
|
|
776
|
+
}
|
|
777
|
+
// --- PromiseLike implementation ---
|
|
778
|
+
then(onfulfilled, onrejected) {
|
|
779
|
+
return this.fetch().then(onfulfilled, onrejected);
|
|
780
|
+
}
|
|
781
|
+
// --- Private helpers ---
|
|
782
|
+
_clone(overrides) {
|
|
783
|
+
return new _QueryBuilder(this._ctx, { ...this._state, ...overrides }, this._createStream);
|
|
784
|
+
}
|
|
785
|
+
_addAgg(fn, column, alias) {
|
|
786
|
+
const agg = { fn, column, alias };
|
|
787
|
+
return this._clone({ aggregations: [...this._state.aggregations, agg] });
|
|
788
|
+
}
|
|
789
|
+
_buildAST(effectiveLimit) {
|
|
790
|
+
const ast = {};
|
|
791
|
+
if (this._state.columns.length > 0) ast.columns = this._state.columns;
|
|
792
|
+
if (this._state.aggregations.length > 0) ast.aggregations = this._state.aggregations;
|
|
793
|
+
if (this._state.filters.length > 0) ast.filters = this._state.filters;
|
|
794
|
+
if (this._state.groupBy.length > 0) ast.group_by = this._state.groupBy;
|
|
795
|
+
if (this._state.orderBy.length > 0) {
|
|
796
|
+
ast.order_by = this._state.orderBy;
|
|
797
|
+
} else if (effectiveLimit != null && this._state.aggregations.length === 0) {
|
|
798
|
+
ast.order_by = [{ column: "received_timestamp", dir: "desc" }];
|
|
799
|
+
}
|
|
800
|
+
if (effectiveLimit != null) ast.limit = effectiveLimit;
|
|
801
|
+
if (this._state.timeRange) ast.time_range = this._state.timeRange;
|
|
802
|
+
return ast;
|
|
803
|
+
}
|
|
804
|
+
async _fetchNext(prevRows, _limit, opts) {
|
|
805
|
+
const orderCol = this._state.orderBy[0]?.column ?? "received_timestamp";
|
|
806
|
+
const orderDir = this._state.orderBy[0]?.dir ?? "desc";
|
|
807
|
+
const lastRow = prevRows[prevRows.length - 1];
|
|
808
|
+
const lastValue = lastRow?.[orderCol];
|
|
809
|
+
if (lastValue === void 0) return okPage([], false);
|
|
810
|
+
const cursorOp = orderDir === "desc" ? "lt" : "gt";
|
|
811
|
+
const cursorFilter = { column: orderCol, op: cursorOp, value: lastValue };
|
|
812
|
+
const orderBy = this._state.orderBy.length > 0 ? this._state.orderBy : [{ column: orderCol, dir: orderDir }];
|
|
813
|
+
const nextBuilder = this._clone({
|
|
814
|
+
filters: [...this._state.filters, cursorFilter],
|
|
815
|
+
orderBy
|
|
816
|
+
});
|
|
817
|
+
return nextBuilder.fetch(opts);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
var FilteredStreamController = class extends StreamController {
|
|
821
|
+
constructor(inner, filters, columns) {
|
|
822
|
+
const transport = {
|
|
823
|
+
onEvent: null,
|
|
824
|
+
onStatus: null,
|
|
825
|
+
onError: null,
|
|
826
|
+
connect() {
|
|
827
|
+
inner.subscribe({
|
|
828
|
+
next: (event) => {
|
|
829
|
+
if (!matchesFilters(event.data, filters)) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const projected = columns.length > 0 ? {
|
|
833
|
+
...event,
|
|
834
|
+
data: projectColumns(event.data, columns)
|
|
835
|
+
} : event;
|
|
836
|
+
this.onEvent?.(projected);
|
|
837
|
+
},
|
|
838
|
+
status: (s) => this.onStatus?.(s),
|
|
839
|
+
error: (e) => this.onError?.(e)
|
|
840
|
+
});
|
|
841
|
+
},
|
|
842
|
+
disconnect() {
|
|
843
|
+
inner.close();
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
super(transport);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function matchesFilters(row, filters) {
|
|
850
|
+
for (const f of filters) {
|
|
851
|
+
const val = row[f.column];
|
|
852
|
+
if (!evaluateFilter(val, f.op, f.value)) return false;
|
|
853
|
+
}
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
function evaluateFilter(actual, op, expected) {
|
|
857
|
+
switch (op) {
|
|
858
|
+
case "eq":
|
|
859
|
+
return actual === expected;
|
|
860
|
+
case "neq":
|
|
861
|
+
return actual !== expected;
|
|
862
|
+
case "gt":
|
|
863
|
+
return compareOrdered(actual, expected, (a, b) => a > b);
|
|
864
|
+
case "gte":
|
|
865
|
+
return compareOrdered(actual, expected, (a, b) => a >= b);
|
|
866
|
+
case "lt":
|
|
867
|
+
return compareOrdered(actual, expected, (a, b) => a < b);
|
|
868
|
+
case "lte":
|
|
869
|
+
return compareOrdered(actual, expected, (a, b) => a <= b);
|
|
870
|
+
case "in":
|
|
871
|
+
return Array.isArray(expected) && expected.includes(actual);
|
|
872
|
+
case "like": {
|
|
873
|
+
if (typeof actual !== "string" || typeof expected !== "string") return false;
|
|
874
|
+
const escaped = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
875
|
+
const pattern = escaped.replace(/%/g, ".*").replace(/_/g, ".");
|
|
876
|
+
return new RegExp(`^${pattern}$`, "i").test(actual);
|
|
877
|
+
}
|
|
878
|
+
case "not_like": {
|
|
879
|
+
if (typeof actual !== "string" || typeof expected !== "string") return false;
|
|
880
|
+
const escaped = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
881
|
+
const pattern = escaped.replace(/%/g, ".*").replace(/_/g, ".");
|
|
882
|
+
return !new RegExp(`^${pattern}$`, "i").test(actual);
|
|
883
|
+
}
|
|
884
|
+
default:
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function compareOrdered(actual, expected, cmp) {
|
|
889
|
+
if (typeof actual === "number" && typeof expected === "number") {
|
|
890
|
+
return cmp(actual, expected);
|
|
891
|
+
}
|
|
892
|
+
if (typeof actual === "string" && typeof expected === "string") {
|
|
893
|
+
return cmp(actual, expected);
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
function projectColumns(row, columns) {
|
|
898
|
+
const result = {};
|
|
899
|
+
for (const col of columns) {
|
|
900
|
+
if (col in row) result[col] = row[col];
|
|
901
|
+
}
|
|
902
|
+
return result;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/table.ts
|
|
906
|
+
var TableRef = class {
|
|
907
|
+
_ctx;
|
|
908
|
+
_table;
|
|
909
|
+
_createStream;
|
|
910
|
+
constructor(ctx, table, createStream) {
|
|
911
|
+
this._ctx = ctx;
|
|
912
|
+
this._table = table;
|
|
913
|
+
this._createStream = createStream;
|
|
914
|
+
}
|
|
915
|
+
/** SELECT * shortcut — fetches rows with optional pagination. */
|
|
916
|
+
async fetch(opts) {
|
|
917
|
+
return this.select().limit(opts?.limit ?? 1e3).fetch(opts);
|
|
918
|
+
}
|
|
919
|
+
/** Start building a typed query. Returns an immutable, PromiseLike QueryBuilder. */
|
|
920
|
+
select(...columns) {
|
|
921
|
+
return new QueryBuilder(
|
|
922
|
+
this._ctx,
|
|
923
|
+
{
|
|
924
|
+
table: this._table,
|
|
925
|
+
columns,
|
|
926
|
+
aggregations: [],
|
|
927
|
+
filters: [],
|
|
928
|
+
groupBy: [],
|
|
929
|
+
orderBy: []
|
|
930
|
+
},
|
|
931
|
+
this._createStream
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
/** Insert one or more rows into this table. */
|
|
935
|
+
async insert(data, opts) {
|
|
936
|
+
if (Array.isArray(data)) {
|
|
937
|
+
const promises = data.map(
|
|
938
|
+
(row) => request(this._ctx, {
|
|
939
|
+
method: "POST",
|
|
940
|
+
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
941
|
+
body: row,
|
|
942
|
+
signal: opts?.signal
|
|
943
|
+
})
|
|
944
|
+
);
|
|
945
|
+
const results = await Promise.all(promises);
|
|
946
|
+
for (const res2 of results) {
|
|
947
|
+
if (res2.error) return err(res2.error);
|
|
948
|
+
}
|
|
949
|
+
return ok({ ok: true });
|
|
950
|
+
}
|
|
951
|
+
const { data: res, error } = await request(this._ctx, {
|
|
952
|
+
method: "POST",
|
|
953
|
+
path: `/v1/ingest?table=${encodeURIComponent(this._table)}`,
|
|
954
|
+
body: data,
|
|
955
|
+
signal: opts?.signal
|
|
956
|
+
});
|
|
957
|
+
if (error) return err(error);
|
|
958
|
+
const result = { ok: res?.ok ?? true };
|
|
959
|
+
if (res?.duplicate != null) result.duplicate = res.duplicate;
|
|
960
|
+
return ok(result);
|
|
961
|
+
}
|
|
962
|
+
/** Fetch the schema for this table. */
|
|
963
|
+
async schema(opts) {
|
|
964
|
+
const { data, error } = await request(this._ctx, {
|
|
965
|
+
method: "GET",
|
|
966
|
+
path: `/v1/schema?table=${encodeURIComponent(this._table)}`,
|
|
967
|
+
signal: opts?.signal
|
|
968
|
+
});
|
|
969
|
+
if (error) return err(error);
|
|
970
|
+
return ok(data);
|
|
971
|
+
}
|
|
972
|
+
/** Subscribe to live events for this table. */
|
|
973
|
+
stream(opts) {
|
|
974
|
+
return this._createStream(this._table, opts);
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/client.ts
|
|
979
|
+
var WaveHouseClient = class {
|
|
980
|
+
/** @internal */
|
|
981
|
+
_ctx;
|
|
982
|
+
/** Schema introspection namespace. */
|
|
983
|
+
schema;
|
|
984
|
+
/** Access control policy namespace (admin). */
|
|
985
|
+
policy;
|
|
986
|
+
/** Dead Letter Queue namespace. */
|
|
987
|
+
dlq;
|
|
988
|
+
/** System health/readiness namespace. */
|
|
989
|
+
sys;
|
|
990
|
+
/** Named query pipes admin namespace. */
|
|
991
|
+
pipes;
|
|
992
|
+
constructor(config) {
|
|
993
|
+
this._ctx = {
|
|
994
|
+
baseURL: config.baseURL.replace(/\/+$/, ""),
|
|
995
|
+
auth: config.auth,
|
|
996
|
+
options: {
|
|
997
|
+
maxRetries: config.options?.maxRetries ?? 2
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
this.schema = new SchemaNamespace(this._ctx);
|
|
1001
|
+
this.policy = new PolicyNamespace(this._ctx);
|
|
1002
|
+
this.dlq = new DLQNamespace(this._ctx, (table, opts) => this._createStream(table, opts));
|
|
1003
|
+
this.sys = new SysNamespace(this._ctx);
|
|
1004
|
+
this.pipes = new PipesNamespace(this._ctx);
|
|
1005
|
+
}
|
|
1006
|
+
/** Get a table reference for building queries, inserts, and streams. */
|
|
1007
|
+
from(table) {
|
|
1008
|
+
return new TableRef(
|
|
1009
|
+
this._ctx,
|
|
1010
|
+
table,
|
|
1011
|
+
(t, opts) => this._createStream(t, opts)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
/** Get a reference to a named query pipe. PromiseLike — `await` it to execute. */
|
|
1015
|
+
pipe(name, params) {
|
|
1016
|
+
return new PipeRef(this._ctx, name, params, (t, opts) => this._createStream(t, opts));
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Execute a raw SQL query against ClickHouse. Requires the admin role (the
|
|
1020
|
+
* configured `admin_role`, `"admin"` by default; there is no separate
|
|
1021
|
+
* `service` role). The endpoint proxies straight to ClickHouse's HTTP
|
|
1022
|
+
* interface so any ClickHouse-accepted SQL works; positional `?` param
|
|
1023
|
+
* binding is NOT supported — inline literals or use the structured query
|
|
1024
|
+
* builder for safe binding. See sql.ts for details.
|
|
1025
|
+
*/
|
|
1026
|
+
sql(query, opts) {
|
|
1027
|
+
if (Array.isArray(opts)) {
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
"[WaveHouse SDK] client.sql(sql, params) was removed. The /v1/admin/query endpoint does not accept positional `?` params. Inline literals into the SQL, or use the structured query builder (wh.from(table)\u2026) for safe binding from user input."
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
return sql(this._ctx, query, opts);
|
|
1033
|
+
}
|
|
1034
|
+
/** @internal Create a stream for the given table. */
|
|
1035
|
+
_createStream(table, opts) {
|
|
1036
|
+
if (typeof EventSource === "undefined") {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
"[WaveHouse SDK] Native EventSource is not available in this environment. Please provide a global polyfill (e.g., `globalThis.EventSource = require('eventsource')`)."
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
const transport = new SSETransport({
|
|
1042
|
+
baseURL: this._ctx.baseURL,
|
|
1043
|
+
table,
|
|
1044
|
+
since: opts?.since,
|
|
1045
|
+
auth: this._ctx.auth
|
|
1046
|
+
});
|
|
1047
|
+
const controller = new StreamController(transport);
|
|
1048
|
+
if (opts?.signal) controller.attachSignal(opts.signal);
|
|
1049
|
+
return controller;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
function createClient(config) {
|
|
1053
|
+
return new WaveHouseClient(config);
|
|
1054
|
+
}
|
|
1055
|
+
export {
|
|
1056
|
+
DLQNamespace,
|
|
1057
|
+
LiveQuery,
|
|
1058
|
+
PipeRef,
|
|
1059
|
+
PipesNamespace,
|
|
1060
|
+
PolicyNamespace,
|
|
1061
|
+
QueryBuilder,
|
|
1062
|
+
SchemaNamespace,
|
|
1063
|
+
StreamController,
|
|
1064
|
+
SysNamespace,
|
|
1065
|
+
TableRef,
|
|
1066
|
+
WaveHouseClient,
|
|
1067
|
+
createClient
|
|
1068
|
+
};
|
|
1069
|
+
//# sourceMappingURL=index.js.map
|