@c9up/eon 0.1.0 → 0.2.1

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.
Files changed (91) hide show
  1. package/README.md +5 -0
  2. package/dist/EonProvider.d.ts +6 -0
  3. package/dist/EonProvider.d.ts.map +1 -1
  4. package/dist/EonProvider.js +43 -0
  5. package/dist/EonProvider.js.map +1 -1
  6. package/dist/configure.d.ts +17 -0
  7. package/dist/configure.d.ts.map +1 -0
  8. package/dist/configure.js +19 -0
  9. package/dist/configure.js.map +1 -0
  10. package/dist/connection/config.d.ts +8 -0
  11. package/dist/connection/config.d.ts.map +1 -1
  12. package/dist/connection/config.js.map +1 -1
  13. package/dist/connection/websocket.d.ts +13 -0
  14. package/dist/connection/websocket.d.ts.map +1 -1
  15. package/dist/connection/websocket.js +16 -1
  16. package/dist/connection/websocket.js.map +1 -1
  17. package/dist/health.d.ts +50 -0
  18. package/dist/health.d.ts.map +1 -0
  19. package/dist/health.js +73 -0
  20. package/dist/health.js.map +1 -0
  21. package/dist/index.d.ts +4 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/query/TimeSeriesQuery.d.ts +12 -1
  26. package/dist/query/TimeSeriesQuery.d.ts.map +1 -1
  27. package/dist/query/TimeSeriesQuery.js +12 -1
  28. package/dist/query/TimeSeriesQuery.js.map +1 -1
  29. package/dist/schema/EonMigrationRunner.d.ts +35 -3
  30. package/dist/schema/EonMigrationRunner.d.ts.map +1 -1
  31. package/dist/schema/EonMigrationRunner.js +169 -7
  32. package/dist/schema/EonMigrationRunner.js.map +1 -1
  33. package/dist/schema/index.d.ts +15 -0
  34. package/dist/schema/index.d.ts.map +1 -0
  35. package/dist/schema/index.js +15 -0
  36. package/dist/schema/index.js.map +1 -0
  37. package/dist/testing/FakeEonConnection.d.ts +1 -1
  38. package/dist/testing/FakeEonConnection.d.ts.map +1 -1
  39. package/dist/testing/FakeEonConnection.js +27 -3
  40. package/dist/testing/FakeEonConnection.js.map +1 -1
  41. package/dist/testing/FakeEonConsumer.d.ts +28 -0
  42. package/dist/testing/FakeEonConsumer.d.ts.map +1 -0
  43. package/dist/testing/FakeEonConsumer.js +66 -0
  44. package/dist/testing/FakeEonConsumer.js.map +1 -0
  45. package/dist/testing/cleanup.d.ts +50 -0
  46. package/dist/testing/cleanup.d.ts.map +1 -0
  47. package/dist/testing/cleanup.js +88 -0
  48. package/dist/testing/cleanup.js.map +1 -0
  49. package/dist/testing/index.d.ts +6 -6
  50. package/dist/testing/index.d.ts.map +1 -1
  51. package/dist/testing/index.js +6 -14
  52. package/dist/testing/index.js.map +1 -1
  53. package/dist/testing/vitest.d.ts +7 -0
  54. package/dist/testing/vitest.d.ts.map +1 -0
  55. package/dist/testing/vitest.js +28 -0
  56. package/dist/testing/vitest.js.map +1 -0
  57. package/dist/tmq/EonConsumer.d.ts +99 -0
  58. package/dist/tmq/EonConsumer.d.ts.map +1 -0
  59. package/dist/tmq/EonConsumer.js +27 -0
  60. package/dist/tmq/EonConsumer.js.map +1 -0
  61. package/dist/tmq/index.d.ts +4 -0
  62. package/dist/tmq/index.d.ts.map +1 -0
  63. package/dist/tmq/index.js +4 -0
  64. package/dist/tmq/index.js.map +1 -0
  65. package/dist/tmq/websocket.d.ts +11 -0
  66. package/dist/tmq/websocket.d.ts.map +1 -0
  67. package/dist/tmq/websocket.js +189 -0
  68. package/dist/tmq/websocket.js.map +1 -0
  69. package/index.darwin-arm64.node +0 -0
  70. package/index.darwin-x64.node +0 -0
  71. package/index.linux-arm64-gnu.node +0 -0
  72. package/index.linux-x64-gnu.node +0 -0
  73. package/index.win32-x64-msvc.node +0 -0
  74. package/package.json +32 -3
  75. package/src/EonProvider.ts +61 -0
  76. package/src/configure.ts +38 -0
  77. package/src/connection/config.ts +8 -0
  78. package/src/connection/websocket.ts +17 -0
  79. package/src/health.ts +99 -0
  80. package/src/index.ts +17 -1
  81. package/src/query/TimeSeriesQuery.ts +12 -1
  82. package/src/schema/EonMigrationRunner.ts +205 -9
  83. package/src/schema/index.ts +28 -0
  84. package/src/testing/FakeEonConnection.ts +37 -7
  85. package/src/testing/FakeEonConsumer.ts +93 -0
  86. package/src/testing/cleanup.ts +120 -0
  87. package/src/testing/index.ts +11 -14
  88. package/src/testing/vitest.ts +27 -0
  89. package/src/tmq/EonConsumer.ts +111 -0
  90. package/src/tmq/index.ts +10 -0
  91. 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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"}
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/eon",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Eon — time-series data layer for the Ream framework (TDengine-backed)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,10 +19,26 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "import": "./dist/index.js"
21
21
  },
22
+ "./configure": {
23
+ "types": "./dist/configure.d.ts",
24
+ "import": "./dist/configure.js"
25
+ },
26
+ "./health": {
27
+ "types": "./dist/health.d.ts",
28
+ "import": "./dist/health.js"
29
+ },
22
30
  "./provider": {
23
31
  "types": "./dist/EonProvider.d.ts",
24
32
  "import": "./dist/EonProvider.js"
25
33
  },
34
+ "./schema": {
35
+ "types": "./dist/schema/index.d.ts",
36
+ "import": "./dist/schema/index.js"
37
+ },
38
+ "./tmq": {
39
+ "types": "./dist/tmq/index.d.ts",
40
+ "import": "./dist/tmq/index.js"
41
+ },
26
42
  "./services/connection": {
27
43
  "types": "./dist/services/connection.d.ts",
28
44
  "import": "./dist/services/connection.js"
@@ -30,6 +46,10 @@
30
46
  "./testing": {
31
47
  "types": "./dist/testing/index.d.ts",
32
48
  "import": "./dist/testing/index.js"
49
+ },
50
+ "./testing/vitest": {
51
+ "types": "./dist/testing/vitest.d.ts",
52
+ "import": "./dist/testing/vitest.js"
33
53
  }
34
54
  },
35
55
  "dependencies": {
@@ -38,9 +58,10 @@
38
58
  },
39
59
  "devDependencies": {
40
60
  "@biomejs/biome": "^2.4.10",
61
+ "@types/node": "^22",
62
+ "tsx": "^4.19.0",
41
63
  "typescript": "^6.0.2",
42
- "vitest": "^4.1.2",
43
- "@types/node": "^22"
64
+ "vitest": "^4.1.2"
44
65
  },
45
66
  "engines": {
46
67
  "node": ">=22.0.0"
@@ -52,6 +73,14 @@
52
73
  "type": "git",
53
74
  "url": "git+https://github.com/C9up/eon.git"
54
75
  },
76
+ "peerDependencies": {
77
+ "vitest": ">=3"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "vitest": {
81
+ "optional": true
82
+ }
83
+ },
55
84
  "scripts": {
56
85
  "build": "tsc -p tsconfig.build.json",
57
86
  "build:rust": "cargo build --release -p eon-query-napi",
@@ -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"
@@ -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
+ }
@@ -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 { connectWsEon } from "./connection/websocket.js";
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
- /** `FILL(mode[, ...values])` — requires an `interval`; `values` only for `value` mode. */
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 };