@c9up/eon 0.1.0 → 0.2.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 +5 -0
- package/dist/EonProvider.d.ts +6 -0
- package/dist/EonProvider.d.ts.map +1 -1
- package/dist/EonProvider.js +43 -0
- package/dist/EonProvider.js.map +1 -1
- package/dist/configure.d.ts +17 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +19 -0
- package/dist/configure.js.map +1 -0
- package/dist/connection/config.d.ts +8 -0
- package/dist/connection/config.d.ts.map +1 -1
- package/dist/connection/config.js.map +1 -1
- package/dist/connection/websocket.d.ts +13 -0
- package/dist/connection/websocket.d.ts.map +1 -1
- package/dist/connection/websocket.js +16 -1
- package/dist/connection/websocket.js.map +1 -1
- package/dist/health.d.ts +50 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +73 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/query/TimeSeriesQuery.d.ts +12 -1
- package/dist/query/TimeSeriesQuery.d.ts.map +1 -1
- package/dist/query/TimeSeriesQuery.js +12 -1
- package/dist/query/TimeSeriesQuery.js.map +1 -1
- package/dist/schema/EonMigrationRunner.d.ts +35 -3
- package/dist/schema/EonMigrationRunner.d.ts.map +1 -1
- package/dist/schema/EonMigrationRunner.js +169 -7
- package/dist/schema/EonMigrationRunner.js.map +1 -1
- package/dist/schema/index.d.ts +15 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/testing/FakeEonConnection.d.ts +1 -1
- package/dist/testing/FakeEonConnection.d.ts.map +1 -1
- package/dist/testing/FakeEonConnection.js +27 -3
- package/dist/testing/FakeEonConnection.js.map +1 -1
- package/dist/testing/FakeEonConsumer.d.ts +28 -0
- package/dist/testing/FakeEonConsumer.d.ts.map +1 -0
- package/dist/testing/FakeEonConsumer.js +66 -0
- package/dist/testing/FakeEonConsumer.js.map +1 -0
- package/dist/testing/cleanup.d.ts +50 -0
- package/dist/testing/cleanup.d.ts.map +1 -0
- package/dist/testing/cleanup.js +88 -0
- package/dist/testing/cleanup.js.map +1 -0
- package/dist/testing/index.d.ts +6 -6
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -14
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/vitest.d.ts +7 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +28 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/tmq/EonConsumer.d.ts +99 -0
- package/dist/tmq/EonConsumer.d.ts.map +1 -0
- package/dist/tmq/EonConsumer.js +27 -0
- package/dist/tmq/EonConsumer.js.map +1 -0
- package/dist/tmq/index.d.ts +4 -0
- package/dist/tmq/index.d.ts.map +1 -0
- package/dist/tmq/index.js +4 -0
- package/dist/tmq/index.js.map +1 -0
- package/dist/tmq/websocket.d.ts +11 -0
- package/dist/tmq/websocket.d.ts.map +1 -0
- package/dist/tmq/websocket.js +189 -0
- package/dist/tmq/websocket.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +12 -3
- package/src/EonProvider.ts +61 -0
- package/src/configure.ts +38 -0
- package/src/connection/config.ts +8 -0
- package/src/connection/websocket.ts +17 -0
- package/src/health.ts +99 -0
- package/src/index.ts +17 -1
- package/src/query/TimeSeriesQuery.ts +12 -1
- package/src/schema/EonMigrationRunner.ts +205 -9
- package/src/schema/index.ts +28 -0
- package/src/testing/FakeEonConnection.ts +37 -7
- package/src/testing/FakeEonConsumer.ts +93 -0
- package/src/testing/cleanup.ts +120 -0
- package/src/testing/index.ts +11 -14
- package/src/testing/vitest.ts +27 -0
- package/src/tmq/EonConsumer.ts +111 -0
- package/src/tmq/index.ts +10 -0
- package/src/tmq/websocket.ts +217 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket implementation of {@link EonConsumer}, over `@tdengine/websocket`'s
|
|
3
|
+
* `WsConsumer` (ws-only via taosAdapter, like the SQL connection).
|
|
4
|
+
*
|
|
5
|
+
* Thin by design: the driver already speaks TMQ. What this adds is the seam
|
|
6
|
+
* (no ws type leaks out), decoded rows instead of a `TaosResult`, one error
|
|
7
|
+
* type, and two defaults chosen for safety rather than convenience — see
|
|
8
|
+
* `autoCommit` and `offsetReset`.
|
|
9
|
+
*/
|
|
10
|
+
import { TDWebSocketClientError, TMQConstants, WsConsumer, } from "@tdengine/websocket";
|
|
11
|
+
import { EonConsumerError, } from "./EonConsumer.js";
|
|
12
|
+
function wrapError(cause, message) {
|
|
13
|
+
if (cause instanceof TDWebSocketClientError) {
|
|
14
|
+
return new EonConsumerError(`${message}: ${cause.message}`, {
|
|
15
|
+
cause,
|
|
16
|
+
code: cause.code,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return new EonConsumerError(message, { cause });
|
|
20
|
+
}
|
|
21
|
+
/** Zip a poll result's column metadata with its rows. */
|
|
22
|
+
function decode(result) {
|
|
23
|
+
const meta = result.getMeta() ?? [];
|
|
24
|
+
const data = result.getData() ?? [];
|
|
25
|
+
if (data.length > 0 && meta.length === 0) {
|
|
26
|
+
// The same trap the query decoder guards: rows with no column metadata
|
|
27
|
+
// would each decode to `{}` — silent data loss. Fail loud.
|
|
28
|
+
throw new EonConsumerError(`[E_EON_DECODE] poll returned rows but no column metadata for topic '${result.getTopic()}'; cannot decode`);
|
|
29
|
+
}
|
|
30
|
+
return data.map((row) => {
|
|
31
|
+
const record = {};
|
|
32
|
+
meta.forEach((column, index) => {
|
|
33
|
+
record[column.name] = row[index];
|
|
34
|
+
});
|
|
35
|
+
return record;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function toPartition(p) {
|
|
39
|
+
return {
|
|
40
|
+
topic: p.topic,
|
|
41
|
+
vgroupId: p.vgroup_id,
|
|
42
|
+
offset: p.offset,
|
|
43
|
+
begin: p.begin,
|
|
44
|
+
end: p.end,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Open a consumer against a taosAdapter WebSocket endpoint.
|
|
49
|
+
*
|
|
50
|
+
* `autoCommit` defaults to FALSE, where the driver defaults it on. Auto-commit
|
|
51
|
+
* acknowledges messages the handler may not have finished with, so a crash
|
|
52
|
+
* drops them silently; a subscription that loses data without saying so is
|
|
53
|
+
* worse than one that redelivers.
|
|
54
|
+
*/
|
|
55
|
+
export async function connectWsConsumer(config) {
|
|
56
|
+
const options = new Map([
|
|
57
|
+
[TMQConstants.WS_URL, config.url],
|
|
58
|
+
[TMQConstants.GROUP_ID, config.groupId],
|
|
59
|
+
[TMQConstants.AUTO_OFFSET_RESET, config.offsetReset ?? "latest"],
|
|
60
|
+
[
|
|
61
|
+
TMQConstants.ENABLE_AUTO_COMMIT,
|
|
62
|
+
config.autoCommit === true ? "true" : "false",
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
TMQConstants.MSG_WITH_TABLE_NAME,
|
|
66
|
+
config.withTableName === false ? "false" : "true",
|
|
67
|
+
],
|
|
68
|
+
]);
|
|
69
|
+
if (config.clientId)
|
|
70
|
+
options.set(TMQConstants.CLIENT_ID, config.clientId);
|
|
71
|
+
if (config.user)
|
|
72
|
+
options.set(TMQConstants.CONNECT_USER, config.user);
|
|
73
|
+
if (config.password)
|
|
74
|
+
options.set(TMQConstants.CONNECT_PASS, config.password);
|
|
75
|
+
if (config.token)
|
|
76
|
+
options.set(TMQConstants.CONNECT_TOKEN, config.token);
|
|
77
|
+
if (config.autoCommit === true && config.autoCommitIntervalMs !== undefined) {
|
|
78
|
+
options.set(TMQConstants.AUTO_COMMIT_INTERVAL_MS, String(config.autoCommitIntervalMs));
|
|
79
|
+
}
|
|
80
|
+
let consumer;
|
|
81
|
+
try {
|
|
82
|
+
consumer = await WsConsumer.newConsumer(options);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw wrapError(error, `eon: failed to open a consumer on '${config.url}'`);
|
|
86
|
+
}
|
|
87
|
+
let closed = false;
|
|
88
|
+
function ensureOpen() {
|
|
89
|
+
if (closed) {
|
|
90
|
+
throw new EonConsumerError("eon: consumer is closed");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
async subscribe(topics) {
|
|
95
|
+
ensureOpen();
|
|
96
|
+
if (topics.length === 0) {
|
|
97
|
+
// An empty subscribe is accepted by the server and then delivers
|
|
98
|
+
// nothing forever — a silent no-op is the worst possible answer.
|
|
99
|
+
throw new EonConsumerError("eon: subscribe() needs at least one topic");
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await consumer.subscribe(topics);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw wrapError(error, `eon: subscribe failed for [${topics.join(", ")}]`);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async subscription() {
|
|
109
|
+
ensureOpen();
|
|
110
|
+
try {
|
|
111
|
+
return await consumer.subscription();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw wrapError(error, "eon: reading the subscription failed");
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async poll(timeoutMs) {
|
|
118
|
+
ensureOpen();
|
|
119
|
+
let polled;
|
|
120
|
+
try {
|
|
121
|
+
polled = await consumer.poll(timeoutMs);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw wrapError(error, "eon: poll failed");
|
|
125
|
+
}
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const [topic, result] of polled) {
|
|
128
|
+
out.push({ topic, rows: decode(result) });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
},
|
|
132
|
+
async commit() {
|
|
133
|
+
ensureOpen();
|
|
134
|
+
try {
|
|
135
|
+
return (await consumer.commit()).map(toPartition);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw wrapError(error, "eon: commit failed");
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async assignment(topics) {
|
|
142
|
+
ensureOpen();
|
|
143
|
+
try {
|
|
144
|
+
return (await consumer.assignment(topics)).map(toPartition);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw wrapError(error, "eon: reading the assignment failed");
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
async seek(partition) {
|
|
151
|
+
ensureOpen();
|
|
152
|
+
if (partition.offset === undefined) {
|
|
153
|
+
throw new EonConsumerError("eon: seek() needs an offset");
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await consumer.seek({
|
|
157
|
+
topic: partition.topic,
|
|
158
|
+
vgroup_id: partition.vgroupId,
|
|
159
|
+
offset: partition.offset,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
throw wrapError(error, `eon: seek failed on '${partition.topic}'`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
async unsubscribe() {
|
|
167
|
+
ensureOpen();
|
|
168
|
+
try {
|
|
169
|
+
await consumer.unsubscribe();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw wrapError(error, "eon: unsubscribe failed");
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
async close() {
|
|
176
|
+
// Idempotent: a `finally` that closes twice must not throw.
|
|
177
|
+
if (closed)
|
|
178
|
+
return;
|
|
179
|
+
closed = true;
|
|
180
|
+
try {
|
|
181
|
+
await consumer.close();
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
throw wrapError(error, "eon: closing the consumer failed");
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/tmq/websocket.ts"],"names":[],"mappings":"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"}
|
package/index.darwin-arm64.node
CHANGED
|
Binary file
|
package/index.darwin-x64.node
CHANGED
|
Binary file
|
|
Binary file
|
package/index.linux-x64-gnu.node
CHANGED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/eon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Eon — time-series data layer for the Ream framework (TDengine-backed)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@biomejs/biome": "^2.4.10",
|
|
41
|
+
"@types/node": "^22",
|
|
42
|
+
"tsx": "^4.19.0",
|
|
41
43
|
"typescript": "^6.0.2",
|
|
42
|
-
"vitest": "^4.1.2"
|
|
43
|
-
"@types/node": "^22"
|
|
44
|
+
"vitest": "^4.1.2"
|
|
44
45
|
},
|
|
45
46
|
"engines": {
|
|
46
47
|
"node": ">=22.0.0"
|
|
@@ -52,6 +53,14 @@
|
|
|
52
53
|
"type": "git",
|
|
53
54
|
"url": "git+https://github.com/C9up/eon.git"
|
|
54
55
|
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"vitest": ">=3"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"vitest": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
55
64
|
"scripts": {
|
|
56
65
|
"build": "tsc -p tsconfig.build.json",
|
|
57
66
|
"build:rust": "cargo build --release -p eon-query-napi",
|
package/src/EonProvider.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface EonAppContext {
|
|
|
33
33
|
token: string | symbol | (new (...args: never[]) => unknown),
|
|
34
34
|
factory: () => unknown,
|
|
35
35
|
): void;
|
|
36
|
+
/**
|
|
37
|
+
* Optional reader — present on ream's real container. Used to look up the
|
|
38
|
+
* `migrations` registry so `ream migrate` can drive eon without naming it.
|
|
39
|
+
* Structural, async-capable, like the binder above.
|
|
40
|
+
*/
|
|
41
|
+
resolve?(
|
|
42
|
+
token: string | symbol | (new (...args: never[]) => unknown),
|
|
43
|
+
): unknown | Promise<unknown>;
|
|
36
44
|
};
|
|
37
45
|
config: { get<T = unknown>(key: string): T | undefined };
|
|
38
46
|
}
|
|
@@ -152,9 +160,53 @@ export class EonProvider {
|
|
|
152
160
|
// construction time.
|
|
153
161
|
const { setConnection } = await import("./services/connection.js");
|
|
154
162
|
setConnection(defaultConn);
|
|
163
|
+
|
|
164
|
+
await this.#registerMigrationSource(defaultConn, config);
|
|
155
165
|
this.#booted = true;
|
|
156
166
|
}
|
|
157
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Hand eon's migration runner to the framework's `migrations` registry, so
|
|
170
|
+
* `ream migrate` drives it without the CLI knowing eon exists.
|
|
171
|
+
*
|
|
172
|
+
* Best-effort and duck-typed: eon does not import `@c9up/ream`, and a host
|
|
173
|
+
* with no registry (an older ream, or another framework) must still boot.
|
|
174
|
+
* The CLI is where a missing registry gets reported, because that is where
|
|
175
|
+
* the user can act on it.
|
|
176
|
+
*/
|
|
177
|
+
async #registerMigrationSource(
|
|
178
|
+
conn: EonConnection,
|
|
179
|
+
config: EonConfig,
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const resolve = this.#app.container.resolve;
|
|
182
|
+
if (typeof resolve !== "function") return;
|
|
183
|
+
|
|
184
|
+
let registry: unknown;
|
|
185
|
+
try {
|
|
186
|
+
registry = await resolve.call(this.#app.container, "migrations");
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
typeof registry !== "object" ||
|
|
192
|
+
registry === null ||
|
|
193
|
+
!("register" in registry) ||
|
|
194
|
+
typeof registry.register !== "function"
|
|
195
|
+
) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { EonMigrationRunner } = await import(
|
|
200
|
+
"./schema/EonMigrationRunner.js"
|
|
201
|
+
);
|
|
202
|
+
const migrationsDir = config.migrationsDir ?? "database/eon-migrations";
|
|
203
|
+
(registry.register as (source: unknown) => unknown)({
|
|
204
|
+
name: "eon",
|
|
205
|
+
directory: migrationsDir,
|
|
206
|
+
runner: new EonMigrationRunner(conn, { migrationsDir }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
158
210
|
async shutdown(): Promise<void> {
|
|
159
211
|
// Close every connection in parallel. `allSettled` (fail-open) so a single
|
|
160
212
|
// stuck close doesn't block the rest, but failures are aggregated and
|
|
@@ -168,6 +220,15 @@ export class EonProvider {
|
|
|
168
220
|
const { clearConnection } = await import("./services/connection.js");
|
|
169
221
|
for (const [, conn] of named) clearConnection(conn);
|
|
170
222
|
|
|
223
|
+
// Closing every connection is NOT enough to let Node exit: the connector
|
|
224
|
+
// keeps process-global handles, so a service that shut down cleanly would
|
|
225
|
+
// hang forever. Only for a real ws connection — a fake-only boot (tests)
|
|
226
|
+
// must not pull the connector in. Dynamic import for the same reason.
|
|
227
|
+
if (named.some(([, c]) => c.transport === "websocket")) {
|
|
228
|
+
const { destroyEonConnector } = await import("./connection/websocket.js");
|
|
229
|
+
await destroyEonConnector();
|
|
230
|
+
}
|
|
231
|
+
|
|
171
232
|
const errors = results
|
|
172
233
|
.map((result, i) =>
|
|
173
234
|
result.status === "rejected"
|
package/src/configure.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream configure @c9up/eon` — wire eon into an app in one command.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors atlas's hook: register the provider, seed the env vars, write the
|
|
5
|
+
* config file. Without it the CLI can only report that eon exports no
|
|
6
|
+
* configure(), and every app wires eon by hand.
|
|
7
|
+
*/
|
|
8
|
+
interface Codemods {
|
|
9
|
+
addProvider(importPath: string): Promise<void>;
|
|
10
|
+
addEnvVars(vars: Record<string, string>): Promise<void>;
|
|
11
|
+
writeFile(
|
|
12
|
+
filePath: string,
|
|
13
|
+
content: string,
|
|
14
|
+
options?: { force?: boolean },
|
|
15
|
+
): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function configure(codemods: Codemods): Promise<void> {
|
|
19
|
+
await codemods.addProvider("@c9up/eon/provider");
|
|
20
|
+
await codemods.addEnvVars({
|
|
21
|
+
TDENGINE_URL: "ws://localhost:6041",
|
|
22
|
+
TDENGINE_USER: "root",
|
|
23
|
+
TDENGINE_PASSWORD: "taosdata",
|
|
24
|
+
TDENGINE_DATABASE: "ream",
|
|
25
|
+
});
|
|
26
|
+
await codemods.writeFile(
|
|
27
|
+
"config/timeseries.ts",
|
|
28
|
+
`import { defineConfig } from '@c9up/eon'
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
url: process.env.TDENGINE_URL ?? 'ws://localhost:6041',
|
|
32
|
+
user: process.env.TDENGINE_USER ?? 'root',
|
|
33
|
+
password: process.env.TDENGINE_PASSWORD ?? '',
|
|
34
|
+
database: process.env.TDENGINE_DATABASE ?? 'ream',
|
|
35
|
+
})
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
38
|
+
}
|
package/src/connection/config.ts
CHANGED
|
@@ -52,6 +52,14 @@ export interface EonConfig extends EonConnectionConfig {
|
|
|
52
52
|
* `connections[default].url`.
|
|
53
53
|
*/
|
|
54
54
|
connections?: Record<string, EonConnectionConfig>;
|
|
55
|
+
/**
|
|
56
|
+
* Where eon's migration files live, relative to the app root.
|
|
57
|
+
*
|
|
58
|
+
* Separate from atlas's `database/migrations`: the two stores migrate
|
|
59
|
+
* independently, and one directory holding both would hand every file to
|
|
60
|
+
* both runners. Default: `database/eon-migrations`.
|
|
61
|
+
*/
|
|
62
|
+
migrationsDir?: string;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/** Identity helper for typed config authoring — mirrors atlas `defineConfig`. */
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
destroy,
|
|
10
11
|
Precision,
|
|
11
12
|
SchemalessProto,
|
|
12
13
|
type StmtBindParams,
|
|
@@ -232,6 +233,22 @@ async function connectWithRetry(config: EonConnectionConfig): Promise<WsSql> {
|
|
|
232
233
|
);
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Tear down the connector's PROCESS-GLOBAL resources.
|
|
238
|
+
*
|
|
239
|
+
* `EonConnection.close()` closes one connection and deliberately leaves this
|
|
240
|
+
* alone (D6). But the connector keeps process-wide handles alive, so closing
|
|
241
|
+
* every connection is NOT enough for Node to exit — a service that shuts down
|
|
242
|
+
* cleanly would hang forever. Call this once, after the last connection is
|
|
243
|
+
* closed; `EonProvider.shutdown()` already does.
|
|
244
|
+
*
|
|
245
|
+
* Reversible: a later `connectWsEon` reconnects normally (verified against a
|
|
246
|
+
* live server), so a boot → shutdown → boot cycle is fine.
|
|
247
|
+
*/
|
|
248
|
+
export async function destroyEonConnector(): Promise<void> {
|
|
249
|
+
await destroy();
|
|
250
|
+
}
|
|
251
|
+
|
|
235
252
|
/**
|
|
236
253
|
* Connect to TDengine over WebSocket and return a transport-agnostic
|
|
237
254
|
* `EonConnection`. Credentials go through `WSConfig` setters, never embedded in
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A health check for a TDengine connection.
|
|
3
|
+
*
|
|
4
|
+
* Upstream ships one per datastore — `DbConnectionCountCheck` in lucid,
|
|
5
|
+
* `RedisMemoryUsageCheck` in redis — and ream now has the runner
|
|
6
|
+
* (`@c9up/ream/health`). This is eon's.
|
|
7
|
+
*
|
|
8
|
+
* AGNOSTIC BY CONSTRUCTION — the contract is re-declared here rather than
|
|
9
|
+
* imported from `@c9up/ream`. eon must not depend on the framework at runtime,
|
|
10
|
+
* and ream's `HealthChecks` consumes anything of this shape: a `name`, an
|
|
11
|
+
* optional `cacheDuration`, and a `run()`.
|
|
12
|
+
*/
|
|
13
|
+
import type { EonConnection } from "./connection/EonConnection.js";
|
|
14
|
+
|
|
15
|
+
/** The result shape ream's health runner consumes. */
|
|
16
|
+
export interface EonHealthResult {
|
|
17
|
+
message: string;
|
|
18
|
+
status: "ok" | "warning" | "error";
|
|
19
|
+
finishedAt: Date;
|
|
20
|
+
meta?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reports whether the server answers, and how slowly.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* healthChecks.register([
|
|
28
|
+
* new EonConnectionCheck(conn).warnWhenSlowerThan(250).cacheFor(30),
|
|
29
|
+
* ])
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class EonConnectionCheck {
|
|
33
|
+
readonly #conn: EonConnection;
|
|
34
|
+
#warnAfterMs = 500;
|
|
35
|
+
|
|
36
|
+
name = "TDengine connection check";
|
|
37
|
+
/** Seconds to reuse the last result for. A probe every second must not ping every second. */
|
|
38
|
+
cacheDuration?: number;
|
|
39
|
+
|
|
40
|
+
constructor(conn: EonConnection) {
|
|
41
|
+
this.#conn = conn;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Rename the check, so two connections stay distinguishable in a report. */
|
|
45
|
+
as(name: string): this {
|
|
46
|
+
this.name = name;
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reuse the last result for this many seconds. */
|
|
51
|
+
cacheFor(seconds: number): this {
|
|
52
|
+
this.cacheDuration = seconds;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Warn once a ping takes longer than this.
|
|
58
|
+
*
|
|
59
|
+
* A reachable-but-slow server is the case worth reporting: it is not down,
|
|
60
|
+
* so failing would pull a healthy pod out of rotation, and it is not fine,
|
|
61
|
+
* so `ok` would hide the thing an operator needs to see.
|
|
62
|
+
*/
|
|
63
|
+
warnWhenSlowerThan(milliseconds: number): this {
|
|
64
|
+
this.#warnAfterMs = milliseconds;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run(): Promise<EonHealthResult> {
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
await this.#conn.ping();
|
|
72
|
+
const latency = Date.now() - startedAt;
|
|
73
|
+
const meta = { latency, warningThreshold: this.#warnAfterMs };
|
|
74
|
+
if (latency >= this.#warnAfterMs) {
|
|
75
|
+
return {
|
|
76
|
+
status: "warning",
|
|
77
|
+
message: `TDengine answered in ${latency}ms, above the threshold of ${this.#warnAfterMs}ms`,
|
|
78
|
+
finishedAt: new Date(),
|
|
79
|
+
meta,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
status: "ok",
|
|
84
|
+
message: "TDengine is reachable",
|
|
85
|
+
finishedAt: new Date(),
|
|
86
|
+
meta,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
status: "error",
|
|
91
|
+
message: "TDengine is unreachable",
|
|
92
|
+
finishedAt: new Date(),
|
|
93
|
+
// The error itself, not just its message: a caller that logs the
|
|
94
|
+
// report keeps the stack, and a dedup key must never be a message.
|
|
95
|
+
meta: { error, latency: Date.now() - startedAt },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,10 @@ export {
|
|
|
16
16
|
type EonSchemalessOptions,
|
|
17
17
|
type EonSchemalessPrecision,
|
|
18
18
|
} from "./connection/EonConnection.js";
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
connectWsEon,
|
|
21
|
+
destroyEonConnector,
|
|
22
|
+
} from "./connection/websocket.js";
|
|
20
23
|
export {
|
|
21
24
|
Column,
|
|
22
25
|
type EonColumnMetadata,
|
|
@@ -38,6 +41,10 @@ export type {
|
|
|
38
41
|
EonService,
|
|
39
42
|
} from "./EonProvider.js";
|
|
40
43
|
export { EonProvider } from "./EonProvider.js";
|
|
44
|
+
export {
|
|
45
|
+
EonConnectionCheck,
|
|
46
|
+
type EonHealthResult,
|
|
47
|
+
} from "./health.js";
|
|
41
48
|
export { toLineProtocol } from "./ingest/schemaless.js";
|
|
42
49
|
export { buildLiteralInserts } from "./ingest/sql.js";
|
|
43
50
|
export {
|
|
@@ -116,3 +123,12 @@ export {
|
|
|
116
123
|
dropSuperTable,
|
|
117
124
|
syncSuperTable,
|
|
118
125
|
} from "./schema/sync.js";
|
|
126
|
+
export {
|
|
127
|
+
type EonConsumer,
|
|
128
|
+
type EonConsumerConfig,
|
|
129
|
+
EonConsumerError,
|
|
130
|
+
type EonMessage,
|
|
131
|
+
type EonOffsetReset,
|
|
132
|
+
type EonTopicPartition,
|
|
133
|
+
} from "./tmq/EonConsumer.js";
|
|
134
|
+
export { connectWsConsumer } from "./tmq/websocket.js";
|
|
@@ -302,7 +302,18 @@ export class TimeSeriesQuery<TPoint extends object = Record<string, unknown>>
|
|
|
302
302
|
return this;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* `FILL(mode[, ...values])` — requires an `interval`; `values` only for
|
|
307
|
+
* `value` mode.
|
|
308
|
+
*
|
|
309
|
+
* TDengine additionally requires a **bounded time range** alongside `FILL`:
|
|
310
|
+
* without one the server answers `9787` ("Start(end) time of query range
|
|
311
|
+
* required or time range too large"), because filling gaps has no meaning
|
|
312
|
+
* over an unbounded window. Pair it with {@link whereBetween} on the
|
|
313
|
+
* timestamp column. Not validated here — the bound may legitimately come
|
|
314
|
+
* from any combination of predicates, and rejecting a valid query would be
|
|
315
|
+
* worse than the server's own error.
|
|
316
|
+
*/
|
|
306
317
|
fill(mode: EonFillMode, ...values: readonly EonScalar[]): this {
|
|
307
318
|
this.#cachedExec = undefined;
|
|
308
319
|
this.#fill = { mode, values };
|