camusdb 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.
Files changed (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +950 -0
  3. package/dist/auth/credentials.d.ts +30 -0
  4. package/dist/auth/credentials.d.ts.map +1 -0
  5. package/dist/auth/credentials.js +23 -0
  6. package/dist/auth/credentials.js.map +1 -0
  7. package/dist/auth/login-client.d.ts +37 -0
  8. package/dist/auth/login-client.d.ts.map +1 -0
  9. package/dist/auth/login-client.js +8 -0
  10. package/dist/auth/login-client.js.map +1 -0
  11. package/dist/auth/rest-login-client.d.ts +17 -0
  12. package/dist/auth/rest-login-client.d.ts.map +1 -0
  13. package/dist/auth/rest-login-client.js +71 -0
  14. package/dist/auth/rest-login-client.js.map +1 -0
  15. package/dist/auth/token-provider.d.ts +123 -0
  16. package/dist/auth/token-provider.d.ts.map +1 -0
  17. package/dist/auth/token-provider.js +209 -0
  18. package/dist/auth/token-provider.js.map +1 -0
  19. package/dist/backup.d.ts +98 -0
  20. package/dist/backup.d.ts.map +1 -0
  21. package/dist/backup.js +153 -0
  22. package/dist/backup.js.map +1 -0
  23. package/dist/cache.d.ts +90 -0
  24. package/dist/cache.d.ts.map +1 -0
  25. package/dist/cache.js +121 -0
  26. package/dist/cache.js.map +1 -0
  27. package/dist/client.d.ts +406 -0
  28. package/dist/client.d.ts.map +1 -0
  29. package/dist/client.js +724 -0
  30. package/dist/client.js.map +1 -0
  31. package/dist/column-type.d.ts +33 -0
  32. package/dist/column-type.d.ts.map +1 -0
  33. package/dist/column-type.js +50 -0
  34. package/dist/column-type.js.map +1 -0
  35. package/dist/column-value.d.ts +77 -0
  36. package/dist/column-value.d.ts.map +1 -0
  37. package/dist/column-value.js +60 -0
  38. package/dist/column-value.js.map +1 -0
  39. package/dist/config.d.ts +181 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +315 -0
  42. package/dist/config.js.map +1 -0
  43. package/dist/connection-string.d.ts +45 -0
  44. package/dist/connection-string.d.ts.map +1 -0
  45. package/dist/connection-string.js +170 -0
  46. package/dist/connection-string.js.map +1 -0
  47. package/dist/endpoint-pool.d.ts +70 -0
  48. package/dist/endpoint-pool.d.ts.map +1 -0
  49. package/dist/endpoint-pool.js +120 -0
  50. package/dist/endpoint-pool.js.map +1 -0
  51. package/dist/error-codes.d.ts +44 -0
  52. package/dist/error-codes.d.ts.map +1 -0
  53. package/dist/error-codes.js +43 -0
  54. package/dist/error-codes.js.map +1 -0
  55. package/dist/errors.d.ts +24 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +33 -0
  58. package/dist/errors.js.map +1 -0
  59. package/dist/hlc.d.ts +19 -0
  60. package/dist/hlc.d.ts.map +1 -0
  61. package/dist/hlc.js +11 -0
  62. package/dist/hlc.js.map +1 -0
  63. package/dist/index.d.ts +55 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +51 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/json.d.ts +41 -0
  68. package/dist/json.d.ts.map +1 -0
  69. package/dist/json.js +345 -0
  70. package/dist/json.js.map +1 -0
  71. package/dist/object-id.d.ts +39 -0
  72. package/dist/object-id.d.ts.map +1 -0
  73. package/dist/object-id.js +99 -0
  74. package/dist/object-id.js.map +1 -0
  75. package/dist/options.d.ts +77 -0
  76. package/dist/options.d.ts.map +1 -0
  77. package/dist/options.js +68 -0
  78. package/dist/options.js.map +1 -0
  79. package/dist/prepared/binder.d.ts +20 -0
  80. package/dist/prepared/binder.d.ts.map +1 -0
  81. package/dist/prepared/binder.js +32 -0
  82. package/dist/prepared/binder.js.map +1 -0
  83. package/dist/prepared/policy.d.ts +81 -0
  84. package/dist/prepared/policy.d.ts.map +1 -0
  85. package/dist/prepared/policy.js +164 -0
  86. package/dist/prepared/policy.js.map +1 -0
  87. package/dist/query-stream.d.ts +55 -0
  88. package/dist/query-stream.d.ts.map +1 -0
  89. package/dist/query-stream.js +94 -0
  90. package/dist/query-stream.js.map +1 -0
  91. package/dist/result-set.d.ts +62 -0
  92. package/dist/result-set.d.ts.map +1 -0
  93. package/dist/result-set.js +100 -0
  94. package/dist/result-set.js.map +1 -0
  95. package/dist/retry.d.ts +38 -0
  96. package/dist/retry.d.ts.map +1 -0
  97. package/dist/retry.js +93 -0
  98. package/dist/retry.js.map +1 -0
  99. package/dist/routing/advice.d.ts +63 -0
  100. package/dist/routing/advice.d.ts.map +1 -0
  101. package/dist/routing/advice.js +61 -0
  102. package/dist/routing/advice.js.map +1 -0
  103. package/dist/routing/route-cache.d.ts +53 -0
  104. package/dist/routing/route-cache.d.ts.map +1 -0
  105. package/dist/routing/route-cache.js +123 -0
  106. package/dist/routing/route-cache.js.map +1 -0
  107. package/dist/routing/router.d.ts +53 -0
  108. package/dist/routing/router.d.ts.map +1 -0
  109. package/dist/routing/router.js +89 -0
  110. package/dist/routing/router.js.map +1 -0
  111. package/dist/row-source.d.ts +48 -0
  112. package/dist/row-source.d.ts.map +1 -0
  113. package/dist/row-source.js +35 -0
  114. package/dist/row-source.js.map +1 -0
  115. package/dist/runtime.d.ts +58 -0
  116. package/dist/runtime.d.ts.map +1 -0
  117. package/dist/runtime.js +137 -0
  118. package/dist/runtime.js.map +1 -0
  119. package/dist/shared-registry.d.ts +32 -0
  120. package/dist/shared-registry.d.ts.map +1 -0
  121. package/dist/shared-registry.js +48 -0
  122. package/dist/shared-registry.js.map +1 -0
  123. package/dist/sql-syntax.d.ts +44 -0
  124. package/dist/sql-syntax.d.ts.map +1 -0
  125. package/dist/sql-syntax.js +83 -0
  126. package/dist/sql-syntax.js.map +1 -0
  127. package/dist/statements.d.ts +15 -0
  128. package/dist/statements.d.ts.map +1 -0
  129. package/dist/statements.js +90 -0
  130. package/dist/statements.js.map +1 -0
  131. package/dist/transaction.d.ts +117 -0
  132. package/dist/transaction.d.ts.map +1 -0
  133. package/dist/transaction.js +204 -0
  134. package/dist/transaction.js.map +1 -0
  135. package/dist/transport/authenticating-transport.d.ts +63 -0
  136. package/dist/transport/authenticating-transport.d.ts.map +1 -0
  137. package/dist/transport/authenticating-transport.js +119 -0
  138. package/dist/transport/authenticating-transport.js.map +1 -0
  139. package/dist/transport/branch-statements.d.ts +39 -0
  140. package/dist/transport/branch-statements.d.ts.map +1 -0
  141. package/dist/transport/branch-statements.js +74 -0
  142. package/dist/transport/branch-statements.js.map +1 -0
  143. package/dist/transport/error-text.d.ts +26 -0
  144. package/dist/transport/error-text.d.ts.map +1 -0
  145. package/dist/transport/error-text.js +75 -0
  146. package/dist/transport/error-text.js.map +1 -0
  147. package/dist/transport/grpc/batcher.d.ts +127 -0
  148. package/dist/transport/grpc/batcher.d.ts.map +1 -0
  149. package/dist/transport/grpc/batcher.js +399 -0
  150. package/dist/transport/grpc/batcher.js.map +1 -0
  151. package/dist/transport/grpc/codec.d.ts +40 -0
  152. package/dist/transport/grpc/codec.d.ts.map +1 -0
  153. package/dist/transport/grpc/codec.js +231 -0
  154. package/dist/transport/grpc/codec.js.map +1 -0
  155. package/dist/transport/grpc/grpc-transport.d.ts +115 -0
  156. package/dist/transport/grpc/grpc-transport.d.ts.map +1 -0
  157. package/dist/transport/grpc/grpc-transport.js +577 -0
  158. package/dist/transport/grpc/grpc-transport.js.map +1 -0
  159. package/dist/transport/grpc/messages.d.ts +221 -0
  160. package/dist/transport/grpc/messages.d.ts.map +1 -0
  161. package/dist/transport/grpc/messages.js +56 -0
  162. package/dist/transport/grpc/messages.js.map +1 -0
  163. package/dist/transport/grpc/proto.d.ts +71 -0
  164. package/dist/transport/grpc/proto.d.ts.map +1 -0
  165. package/dist/transport/grpc/proto.js +67 -0
  166. package/dist/transport/grpc/proto.js.map +1 -0
  167. package/dist/transport/http.d.ts +62 -0
  168. package/dist/transport/http.d.ts.map +1 -0
  169. package/dist/transport/http.js +169 -0
  170. package/dist/transport/http.js.map +1 -0
  171. package/dist/transport/ndjson-row-source.d.ts +38 -0
  172. package/dist/transport/ndjson-row-source.d.ts.map +1 -0
  173. package/dist/transport/ndjson-row-source.js +197 -0
  174. package/dist/transport/ndjson-row-source.js.map +1 -0
  175. package/dist/transport/rest-decode.d.ts +33 -0
  176. package/dist/transport/rest-decode.d.ts.map +1 -0
  177. package/dist/transport/rest-decode.js +162 -0
  178. package/dist/transport/rest-decode.js.map +1 -0
  179. package/dist/transport/rest-prepared-cache.d.ts +50 -0
  180. package/dist/transport/rest-prepared-cache.d.ts.map +1 -0
  181. package/dist/transport/rest-prepared-cache.js +101 -0
  182. package/dist/transport/rest-prepared-cache.js.map +1 -0
  183. package/dist/transport/rest-transport.d.ts +88 -0
  184. package/dist/transport/rest-transport.d.ts.map +1 -0
  185. package/dist/transport/rest-transport.js +388 -0
  186. package/dist/transport/rest-transport.js.map +1 -0
  187. package/dist/transport/transport-pool.d.ts +40 -0
  188. package/dist/transport/transport-pool.d.ts.map +1 -0
  189. package/dist/transport/transport-pool.js +48 -0
  190. package/dist/transport/transport-pool.js.map +1 -0
  191. package/dist/transport/transport.d.ts +199 -0
  192. package/dist/transport/transport.d.ts.map +1 -0
  193. package/dist/transport/transport.js +11 -0
  194. package/dist/transport/transport.js.map +1 -0
  195. package/dist/values/decode.d.ts +39 -0
  196. package/dist/values/decode.d.ts.map +1 -0
  197. package/dist/values/decode.js +75 -0
  198. package/dist/values/decode.js.map +1 -0
  199. package/dist/values/encode.d.ts +45 -0
  200. package/dist/values/encode.d.ts.map +1 -0
  201. package/dist/values/encode.js +271 -0
  202. package/dist/values/encode.js.map +1 -0
  203. package/dist/values/ticks.d.ts +31 -0
  204. package/dist/values/ticks.d.ts.map +1 -0
  205. package/dist/values/ticks.js +52 -0
  206. package/dist/values/ticks.js.map +1 -0
  207. package/dist/values/typed.d.ts +67 -0
  208. package/dist/values/typed.d.ts.map +1 -0
  209. package/dist/values/typed.js +134 -0
  210. package/dist/values/typed.js.map +1 -0
  211. package/dist/values/uuid.d.ts +27 -0
  212. package/dist/values/uuid.d.ts.map +1 -0
  213. package/dist/values/uuid.js +76 -0
  214. package/dist/values/uuid.js.map +1 -0
  215. package/dist/vector.d.ts +26 -0
  216. package/dist/vector.d.ts.map +1 -0
  217. package/dist/vector.js +48 -0
  218. package/dist/vector.js.map +1 -0
  219. package/package.json +86 -0
  220. package/proto/camus_sql.proto +566 -0
package/README.md ADDED
@@ -0,0 +1,950 @@
1
+ # CamusDB Connector for Node.js
2
+
3
+ The TypeScript client library for [CamusDB](https://github.com/camusdb/camusdb).
4
+
5
+ ```shell
6
+ npm install camusdb
7
+ ```
8
+
9
+ The gRPC transport needs two more packages. Install them only if you use it:
10
+
11
+ ```shell
12
+ npm install @grpc/grpc-js @grpc/proto-loader
13
+ ```
14
+
15
+ **Requirements**: Node.js 20.11 or later. The package ships as ECMAScript modules with TypeScript
16
+ declarations.
17
+
18
+ ---
19
+
20
+ ## Contents
21
+
22
+ - [Quick start](#quick-start)
23
+ - [Configuration](#configuration)
24
+ - [Queries](#queries)
25
+ - [Parameters](#parameters)
26
+ - [Types](#types)
27
+ - [Writes](#writes)
28
+ - [Streaming](#streaming)
29
+ - [Transactions](#transactions)
30
+ - [Retries](#retries)
31
+ - [Prepared statements](#prepared-statements)
32
+ - [Authentication](#authentication)
33
+ - [Transport: REST and gRPC](#transport-rest-and-grpc)
34
+ - [Endpoint pools](#endpoint-pools)
35
+ - [Learned routing](#learned-routing)
36
+ - [Databases and branches](#databases-and-branches)
37
+ - [The query result cache](#the-query-result-cache)
38
+ - [Backups](#backups)
39
+ - [Errors](#errors)
40
+ - [Cancellation and deadlines](#cancellation-and-deadlines)
41
+ - [What a client shares](#what-a-client-shares)
42
+ - [Security notes](#security-notes)
43
+ - [Development](#development)
44
+
45
+ ---
46
+
47
+ ## Quick start
48
+
49
+ ```ts
50
+ import { CamusClient } from 'camusdb';
51
+
52
+ const client = new CamusClient({
53
+ endpoint: 'http://localhost:8082',
54
+ database: 'test',
55
+ });
56
+
57
+ await client.executeDdl(`
58
+ CREATE TABLE robots (
59
+ id ID PRIMARY KEY,
60
+ name STRING NOT NULL,
61
+ year INT64 NOT NULL
62
+ )
63
+ `);
64
+
65
+ await client.execute('INSERT INTO robots (id, name, year) VALUES (GEN_ID(), @name, @year)', {
66
+ name: 'r1',
67
+ year: 1974,
68
+ });
69
+
70
+ interface Robot {
71
+ id: string;
72
+ name: string;
73
+ year: number;
74
+ }
75
+
76
+ const { rows } = await client.query<Robot>('SELECT id, name, year FROM robots WHERE year = @year', {
77
+ year: 1974,
78
+ });
79
+
80
+ console.log(rows);
81
+ ```
82
+
83
+ A client is cheap to create and safe to share. It opens no socket of its own — see
84
+ [What a client shares](#what-a-client-shares).
85
+
86
+ ---
87
+
88
+ ## Configuration
89
+
90
+ Build a client from an options object, or from a connection string. The two forms are
91
+ interchangeable, and every option maps to one connection-string key.
92
+
93
+ ```ts
94
+ import { CamusClient } from 'camusdb';
95
+
96
+ // An options object.
97
+ const a = new CamusClient({ endpoint: 'http://localhost:8082', database: 'test' });
98
+
99
+ // A connection string.
100
+ const b = CamusClient.fromConnectionString('Endpoint=http://localhost:8082;Database=test');
101
+ ```
102
+
103
+ ### Options
104
+
105
+ | Option | Connection-string key | Required | Description |
106
+ | --- | --- | --- | --- |
107
+ | `endpoint` | `Endpoint` | Yes | The base URL, or several to rotate through. Pass an array, or a comma-separated list. |
108
+ | `database` | `Database` | Yes | The database name sent with every request. |
109
+ | `timeoutSeconds` | `Timeout` | No | The request timeout in seconds. Default `10`. |
110
+ | `protocol` | `Protocol` | No | `rest` (the default) or `grpc`. |
111
+ | `user` | `User` | No | The user to authenticate as. Key aliases: `UserId`, `Uid`, `Username`. |
112
+ | `password` | `Password` | No | That user's password. Key alias: `Pwd`. |
113
+ | `accessToken` | `AccessToken` | No | A bearer token obtained elsewhere, used as written. |
114
+ | `tokenLifetimeSeconds` | `TokenLifetime` | No | Fallback seconds to reuse a token when the server reports no expiry. Default `600`. |
115
+ | `maxAutoPrepare` | `MaxAutoPrepare` | No | How many statements to keep prepared. Default `128`. Zero turns it off. |
116
+ | `autoPrepareMinUsages` | `AutoPrepareMinUsages` | No | Executions of the same SQL before it is prepared. Default `2`. |
117
+ | `channelPoolSize` | `ChannelPoolSize` | No | gRPC only: streams per endpoint. Default `2`. |
118
+ | `coalescingThreshold` | `CoalescingThreshold` | No | gRPC only. Default `10`. `1` turns coalescing off. |
119
+ | `coalescingDelayMs` | `CoalescingDelay` | No | gRPC only: milliseconds. Default `0`, which is off. |
120
+ | `backupEndpoint` | `BackupEndpoint` | No | The HTTP endpoint for the backup admin API. Required with `grpc`. |
121
+ | `backupTimeoutSeconds` | `BackupTimeout` | No | The backup request timeout in seconds. Default `300`. |
122
+ | `allowInsecureCredentials` | `AllowInsecureCredentials` | No | Waives the refusal to send credentials to a remote plaintext endpoint. |
123
+ | `routingMode` | `RoutingMode` | No | `auto` (the default), `learned`, or `off`. |
124
+ | `routingNodes` | `RoutingNodes` | No | The trust map from node identities to endpoints. |
125
+ | `routingMaxHintAgeMs` | `RoutingMaxHintAge` | No | The ceiling on how long a learned route is reused. Default `5000`. |
126
+ | `defaultTransactionOptions` | `IsolationLevel`, `TransactionMode`, `Locking` | No | Concurrency defaults for every transaction and autocommit statement. |
127
+ | `int64` | `Int64` | No | `auto` (the default), `number`, or `bigint`. See [Types](#types). |
128
+
129
+ ### How a connection string is parsed
130
+
131
+ Keys are matched without regard to case and are trimmed, so `password=`, `Password=` and
132
+ ` Password ` all reach the same entry. A key that appears twice is an error rather than a silent
133
+ first-wins: the two spellings usually disagree, and the one that lost would disappear without a
134
+ diagnostic.
135
+
136
+ An unquoted value is trimmed and ends at the next `;`. Wrap a value in single or double quotes to
137
+ keep a semicolon or surrounding spaces in it, and double the quote character to include one:
138
+
139
+ ```ts
140
+ const client = CamusClient.fromConnectionString(
141
+ "Endpoint=https://db.example:5095;Database=test;User=app;Password='pa;ss''word'",
142
+ );
143
+ // The password is: pa;ss'word
144
+ ```
145
+
146
+ ### Logging a configuration
147
+
148
+ `client.toString()` masks every secret. So does `redactConnectionString`, which is a standalone
149
+ function for a caller that holds only the string:
150
+
151
+ ```ts
152
+ import { redactConnectionString } from 'camusdb';
153
+
154
+ redactConnectionString('Endpoint=https://db:5095;Database=test;User=app;Password=secret');
155
+ // 'Endpoint=https://db:5095;Database=test;User=app;Password=***'
156
+ ```
157
+
158
+ Never log a raw connection string. `Password`, `Pwd` and `AccessToken` are masked; everything else
159
+ is left as written, so the result still identifies the connection.
160
+
161
+ ---
162
+
163
+ ## Queries
164
+
165
+ ### `query`
166
+
167
+ Runs a `SELECT` and reads every row.
168
+
169
+ ```ts
170
+ const result = await client.query<Robot>('SELECT id, name FROM robots WHERE year > @year', {
171
+ year: 1970,
172
+ });
173
+
174
+ result.rows; // Robot[]
175
+ result.rowCount; // number
176
+ result.columns; // [{ name: 'id', type: 1, typeName: 'Id' }, …]
177
+ ```
178
+
179
+ The type argument states what you expect. Nothing on this side can check it against the server's
180
+ schema, so it is documentation and editor support, not validation. The driver maps each column to
181
+ the JavaScript value its **declared** type calls for.
182
+
183
+ `columns` is reported even for a result with no rows, because the server sends an authoritative
184
+ schema separately from the rows.
185
+
186
+ ### `queryOne` and `scalar`
187
+
188
+ ```ts
189
+ const robot = await client.queryOne<Robot>('SELECT * FROM robots WHERE id = @id', { id });
190
+ // Robot | undefined
191
+
192
+ const total = await client.scalar<number>('SELECT COUNT(*) FROM robots');
193
+ // number | undefined
194
+ ```
195
+
196
+ ### Duplicate column names
197
+
198
+ A join can project the same column name twice. The leftmost column keeps the name, and the ones
199
+ after it take `name_2`, `name_3`, and so on. No column is dropped.
200
+
201
+ ---
202
+
203
+ ## Parameters
204
+
205
+ Bind values as an object. A leading `@` is optional, so `{ year: 1974 }` and `{ '@year': 1974 }`
206
+ are the same parameter.
207
+
208
+ ```ts
209
+ await client.query('SELECT * FROM robots WHERE year = @year AND name = @name', {
210
+ year: 1974,
211
+ name: 'r1',
212
+ });
213
+ ```
214
+
215
+ Never build a statement by joining strings. A bound value never becomes SQL text, so it cannot
216
+ change what the statement means.
217
+
218
+ ### What the driver infers
219
+
220
+ | JavaScript value | Column type |
221
+ | --- | --- |
222
+ | `null`, `undefined` | `Null` |
223
+ | `boolean` | `Bool` |
224
+ | `number` that is an integer | `Integer64` |
225
+ | `number` that is not | `Float64` |
226
+ | `bigint` | `Integer64` |
227
+ | `string` | `String` |
228
+ | `Date` | `DateTime` |
229
+ | `Uint8Array`, `Buffer`, `ArrayBuffer` | `Bytes` |
230
+ | `Float32Array` | `Bytes`, packed as a vector |
231
+ | `CamusObjectId` | `Id` |
232
+ | an array | `Array`, with the element type read from the first element that is not null |
233
+
234
+ A UUID-shaped string is **not** read as a `uuid`. A 36-character string that happens to look like
235
+ one would otherwise change type silently.
236
+
237
+ ### Stating a type
238
+
239
+ Use a `camus.*` helper when inference cannot reach the type you need.
240
+
241
+ ```ts
242
+ import { camus, ColumnType } from 'camusdb';
243
+
244
+ await client.query('SELECT * FROM robots WHERE key = @key AND made = @made AND tags = @tags', {
245
+ key: camus.uuid('550e8400-e29b-41d4-a716-446655440000'),
246
+ made: camus.date(new Date('2024-03-15T00:00:00Z')),
247
+ tags: camus.array([], ColumnType.String),
248
+ });
249
+ ```
250
+
251
+ | Helper | Column type |
252
+ | --- | --- |
253
+ | `camus.id(value)` | `Id` |
254
+ | `camus.uuid(value)` | `Uuid` |
255
+ | `camus.int64(value)` | `Integer64` |
256
+ | `camus.float64(value)` | `Float64` |
257
+ | `camus.float32(value)` | `Float32` |
258
+ | `camus.bool(value)` | `Bool` |
259
+ | `camus.string(value)` | `String` |
260
+ | `camus.bytes(value)` | `Bytes` |
261
+ | `camus.vector(values)` | `Bytes`, packed as float32 elements |
262
+ | `camus.date(value)` | `Date`, truncated to UTC midnight |
263
+ | `camus.dateTime(value)` | `DateTime` |
264
+ | `camus.array(items, elementType)` | `Array` with a stated element type |
265
+ | `camus.null()` | `Null` |
266
+ | `camus.raw(columnValue)` | Whatever the value holds |
267
+
268
+ An empty array needs `camus.array`: there is no element to read a type from.
269
+
270
+ ---
271
+
272
+ ## Types
273
+
274
+ ### How a column reaches you
275
+
276
+ | Column type | JavaScript value |
277
+ | --- | --- |
278
+ | `Null` | `null` |
279
+ | `Id`, `String` | `string` |
280
+ | `Integer64` | `number` or `bigint` — see below |
281
+ | `Float64`, `Float32` | `number` |
282
+ | `Bool` | `boolean` |
283
+ | `Bytes` | `Uint8Array` |
284
+ | `Date`, `DateTime` | `Date`, in UTC |
285
+ | `Uuid` | `string`, canonical and lowercase |
286
+ | `Array` | an array of the values above |
287
+
288
+ ### 64-bit integers
289
+
290
+ CamusDB is a 64-bit database, and a JavaScript number stops being able to name every integer above
291
+ 2^53. The driver therefore keeps such a value exact — it never passes through a number on its way
292
+ in or out, in either transport.
293
+
294
+ `int64` chooses how the value reaches you:
295
+
296
+ - **`auto`** (the default) — a `number` inside the safe range, a `bigint` outside it. Every value
297
+ round-trips exactly, and ordinary counters stay numbers.
298
+ - **`number`** — always a `number`. A value above 2^53 loses precision. Choose it only when the
299
+ schema cannot hold one.
300
+ - **`bigint`** — always a `bigint`. Choose it when one uniform type matters more than convenience.
301
+
302
+ Set it for the whole client, or for one statement:
303
+
304
+ ```ts
305
+ const client = new CamusClient({ endpoint, database, int64: 'bigint' });
306
+
307
+ await client.query('SELECT n FROM counters', undefined, { int64: 'number' });
308
+ ```
309
+
310
+ ### Dates
311
+
312
+ A `Date` or `DateTime` column stores .NET ticks: 100-nanosecond units since 0001-01-01 UTC. A
313
+ JavaScript `Date` holds whole milliseconds, so reading one truncates sub-millisecond precision.
314
+ The loss is one-way and unavoidable. Convert the raw value yourself when you need the exact stored
315
+ instant:
316
+
317
+ ```ts
318
+ import { ticksToDate, dateToTicks } from 'camusdb';
319
+ ```
320
+
321
+ ### Vectors
322
+
323
+ An embedding is a `bytes` column holding tightly packed little-endian float32 elements. There is no
324
+ separate vector column type.
325
+
326
+ ```ts
327
+ import { CamusVector, camus } from 'camusdb';
328
+
329
+ await client.execute('INSERT INTO docs (id, embedding) VALUES (GEN_ID(), @embedding)', {
330
+ embedding: camus.vector([0.1, 0.2, 0.3]),
331
+ });
332
+
333
+ const row = await client.queryOne<{ embedding: Uint8Array }>('SELECT embedding FROM docs LIMIT 1');
334
+ const vector = CamusVector.toFloats(row!.embedding); // Float32Array
335
+ ```
336
+
337
+ A `Float32Array` bound directly is packed the same way.
338
+
339
+ ### ObjectIds
340
+
341
+ ```ts
342
+ import { CamusObjectId } from 'camusdb';
343
+
344
+ const id = CamusObjectId.generateAsString(); // 24 lowercase hexadecimal characters
345
+ ```
346
+
347
+ The layout mirrors the server's own generator, so an id minted here is indistinguishable from one
348
+ the server mints.
349
+
350
+ ---
351
+
352
+ ## Writes
353
+
354
+ ```ts
355
+ const { affectedRows } = await client.execute('DELETE FROM robots WHERE year < @year', {
356
+ year: 1970,
357
+ });
358
+ ```
359
+
360
+ `execute` also accepts a schema statement and sends it to the DDL route, where it reports zero
361
+ rows. Call `executeDdl` directly when you want that explicitly.
362
+
363
+ ### The typed row insert
364
+
365
+ The server has a row-level insert that names a table and a column-to-value map rather than SQL
366
+ text. Use it to avoid composing statement text on a hot write path. It performs exactly what
367
+ `INSERT INTO` performs, and takes the same authorization and error handling.
368
+
369
+ ```ts
370
+ await client.insert('robots', {
371
+ id: CamusObjectId.generateAsString(),
372
+ name: 'r1',
373
+ year: 1974,
374
+ });
375
+ ```
376
+
377
+ Column names carry no `@` here, because they bind to columns rather than to placeholders.
378
+
379
+ ### `TRUNCATE`
380
+
381
+ `TRUNCATE` commits a replicated schema entry that a rollback cannot undo, so the server refuses it
382
+ inside an explicit transaction. The driver refuses it before the round trip, with `CADB0538`.
383
+
384
+ ---
385
+
386
+ ## Streaming
387
+
388
+ `queryStream` pulls rows off the network as you advance, so a large result never fully materializes
389
+ on this side.
390
+
391
+ ```ts
392
+ await using stream = await client.queryStream<Robot>('SELECT * FROM robots');
393
+
394
+ console.log(stream.columns); // known before the first row
395
+
396
+ for await (const robot of stream) {
397
+ process(robot);
398
+ }
399
+ ```
400
+
401
+ The iteration owns the underlying response and releases it when it ends — whether it ran to the
402
+ last row, was left with `break`, or was ended by a throw. A stream that is never iterated must be
403
+ released with `await using`, or with `stream.close()`.
404
+
405
+ Two limits are worth knowing:
406
+
407
+ - **It is incremental over REST only.** The gRPC data plane multiplexes results over shared streams
408
+ that decode a whole result before returning, so gRPC buffers and replays. Your code is the same;
409
+ the memory saving is not there.
410
+ - **It gives up the transparent retry of a lost conflict.** Rows can reach you before the
411
+ statement's own short transaction commits, so a conflict that surfaces late is raised from the
412
+ iteration rather than retried. Use `query`, or drive an explicit transaction and retry it
413
+ yourself, when you need that.
414
+
415
+ ---
416
+
417
+ ## Transactions
418
+
419
+ ### `transaction`
420
+
421
+ The usual form. It commits when the work returns and rolls back when the work throws.
422
+
423
+ ```ts
424
+ await client.transaction(async (txn) => {
425
+ await client.execute('UPDATE accounts SET balance = balance - @amount WHERE id = @id',
426
+ { amount, id: from }, { transaction: txn });
427
+
428
+ await client.execute('UPDATE accounts SET balance = balance + @amount WHERE id = @id',
429
+ { amount, id: to }, { transaction: txn });
430
+ });
431
+ ```
432
+
433
+ A serializable conflict is retried: the whole function runs again on a fresh transaction, with an
434
+ exponential back-off. **So the function must be safe to run more than once.** It must not send an
435
+ email, charge a card, or change state outside the database on a path that could repeat.
436
+
437
+ ### `beginTransaction`
438
+
439
+ Use it when the unit of work does not fit in one function.
440
+
441
+ ```ts
442
+ await using txn = await client.beginTransaction();
443
+
444
+ await client.execute('UPDATE robots SET year = @year WHERE id = @id',
445
+ { year: 1984, id }, { transaction: txn });
446
+
447
+ await txn.commit();
448
+ ```
449
+
450
+ With `await using`, a transaction left without a commit is rolled back as the block unwinds.
451
+
452
+ ### Concurrency options
453
+
454
+ ```ts
455
+ import { CamusIsolationLevel, CamusLocking, CamusTransactionMode } from 'camusdb';
456
+
457
+ await client.transaction(work, {
458
+ transactionOptions: {
459
+ isolationLevel: CamusIsolationLevel.Serializable,
460
+ mode: CamusTransactionMode.ReadOnly,
461
+ locking: CamusLocking.Optimistic,
462
+ },
463
+ });
464
+ ```
465
+
466
+ - **`isolationLevel`** — `ReadCommitted` or `Serializable`.
467
+ - **`mode`** — `ReadWrite` or `ReadOnly`. `Serializable` plus `ReadOnly` is a lock-free consistent
468
+ snapshot, pinned to the instant it began.
469
+ - **`locking`** — `Pessimistic` (the default) takes locks up front. `Optimistic` stages the writes
470
+ and detects a conflict only at commit; a losing transaction fails its commit and must be retried.
471
+ It does not protect against phantoms.
472
+
473
+ Every knob is optional. A knob left unset falls back to the client's `defaultTransactionOptions`,
474
+ then to the connection-string defaults, then to the server default.
475
+
476
+ Prebuilt sets are exported: `OPTIMISTIC_TRANSACTION_OPTIONS` and `SNAPSHOT_TRANSACTION_OPTIONS`.
477
+
478
+ ### An unresolved commit
479
+
480
+ A commit can come back saying its outcome is not known yet (`CADB0509`). The transaction is not
481
+ dead, so the driver re-issues the **same** commit on the **same** handle, backing off up to ten
482
+ times. It never replays the work from the start, because that could apply an already-durable commit
483
+ a second time. This is handled inside the driver; you do not write it.
484
+
485
+ ---
486
+
487
+ ## Retries
488
+
489
+ `isRetryable` is the driver's own classification, and `withRetry` is the loop it uses:
490
+
491
+ ```ts
492
+ import { isRetryable, withRetry } from 'camusdb';
493
+
494
+ await withRetry(async () => {
495
+ await client.execute('UPDATE counters SET n = n + 1 WHERE id = @id', { id });
496
+ }, { maxAttempts: 5 });
497
+ ```
498
+
499
+ Use it around a unit of work that is safe to run twice. `client.transaction` already applies it.
500
+
501
+ `CADB0509` is deliberately **not** retryable here: replaying that work could double-apply a commit.
502
+
503
+ ---
504
+
505
+ ## Prepared statements
506
+
507
+ A prepared statement costs one extra round trip to register, and saves the SQL text and the
508
+ parameter names on every execution after it. The driver decides on its own:
509
+
510
+ - A statement is registered after `autoPrepareMinUsages` executions (default 2).
511
+ - The `maxAutoPrepare` most recently used registrations are kept (default 128).
512
+ - An evicted registration is released on the server.
513
+
514
+ You can skip the warm-up for a statement you already know is hot:
515
+
516
+ ```ts
517
+ await client.prepare('SELECT * FROM robots WHERE year = @year');
518
+
519
+ client.isPrepared('SELECT * FROM robots WHERE year = @year'); // true
520
+ client.preparedStatementCount; // 1
521
+ ```
522
+
523
+ Preparing is an optimization and never the reason a working statement fails. A registration that is
524
+ refused — schema statements, a full server-side cap, a server with no support at all — leaves the
525
+ statement running inline, and `prepare` reports no error for it.
526
+
527
+ Set `maxAutoPrepare: 0` to turn automatic preparation off and keep only explicit `prepare` calls.
528
+
529
+ ---
530
+
531
+ ## Authentication
532
+
533
+ CamusDB authentication is **off by default**. A configuration with no credentials sends no
534
+ `Authorization` header at all.
535
+
536
+ Against a server started with `CAMUSDB_AUTH_ENABLED=true`, add credentials:
537
+
538
+ ```ts
539
+ const client = new CamusClient({
540
+ endpoint: 'https://db.example:5095',
541
+ database: 'test',
542
+ user: 'app',
543
+ password: 'app-secret',
544
+ });
545
+
546
+ // Nothing else changes. The first statement authenticates on its own.
547
+ ```
548
+
549
+ ### Logging in at run time
550
+
551
+ Prefer this when the password comes from a secret manager rather than from configuration:
552
+
553
+ ```ts
554
+ await client.login('app', await secrets.get('camus-password'));
555
+
556
+ client.accessToken; // the minted token
557
+ await client.logout();
558
+ ```
559
+
560
+ `logout` revokes the token and keeps the credentials, so a later statement authenticates again. It
561
+ ends a session; it does not switch authentication off.
562
+
563
+ ### Using a token obtained elsewhere
564
+
565
+ ```ts
566
+ const client = new CamusClient({ endpoint, database, accessToken: token });
567
+ ```
568
+
569
+ Such a token is used as written and is never renewed — the driver has no password to mint a
570
+ replacement with — so a rejection reaches you rather than being retried.
571
+
572
+ ### How the token is managed
573
+
574
+ - **One login, not many.** A token is shared by every client presenting the same identity to the
575
+ same deployment. Password verification is expensive on the server and is rate limited per
576
+ account, so a login stampede is exactly what must be avoided.
577
+ - **Concurrent callers share one login.** Callers that find no usable token await the same login.
578
+ - **Renewal is proactive.** The server reports how long a token is good for, and the driver renews
579
+ at 80% of that. Against a server that reports none it falls back to `tokenLifetimeSeconds`.
580
+ - **A rejection is the backstop.** A `CADB0516` from a real request discards the token and replays
581
+ the statement once with a fresh one. That covers what a clock cannot predict: a rotated password,
582
+ a dropped user, a logout from elsewhere, a server restart.
583
+ - **A privilege refusal is never retried.** Authenticating again as the same user cannot grant a
584
+ privilege, so `CADB0517` reaches you at once.
585
+
586
+ ---
587
+
588
+ ## Transport: REST and gRPC
589
+
590
+ ```ts
591
+ const client = new CamusClient({
592
+ endpoint: 'http://localhost:5096', // the gRPC port
593
+ database: 'test',
594
+ protocol: 'grpc',
595
+ backupEndpoint: 'http://localhost:8082',
596
+ });
597
+ ```
598
+
599
+ Both transports carry the same features, and the client API does not change between them.
600
+
601
+ | | REST | gRPC |
602
+ | --- | --- | --- |
603
+ | Calls per statement | One HTTP request | Multiplexed over shared streams |
604
+ | Row-incremental streaming | Yes | No — it buffers and replays |
605
+ | Prepared statements | Node-local handles | Stream-scoped handles |
606
+ | Backup admin API | Yes | No — set `backupEndpoint` |
607
+ | Extra packages | None | `@grpc/grpc-js`, `@grpc/proto-loader` |
608
+
609
+ The gRPC transport does not make one call per statement. Statements ride a small pool of long-lived
610
+ `BatchExecute` duplex streams per endpoint, so many statements — and many concurrent transactions —
611
+ share one HTTP/2 connection. Statements that share a transaction go to one stream, in arrival
612
+ order, so the server sees them in the order you wrote them.
613
+
614
+ It also carries the session's causal token. Every reply reports a hybrid-logical-clock instant, the
615
+ transport keeps the greatest one it has seen, and every request carries it back. That is what makes
616
+ a read see this session's own earlier writes even when it lands on a different node.
617
+
618
+ Tune the stream pool with `channelPoolSize`. It is **not** a cap on in-flight transactions — many
619
+ transactions hash onto the same streams and interleave — so the default of 2 suits most workloads.
620
+ Raise it when many long-running queries per endpoint would otherwise queue behind each other.
621
+
622
+ The `.proto` file is the server's own, so the two surfaces cannot drift apart.
623
+
624
+ ---
625
+
626
+ ## Endpoint pools
627
+
628
+ Give several endpoints to rotate through them:
629
+
630
+ ```ts
631
+ const client = new CamusClient({
632
+ endpoint: ['http://a:8082', 'http://b:8082', 'http://c:8082'],
633
+ database: 'test',
634
+ });
635
+ ```
636
+
637
+ When a request fails because an endpoint is unreachable, that endpoint is set aside for 30 seconds
638
+ and skipped meanwhile. A node that is still down is set aside again by the next request that draws
639
+ it, at a cost of one failed request per period.
640
+
641
+ When every endpoint is set aside, the one closest to leaving is used anyway. The deployment is
642
+ evidently down, and letting the request fail against the real node reports why, where a synthetic
643
+ "no endpoints" would replace the server's diagnosis with the driver's bookkeeping.
644
+
645
+ ---
646
+
647
+ ## Learned routing
648
+
649
+ A CamusDB server can attach advisory routing metadata to a successful response: which node leads
650
+ the data behind that statement. With routing on, the driver learns from it and sends that
651
+ statement's future executions straight to that node, skipping a forwarding hop.
652
+
653
+ It is a latency optimization only. It changes no result, no isolation, and no commit behaviour, and
654
+ the server still executes correctly wherever a request lands.
655
+
656
+ ```ts
657
+ const client = new CamusClient({
658
+ endpoint: ['http://a:8082', 'http://b:8082', 'http://c:8082'],
659
+ database: 'test',
660
+ routingMode: 'learned',
661
+ routingNodes: {
662
+ 'camus-a:7070': 'http://a:8082',
663
+ 'camus-b:7070': 'http://b:8082',
664
+ 'camus-c:7070': 'http://c:8082',
665
+ },
666
+ });
667
+ ```
668
+
669
+ - **The trust map is the routing authority.** The server advertises an opaque node identity. It
670
+ becomes a destination only through `routingNodes`, and only when the mapped address is also a
671
+ member of the endpoint pool. The driver never dials an address the server supplied, so a response
672
+ can never steer traffic — or credentials — anywhere the operator did not list.
673
+ - **Modes.** `auto` (the default) negotiates only when the map names at least two distinct
674
+ endpoints, because one destination is not a routing decision and a single load-balancer URL is
675
+ not a set of routable nodes. So a configuration without `routingNodes` sends requests identical
676
+ to a driver that has no routing at all. `learned` negotiates with at least one identity mapped.
677
+ `off` never negotiates, and is the explicit switch.
678
+ - **What is learned.** Advice is cached per database, exact SQL text, and statement kind, with a
679
+ reuse period of the smaller of the server's own value and `routingMaxHintAgeMs`, measured on a
680
+ monotonic clock. A hint never generalizes from one SQL string to another, so parameterized and
681
+ prepared workloads are where it pays. A late reply cannot overwrite a newer route, and the cache
682
+ is bounded at 4096 entries and 4 MiB.
683
+ - **Precedence.** An explicit transaction stays pinned to the endpoint it started on: advice may
684
+ inform the next transaction, never relocate a live one. A route pointing at an endpoint that was
685
+ set aside falls back to rotation. Routing adds no retry of any kind.
686
+
687
+ ### Where a routed transaction starts
688
+
689
+ With routing on, `BEGIN` is deferred. `beginTransaction` sends nothing, and the transaction's first
690
+ statement starts it on that statement's learned endpoint — so a transaction whose first statement
691
+ touches a table runs where that table's leader is. Every later statement and the finalize stay
692
+ there.
693
+
694
+ Two consequences are visible, and only in routed mode:
695
+
696
+ - `txnIdPT`, `txnIdCounter` and `transactionId` read as zero until the first statement.
697
+ `isStarted` says which state the transaction is in.
698
+ - A failure to begin surfaces from that first statement, with the exception `beginTransaction`
699
+ would have raised.
700
+
701
+ A transaction that runs no statement is started by its own commit or rollback, so the server sees
702
+ the same pair it always did.
703
+
704
+ To choose the endpoint from a later, hotter statement instead, name it as an affinity. `BEGIN` is
705
+ then sent at once, to that statement's learned endpoint:
706
+
707
+ ```ts
708
+ const txn = await client.beginTransaction({ affinity: 'SELECT * FROM robots WHERE id = @id' });
709
+ ```
710
+
711
+ `client.learnedRouteCount` reports how many destinations are currently held, and
712
+ `result.routingAdvice` reports the advice one statement received. Both are diagnostic: the driver
713
+ has already applied the advice.
714
+
715
+ ---
716
+
717
+ ## Databases and branches
718
+
719
+ ```ts
720
+ await client.createDatabase('analytics', { ifNotExists: true });
721
+ await client.dropDatabase('analytics');
722
+
723
+ // A copy-on-write branch.
724
+ await client.createBranchDatabase('feature-x', 'production');
725
+
726
+ await client.showBranches('production'); // every transitive descendant
727
+ await client.showAncestors('feature-x'); // the chain up to the root
728
+
729
+ client.changeDatabase('analytics'); // sends nothing
730
+ ```
731
+
732
+ Concurrent creations can collide transiently while the server allocates the shared database id
733
+ sequence, which is what several environments provisioning in parallel produce. The driver retries
734
+ that. With `ifNotExists`, a creation that loses a registration race is treated as success: you
735
+ asked for the database to exist, and it does.
736
+
737
+ ---
738
+
739
+ ## The query result cache
740
+
741
+ A `SELECT` can carry a hint asking the server to cache its result. Put the hint **immediately after
742
+ the table reference**, and after its alias when it has one. It applies to the whole statement, not
743
+ only to the table it is attached to, and only one hint per statement is allowed.
744
+
745
+ ```ts
746
+ import { cacheHint } from 'camusdb';
747
+
748
+ const hint = cacheHint('recent_orders', { ttlMs: 30_000 });
749
+
750
+ const result = await client.query(`SELECT id, total FROM orders ${hint} WHERE status = @status`, {
751
+ status: 1,
752
+ });
753
+
754
+ result.cacheMetadata?.status; // 'hit' | 'miss' | 'bypass' | 'stale-revalidated' | …
755
+ result.cacheMetadata?.isHit;
756
+ result.cacheMetadata?.ageMs;
757
+ result.cacheMetadata?.cachedAtHlc;
758
+ ```
759
+
760
+ `cacheMetadata` is `undefined` when the statement carried no hint. A hinted statement that skipped
761
+ the cache reports `bypass` with a `bypassReason`.
762
+
763
+ Options go inside the braces. `cacheHint` writes them for you:
764
+
765
+ ```ts
766
+ cacheHint('hot_orders'); // {cache=hot_orders}
767
+ cacheHint('hot_orders', { ttlMs: 30_000 }); // {cache=hot_orders, ttl=30000}
768
+ cacheHint('hot_orders', { ttlMs: 5_000, strict: true }); // {cache=hot_orders, ttl=5000, strict}
769
+ ```
770
+
771
+ `strict` makes the server validate each hit against live storage, for a reader that must not see
772
+ staleness between nodes.
773
+
774
+ Drop cached results:
775
+
776
+ ```ts
777
+ await client.evictCache('recent_orders');
778
+ await client.evictAllCache(); // this database only
779
+ ```
780
+
781
+ **A family name in a hint is a bare identifier**: a letter or an underscore, then letters, digits,
782
+ or underscores. The hint writes it unquoted, so a hyphen, a dot or a colon is a parse error on the
783
+ server — `cacheHint` refuses those, where the name was written, rather than send a statement that
784
+ fails. `evictCache` accepts more, because `EVICT CACHE` takes a quoted string, so a family minted
785
+ elsewhere can still be evicted.
786
+
787
+ ---
788
+
789
+ ## Backups
790
+
791
+ The node's online backup administration API. It is server-level and node-wide, not scoped to this
792
+ client's database — every database on the server shares one storage node, so a backup captures all
793
+ of them. It needs a superuser token when authentication is on.
794
+
795
+ ```ts
796
+ const full = await client.backups.takeFullBackup();
797
+ const incremental = await client.backups.takeIncrementalBackup(full.backupId);
798
+ const coordinated = await client.backups.takeCoordinatedBackup();
799
+
800
+ const catalog = await client.backups.listBackups();
801
+ const chain = await client.backups.getChain(incremental.backupId);
802
+
803
+ const preview = await client.backups.previewGarbageCollection();
804
+ const run = await client.backups.collectGarbage();
805
+ ```
806
+
807
+ Check `chain` before you rely on a leaf backup: it lists every link a restore needs, and each entry
808
+ reports `isInvalid` and `invalidReason`. `wasSubstituted` says the server took a different kind of
809
+ backup than the one requested, and `substitutionReason` says why.
810
+
811
+ Restore is an offline operator procedure and is deliberately not reachable here.
812
+
813
+ The backup routes are REST and JSON only. A gRPC client must set `backupEndpoint` to the server's
814
+ HTTP endpoint; the driver refuses to guess rather than send HTTP at a gRPC port.
815
+
816
+ ---
817
+
818
+ ## Errors
819
+
820
+ Every database-level failure is a `CamusError` carrying the server's `CADBxxxx` code.
821
+
822
+ ```ts
823
+ import { CamusError, CamusErrorCode } from 'camusdb';
824
+
825
+ try {
826
+ await client.execute('INSERT INTO robots (id) VALUES (@id)', { id });
827
+ } catch (error) {
828
+ if (CamusError.is(error) && error.code === CamusErrorCode.DatabaseAlreadyExists) {
829
+ // …
830
+ }
831
+
832
+ throw error;
833
+ }
834
+ ```
835
+
836
+ Switch on `code`. The message is human text and is not a stable contract. `CADB0000` is the generic
837
+ code the driver uses when the far end supplied none.
838
+
839
+ The codes the driver itself reacts to are named in `CamusErrorCode`; every other code reaches you
840
+ unchanged.
841
+
842
+ Every message the far end supplied is cleaned before it becomes an error: credential-shaped runs
843
+ are masked, control characters are folded to spaces, and the length is bounded. Applications log
844
+ these messages verbatim, and none of that text is the driver's.
845
+
846
+ ---
847
+
848
+ ## Cancellation and deadlines
849
+
850
+ Every method that reaches the server accepts an `AbortSignal`, and most accept a per-statement
851
+ timeout.
852
+
853
+ ```ts
854
+ const controller = new AbortController();
855
+ setTimeout(() => controller.abort(), 5_000);
856
+
857
+ await client.query('SELECT * FROM robots', undefined, {
858
+ signal: controller.signal,
859
+ timeoutSeconds: 30,
860
+ });
861
+ ```
862
+
863
+ A cancellation you asked for is re-thrown as it is. It is not a database failure and the node is
864
+ not marked unreachable.
865
+
866
+ ---
867
+
868
+ ## What a client shares
869
+
870
+ A client holds no socket of its own. Four kinds of state are shared process-wide, keyed by what
871
+ they actually depend on:
872
+
873
+ | State | Shared by |
874
+ | --- | --- |
875
+ | The endpoint rotation and its health | The endpoint list |
876
+ | The bearer token | The identity and the deployment |
877
+ | The transport, with its prepared-statement registrations | The identity, the deployment, and the stream tuning |
878
+ | The learned routes | The deployment and the routing configuration |
879
+
880
+ Each is a property of a deployment, not of one client object. So building a client per request
881
+ costs almost nothing and still reuses the connections, the token, and the prepared statements a
882
+ long-lived one would. An application that built one per request would otherwise start every one of
883
+ them cold — a multi-endpoint deployment would send all its traffic to one node, and every request
884
+ would perform its own login against a per-account rate limit.
885
+
886
+ `client.close()` releases what a client owns on its own, which today is nothing. To release the
887
+ shared gRPC channels when a process is shutting down:
888
+
889
+ ```ts
890
+ import { CamusTransportPool } from 'camusdb';
891
+
892
+ await CamusTransportPool.closeAll();
893
+ ```
894
+
895
+ ---
896
+
897
+ ## Security notes
898
+
899
+ - **Credentials are refused over a remote plaintext endpoint.** A password is posted to `/login`
900
+ and a bearer token rides every later request; on an `http://` endpoint both cross the network in
901
+ the clear. The server's own check fires only after the password has already been sent, so the
902
+ driver refuses first, before anything leaves the process. Loopback is allowed, because that
903
+ traffic never reaches a network. Set `allowInsecureCredentials` for a deployment reached over a
904
+ private link that terminates TLS elsewhere.
905
+ - **A response can never steer traffic.** Routing advice names an opaque node identity, and it
906
+ becomes a destination only through the operator's own `routingNodes` map, and only when the
907
+ mapped address is already in the endpoint pool.
908
+ - **The driver never follows a redirect.** A redirected statement would leave the configured
909
+ endpoint and take its bearer token with it.
910
+ - **Error text is sanitized.** See [Errors](#errors).
911
+ - **A token key is hashed and salted.** The map key that shares a token holds no plaintext
912
+ password, and cannot be tested against a guessed one outside the process.
913
+
914
+ ---
915
+
916
+ ## Development
917
+
918
+ ```shell
919
+ npm install
920
+ npm run build # compile to dist/
921
+ npm test # the offline suite
922
+ npm run test:coverage
923
+ npm run test:live # the suite that needs a running server
924
+ npm run lint
925
+ npm run typecheck
926
+ npm run format
927
+ ```
928
+
929
+ The offline suite needs no CamusDB installation. It runs against a real HTTP server on a loopback
930
+ port rather than a stubbed `fetch`, because the REST path depends on streams, deadlines, and abort
931
+ signals that a stub does not reproduce.
932
+
933
+ The live suite runs the whole surface against a real server, over both transports. It reads its
934
+ endpoints from the environment:
935
+
936
+ | Variable | Default |
937
+ | --- | --- |
938
+ | `CAMUS_LIVE_ENDPOINT` | `http://localhost:5095` |
939
+ | `CAMUS_LIVE_GRPC_ENDPOINT` | `http://localhost:5096` |
940
+ | `CAMUS_LIVE_DATABASE` | `camusdb_ts_live` |
941
+ | `CAMUS_LIVE_USER`, `CAMUS_LIVE_PASSWORD` | none, for a server with authentication off |
942
+
943
+ Each case creates its own table and drops it afterwards, so a run leaves nothing behind and two
944
+ runs never collide.
945
+
946
+ ---
947
+
948
+ ## License
949
+
950
+ MIT. See [LICENSE](LICENSE).