@tablecraft-app/plugin-api 1.5.0 → 1.7.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/dist/errors.d.ts +1 -1
- package/dist/errors.js +5 -0
- package/dist/explain.d.ts +8 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.js +17 -0
- package/dist/plugin.d.ts +41 -1
- package/dist/plugin.js +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.js +108 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Failures the host tells apart; anything else a plugin throws is reported as `statementFailed`. */
|
|
2
|
-
export declare const PLUGIN_ERROR_CODES: readonly ["cancelled", "connectionFailed", "statementFailed", "introspectionFailed", "definitionNotFound", "explainUnsupported", "connectionLost", "sessionBusy", "conflict"];
|
|
2
|
+
export declare const PLUGIN_ERROR_CODES: readonly ["cancelled", "connectionFailed", "statementFailed", "introspectionFailed", "definitionNotFound", "explainUnsupported", "connectionLost", "sessionBusy", "conflict", "timedOut"];
|
|
3
3
|
export type PluginErrorCode = (typeof PLUGIN_ERROR_CODES)[number];
|
|
4
4
|
/**
|
|
5
5
|
* Where the database located a failure, relative to the statement text the plugin received:
|
package/dist/errors.js
CHANGED
|
@@ -23,6 +23,11 @@ export const PLUGIN_ERROR_CODES = [
|
|
|
23
23
|
* nothing (decision 115); the host offers to reload or to overwrite.
|
|
24
24
|
*/
|
|
25
25
|
"conflict",
|
|
26
|
+
/**
|
|
27
|
+
* The server stopped the statement at the limit `ExecuteOptions.serverTimeoutMs` set (decision
|
|
28
|
+
* 129); the message names the server's setting.
|
|
29
|
+
*/
|
|
30
|
+
"timedOut",
|
|
26
31
|
];
|
|
27
32
|
/** What a plugin throws when the host should react to the kind of failure, not just show it. */
|
|
28
33
|
export class PluginError extends Error {
|
package/dist/explain.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface PlanNode {
|
|
|
26
26
|
/** Everything else the dialect reports about the step, in its order. */
|
|
27
27
|
readonly properties: readonly PlanProperty[];
|
|
28
28
|
readonly children: readonly PlanNode[];
|
|
29
|
+
/**
|
|
30
|
+
* What a reviewer should notice about the step, one sentence each, such as a full table scan or
|
|
31
|
+
* estimates made without statistics (decision 130). The host marks the step and lists each
|
|
32
|
+
* distinct sentence once. Absent or empty when there is nothing to say.
|
|
33
|
+
*/
|
|
34
|
+
readonly warnings?: readonly string[];
|
|
29
35
|
}
|
|
30
36
|
/** What `Session.explain` answers. */
|
|
31
37
|
export interface ExplainedPlan {
|
|
@@ -44,6 +50,8 @@ export interface ExplainedPlan {
|
|
|
44
50
|
export interface ExplainOptions {
|
|
45
51
|
/** Run the statement and report what it did (Explain Analyse), not only how it would run. */
|
|
46
52
|
readonly analyze: boolean;
|
|
53
|
+
/** `ExecuteOptions.serverTimeoutMs` for the statement `explain` runs (decision 129). */
|
|
54
|
+
readonly serverTimeoutMs?: number;
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* Whether a statement can be explained, decided without a database. `writes` says the statement
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,9 @@ export { ConflictError, DefinitionNotFoundError, type ErrorPosition, errorPositi
|
|
|
5
5
|
export { type ExplainedPlan, type Explainability, type ExplainOptions, type PlanNode, type PlanProperty, } from "./explain.js";
|
|
6
6
|
export { KEY_TYPES, type KeyApplied, type KeyApplyOptions, type KeyBytes, type KeyChange, type KeyContents, type KeyDatabase, type KeyDetails, type KeyExpiry, type KeyInfo, type KeyName, type KeyReadRequest, type KeyScan, type KeyScanRequest, type KeySource, type Keyspace, type KeyType, } from "./keyspace.js";
|
|
7
7
|
export { OBJECT_KINDS, type CatalogObject, type CheckDesign, type ColumnDesign, type DdlContext, type DdlScript, type ForeignKeyDesign, type IndexDesign, type ObjectChange, type ObjectChangeKind, type ObjectEditor, type ObjectFeatures, type ObjectKind, type TableDesign, } from "./objects.js";
|
|
8
|
-
export { CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, type ConnectionField, type ConnectionFieldGroup, type ConnectionFieldKind, type ConnectionUrl, type ConnectionUrlForm, type ConnectionUrlPart, type NetworkSpec, type PluginManifest, type TlsFieldRole, type TlsSpec, } from "./manifest.js";
|
|
8
|
+
export { BRAND_ID, brandSchema, CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, type Brand, type ConnectionField, type ConnectionFieldGroup, type ConnectionFieldKind, type ConnectionUrl, type ConnectionUrlForm, type ConnectionUrlPart, type NetworkSpec, type PluginManifest, type TlsFieldRole, type TlsSpec, } from "./manifest.js";
|
|
9
9
|
export { ConnectionUrlError, formatConnectionUrl, parseConnectionUrl, type ConnectionForm, type ConnectionUrlSpec, type ConnectionValue, type ParsedConnectionUrl, } from "./url.js";
|
|
10
|
-
export { type BoundStatement, CAPABILITIES, type Capability, type Column, type ColumnOrigin, type CommandLanguage, type CommandReply, type ConnectOptions, type DatasourcePlugin, type ExecuteOptions, type ExecuteResult, type FormatOptions, FORMAT_STYLES, type FormatStyle, GENERATED_STATEMENTS, type GeneratedStatement, IMPORT_KINDS, type ImportKind, type InferredColumn, type InsertRow, type InsertTarget, KEYWORD_CASES, type NamespaceSwitch, NODE_ALL_PRIMARIES, NODE_AUTO, NODE_ROLES, type NodeRole, type ServerInfo, type ServerInfoSection, type ServerNode, type SlowCommand, switchableTo, type NewColumn, type KeywordCase, PARAMETER_STYLES, type ParameterStyle, type ParameterValue, type RoutineArgument, type RoutineCall, type Row, type SavepointStatements, type SelectSource, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, type ServerMessage, type ServerMessageLevel, type SqlParameter, type Session, type SqlLanguage, STATEMENT_KINDS, type StatementClass, type StatementKind, type StatementSpan, type StatementStream, type StreamedStatement, type TlsInfo, TRANSACTION_MODES, type TransactionMode, type Transactions, } from "./plugin.js";
|
|
10
|
+
export { type BoundStatement, CAPABILITIES, type Capability, type Column, type ColumnOrigin, type CommandLanguage, type CommandReply, type ConnectOptions, type DatasourcePlugin, type ExecuteOptions, type ExecuteResult, type FormatOptions, FORMAT_STYLES, type FormatStyle, GENERATED_STATEMENTS, type GeneratedStatement, IMPORT_KINDS, type ImportKind, type InferredColumn, type InsertRow, type InsertTarget, KEYWORD_CASES, type NamespaceSwitch, NODE_ALL_PRIMARIES, NODE_AUTO, NODE_ROLES, type NodeRole, type ServerInfo, type ServerInfoSection, type ServerNode, type ServerTimeout, type CancelReach, type SlowCommand, switchableTo, type NewColumn, type KeywordCase, PARAMETER_STYLES, type ParameterStyle, type ParameterValue, type RoutineArgument, type RoutineCall, type Row, type SavepointStatements, type SelectSource, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, type ServerMessage, type ServerMessageLevel, type SqlParameter, type Session, type SqlLanguage, STATEMENT_KINDS, type StatementClass, type StatementKind, type StatementSpan, type StatementStream, type StreamedStatement, type TlsInfo, TRANSACTION_MODES, type TransactionMode, type Transactions, } from "./plugin.js";
|
|
11
11
|
export { SERVER_SESSION_STATES, type ServerSession, type ServerSessionState, type SessionListing, type SessionMonitor, type SessionTarget, } from "./sessions.js";
|
|
12
12
|
export { TLS_CHAIN_CODES, TLS_HOST_CODE, homeExpanded, tlsFailureMessage, tlsFileMessage, tlsKeyIsEncrypted, tlsKeyMessage, type TlsVerification, } from "./tls.js";
|
|
13
13
|
export { BYTES_PREVIEW_LENGTH, toBytesValue, toWireInteger, toWireValue, type WireValueOptions, type BigIntValue, type BytesValue, type DateValue, type JsonValue, type TypeClass, type WireValue, } from "./values.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { ConflictError, DefinitionNotFoundError, errorPositionOf, isPluginError,
|
|
|
5
5
|
export {} from "./explain.js";
|
|
6
6
|
export { KEY_TYPES, } from "./keyspace.js";
|
|
7
7
|
export { OBJECT_KINDS, } from "./objects.js";
|
|
8
|
-
export { CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, } from "./manifest.js";
|
|
8
|
+
export { BRAND_ID, brandSchema, CONNECTION_FIELD_GROUPS, CONNECTION_FIELD_KINDS, CONNECTION_URL_FORMS, CONNECTION_URL_PARTS, connectionFieldSchema, connectionUrlSchema, MANIFEST_FILE, networkSpecSchema, pluginManifestSchema, TLS_FIELD_ROLES, tlsSpecSchema, } from "./manifest.js";
|
|
9
9
|
export { ConnectionUrlError, formatConnectionUrl, parseConnectionUrl, } from "./url.js";
|
|
10
10
|
export { CAPABILITIES, FORMAT_STYLES, GENERATED_STATEMENTS, IMPORT_KINDS, KEYWORD_CASES, NODE_ALL_PRIMARIES, NODE_AUTO, NODE_ROLES, switchableTo, PARAMETER_STYLES, omittedMessages, SERVER_MESSAGE_LEVELS, SERVER_MESSAGE_LIMIT, STATEMENT_KINDS, TRANSACTION_MODES, } from "./plugin.js";
|
|
11
11
|
export { SERVER_SESSION_STATES, } from "./sessions.js";
|
package/dist/manifest.d.ts
CHANGED
|
@@ -106,6 +106,14 @@ export type TlsSpec = z.infer<typeof tlsSpecSchema>;
|
|
|
106
106
|
/** The roles of `TlsSpec` that name a field, in the order the dialog draws them. */
|
|
107
107
|
export declare const TLS_FIELD_ROLES: readonly ["mode", "caFile", "certFile", "keyFile", "keyPassphrase"];
|
|
108
108
|
export type TlsFieldRole = (typeof TLS_FIELD_ROLES)[number];
|
|
109
|
+
/** A simple-icons slug, the one public catalogue of database product ids (decision 128). */
|
|
110
|
+
export declare const BRAND_ID: RegExp;
|
|
111
|
+
/** A database product a plugin speaks; `id` is a simple-icons slug, `name` what a row shows. */
|
|
112
|
+
export declare const brandSchema: z.ZodObject<{
|
|
113
|
+
id: z.ZodString;
|
|
114
|
+
name: z.ZodString;
|
|
115
|
+
}, z.core.$strict>;
|
|
116
|
+
export type Brand = z.infer<typeof brandSchema>;
|
|
109
117
|
export declare const pluginManifestSchema: z.ZodObject<{
|
|
110
118
|
id: z.ZodString;
|
|
111
119
|
name: z.ZodString;
|
|
@@ -131,8 +139,13 @@ export declare const pluginManifestSchema: z.ZodObject<{
|
|
|
131
139
|
keyValue: "keyValue";
|
|
132
140
|
nodes: "nodes";
|
|
133
141
|
serverInfo: "serverInfo";
|
|
142
|
+
serverTimeout: "serverTimeout";
|
|
134
143
|
}>>;
|
|
135
144
|
highlight: z.ZodString;
|
|
145
|
+
brands: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
146
|
+
id: z.ZodString;
|
|
147
|
+
name: z.ZodString;
|
|
148
|
+
}, z.core.$strict>>>;
|
|
136
149
|
connection: z.ZodObject<{
|
|
137
150
|
fields: z.ZodArray<z.ZodObject<{
|
|
138
151
|
key: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -157,6 +157,15 @@ function urlSetsDeclared(connection) {
|
|
|
157
157
|
return Object.entries(url.sets).every(([scheme, values]) => url.schemes.includes(scheme) &&
|
|
158
158
|
Object.keys(values).every((key) => connection.fields.some((field) => field.key === key)));
|
|
159
159
|
}
|
|
160
|
+
/** A simple-icons slug, the one public catalogue of database product ids (decision 128). */
|
|
161
|
+
export const BRAND_ID = /^[a-z0-9_]+$/;
|
|
162
|
+
/** A database product a plugin speaks; `id` is a simple-icons slug, `name` what a row shows. */
|
|
163
|
+
export const brandSchema = z.strictObject({
|
|
164
|
+
id: z
|
|
165
|
+
.string()
|
|
166
|
+
.regex(BRAND_ID, { message: "expected a simple-icons slug: lower-case letters, digits and underscores" }),
|
|
167
|
+
name: z.string().min(1),
|
|
168
|
+
});
|
|
160
169
|
function includesAny(capabilities, ...wanted) {
|
|
161
170
|
return wanted.some((capability) => capabilities.includes(capability));
|
|
162
171
|
}
|
|
@@ -171,6 +180,14 @@ export const pluginManifestSchema = z
|
|
|
171
180
|
capabilities: z.array(z.enum(CAPABILITIES)),
|
|
172
181
|
/** The highlighter the console uses for this plugin's dialect (decision 008). */
|
|
173
182
|
highlight: z.string().min(1),
|
|
183
|
+
/** The database products the plugin speaks, its default first (decision 128). Absent: rows name the plugin. */
|
|
184
|
+
brands: z
|
|
185
|
+
.array(brandSchema)
|
|
186
|
+
.min(1)
|
|
187
|
+
.refine((brands) => new Set(brands.map((brand) => brand.id)).size === brands.length, {
|
|
188
|
+
message: "brand ids must be unique",
|
|
189
|
+
})
|
|
190
|
+
.optional(),
|
|
174
191
|
/** The connection form, readable without running plugin code (decision 019). */
|
|
175
192
|
connection: z
|
|
176
193
|
.strictObject({
|
package/dist/plugin.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { TypeClass, WireValue } from "./values.js";
|
|
|
11
11
|
* What a plugin can do. The UI branches on these, never on a plugin id (decision 007), and a
|
|
12
12
|
* capability without contract-suite coverage cannot be declared (decision 013).
|
|
13
13
|
*/
|
|
14
|
-
export declare const CAPABILITIES: readonly ["sql", "cancel", "schemaTree", "transactions", "ddl", "explain", "explainAnalyze", "currentNamespace", "keepAlive", "bulkInsert", "objects", "serverMessages", "sessions", "routineCall", "commands", "keyValue", "nodes", "serverInfo"];
|
|
14
|
+
export declare const CAPABILITIES: readonly ["sql", "cancel", "schemaTree", "transactions", "ddl", "explain", "explainAnalyze", "currentNamespace", "keepAlive", "bulkInsert", "objects", "serverMessages", "sessions", "routineCall", "commands", "keyValue", "nodes", "serverInfo", "serverTimeout"];
|
|
15
15
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
16
16
|
/** Statements `SqlLanguage.generate` writes from a catalog relation (plan 010). */
|
|
17
17
|
export declare const GENERATED_STATEMENTS: readonly ["drop", "select", "insert", "update"];
|
|
@@ -128,6 +128,36 @@ export interface ExecuteOptions {
|
|
|
128
128
|
* many times and answers with the last run; any other plugin never receives it.
|
|
129
129
|
*/
|
|
130
130
|
readonly repeat?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Have the server itself stop the statement after this many milliseconds, at least 1 (decision
|
|
133
|
+
* 129), through its own setting, never above a limit the server already had. The host sends it
|
|
134
|
+
* only to a session whose `serverTimeout` is not null. Absent: the plugin puts back the
|
|
135
|
+
* server's own value. A statement the server stops rejects with `timedOut`.
|
|
136
|
+
*/
|
|
137
|
+
readonly serverTimeoutMs?: number;
|
|
138
|
+
}
|
|
139
|
+
/** What the connected server stops by itself once `ExecuteOptions.serverTimeoutMs` passes (decision 129). */
|
|
140
|
+
export interface ServerTimeout {
|
|
141
|
+
/** The server's own name for it, for messages: `max_execution_time`, `statement_timeout`. */
|
|
142
|
+
readonly setting: string;
|
|
143
|
+
/** `reads`: only `SELECT` (and `SELECT … FOR UPDATE`); `all`: every statement. */
|
|
144
|
+
readonly covers: "reads" | "all";
|
|
145
|
+
/**
|
|
146
|
+
* `statement`: from the start until the last row is sent, so a result read slowly is cut too;
|
|
147
|
+
* `serverWork`: only while the server works on it.
|
|
148
|
+
*/
|
|
149
|
+
readonly counts: "statement" | "serverWork";
|
|
150
|
+
}
|
|
151
|
+
/** How `cancel` stops a running statement on this server (decision 131). */
|
|
152
|
+
export interface CancelReach {
|
|
153
|
+
/** What the plugin sends, for people: `KILL QUERY`, `pg_cancel_backend`, `CLIENT UNBLOCK`. */
|
|
154
|
+
readonly method: string;
|
|
155
|
+
/**
|
|
156
|
+
* `statement`: the server stops the work. `connection`: the plugin drops its connection and the
|
|
157
|
+
* server may keep running the statement. `blockingCommands`: only commands waiting on the server
|
|
158
|
+
* (Redis `BLPOP`) are stopped; the rest run to their end.
|
|
159
|
+
*/
|
|
160
|
+
readonly reaches: "statement" | "connection" | "blockingCommands";
|
|
131
161
|
}
|
|
132
162
|
/** `ExecuteOptions.node`: the plugin routes the command by its keys (decision 114). */
|
|
133
163
|
export declare const NODE_AUTO = "auto";
|
|
@@ -275,6 +305,8 @@ export interface Session {
|
|
|
275
305
|
close(): Promise<void>;
|
|
276
306
|
/** What the server reports about itself, such as `17.4`; null when it reports nothing. */
|
|
277
307
|
readonly serverVersion: string | null;
|
|
308
|
+
/** The manifest `brands` id of the product this session reached; absent when the plugin declares no brands or cannot tell. */
|
|
309
|
+
readonly serverBrand?: string;
|
|
278
310
|
/**
|
|
279
311
|
* True when another session on the same configuration sees a different database, as with
|
|
280
312
|
* SQLite's `:memory:`; the engine then never re-runs this session's statements elsewhere.
|
|
@@ -285,6 +317,14 @@ export interface Session {
|
|
|
285
317
|
* (decision 072).
|
|
286
318
|
*/
|
|
287
319
|
readonly tls?: TlsInfo | null;
|
|
320
|
+
/**
|
|
321
|
+
* Present exactly when the plugin declares `serverTimeout` (decision 129): what this server stops
|
|
322
|
+
* by itself under `ExecuteOptions.serverTimeoutMs`, read once at connect; null when this server
|
|
323
|
+
* version has no such setting.
|
|
324
|
+
*/
|
|
325
|
+
readonly serverTimeout?: ServerTimeout | null;
|
|
326
|
+
/** Optional, only with `cancel`: how this session's cancel reaches the server, read once at connect. */
|
|
327
|
+
readonly cancelReach?: CancelReach;
|
|
288
328
|
}
|
|
289
329
|
/** What a TLS handshake agreed, as Node's socket names it: `TLSv1.3`, `TLS_AES_256_GCM_SHA384`. */
|
|
290
330
|
export interface TlsInfo {
|
package/dist/plugin.js
CHANGED
|
@@ -21,6 +21,7 @@ export const CAPABILITIES = [
|
|
|
21
21
|
"keyValue",
|
|
22
22
|
"nodes",
|
|
23
23
|
"serverInfo",
|
|
24
|
+
"serverTimeout",
|
|
24
25
|
];
|
|
25
26
|
/** Statements `SqlLanguage.generate` writes from a catalog relation (plan 010). */
|
|
26
27
|
export const GENERATED_STATEMENTS = ["drop", "select", "insert", "update"];
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -9,6 +9,18 @@ export interface ContractFixture {
|
|
|
9
9
|
readonly statements?: {
|
|
10
10
|
/** A statement that runs for `seconds`; required when the plugin declares `cancel`. */
|
|
11
11
|
readonly sleep?: (seconds: number) => string;
|
|
12
|
+
/**
|
|
13
|
+
* A read that runs at least `seconds` on an idle server and then ends by itself, which the
|
|
14
|
+
* server's own limit stops with an error; required when a session reports a `serverTimeout`
|
|
15
|
+
* (decision 129). Not `sleep`, which MySQL answers with 1 instead of an error when the limit
|
|
16
|
+
* cuts it.
|
|
17
|
+
*/
|
|
18
|
+
readonly slowRead?: (seconds: number) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Sets the session's own statement limit to `ms`, as a DBA's per-user default would; required
|
|
21
|
+
* with `slowRead`.
|
|
22
|
+
*/
|
|
23
|
+
readonly sessionTimeout?: (ms: number) => string;
|
|
12
24
|
/**
|
|
13
25
|
* A write that succeeds on a writable session and leaves the fixture unchanged, such as an
|
|
14
26
|
* update setting a column to itself; the read-only case is skipped without one.
|
package/dist/testing/index.js
CHANGED
|
@@ -179,6 +179,17 @@ const SLEEP_SECONDS = 30;
|
|
|
179
179
|
const CANCEL_DELAY_MS = 500;
|
|
180
180
|
/** Well under `SLEEP_SECONDS`, so a cancel that did nothing fails the test instead of passing late. */
|
|
181
181
|
const CANCEL_DEADLINE_MS = 10_000;
|
|
182
|
+
const CANCEL_REACHES = ["statement", "connection", "blockingCommands"];
|
|
183
|
+
/** The limit the `serverTimeout` cases ask for; servers check theirs about every 100 ms. */
|
|
184
|
+
const SERVER_LIMIT_MS = 1500;
|
|
185
|
+
/** Far past `SERVER_LIMIT_MS`, so a limit that did nothing fails the test instead of passing late. */
|
|
186
|
+
const SLOW_READ_SECONDS = 8;
|
|
187
|
+
/** Long enough to show a read ran past `SERVER_LIMIT_MS` with no limit, short enough to wait for. */
|
|
188
|
+
const SHORT_READ_SECONDS = 3;
|
|
189
|
+
/** A limit the slow read never reaches, for the cases where something else must stop it first. */
|
|
190
|
+
const UNREACHED_LIMIT_MS = 2 * SLOW_READ_SECONDS * 1000;
|
|
191
|
+
/** When a stopped statement must have rejected: the limit plus the server's slack. */
|
|
192
|
+
const SERVER_LIMIT_DEADLINE_MS = 5_000;
|
|
182
193
|
const KEEPALIVE_DELAY_MS = 1000;
|
|
183
194
|
/** How long a listing may take to show what another connection just did. */
|
|
184
195
|
const LISTING_DEADLINE_MS = 10_000;
|
|
@@ -360,6 +371,15 @@ export function runPluginContract(plugin, fixture) {
|
|
|
360
371
|
return Promise.resolve();
|
|
361
372
|
});
|
|
362
373
|
});
|
|
374
|
+
const brands = fixture.manifest.brands;
|
|
375
|
+
it.runIf(brands !== undefined)("givenBrands_whenSessionOpens_thenServerBrandIsDeclared", async () => {
|
|
376
|
+
await withSession(plugin, fixture, (session) => {
|
|
377
|
+
if (session.serverBrand !== undefined) {
|
|
378
|
+
expect(brands?.map((brand) => brand.id)).toContain(session.serverBrand);
|
|
379
|
+
}
|
|
380
|
+
return Promise.resolve();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
363
383
|
it("givenSession_whenConnected_thenTlsIsNullOrDescribed", async () => {
|
|
364
384
|
await withSession(plugin, fixture, (session) => {
|
|
365
385
|
if (session.tls !== undefined && session.tls !== null) {
|
|
@@ -2142,6 +2162,94 @@ export function runPluginContract(plugin, fixture) {
|
|
|
2142
2162
|
await expectAlive(session);
|
|
2143
2163
|
});
|
|
2144
2164
|
});
|
|
2165
|
+
it("givenCancelReach_whenSessionOpens_thenMethodNamedAndReachKnown", async () => {
|
|
2166
|
+
await withSession(plugin, fixture, (session) => {
|
|
2167
|
+
const reach = session.cancelReach;
|
|
2168
|
+
if (reach === undefined)
|
|
2169
|
+
return Promise.resolve();
|
|
2170
|
+
expect(reach.method).toMatch(/\S/);
|
|
2171
|
+
expect(CANCEL_REACHES).toContain(reach.reaches);
|
|
2172
|
+
return Promise.resolve();
|
|
2173
|
+
});
|
|
2174
|
+
});
|
|
2175
|
+
});
|
|
2176
|
+
describe.runIf(!plugin.capabilities.includes("cancel"))("no cancel", () => {
|
|
2177
|
+
it("givenNoCancel_whenSessionOpens_thenNoCancelReach", async () => {
|
|
2178
|
+
await withSession(plugin, fixture, (session) => {
|
|
2179
|
+
expect(session.cancelReach).toBeUndefined();
|
|
2180
|
+
return Promise.resolve();
|
|
2181
|
+
});
|
|
2182
|
+
});
|
|
2183
|
+
});
|
|
2184
|
+
describe.runIf(plugin.capabilities.includes("serverTimeout"))("serverTimeout", () => {
|
|
2185
|
+
function slowRead(seconds) {
|
|
2186
|
+
const read = fixture.statements?.slowRead;
|
|
2187
|
+
if (read === undefined)
|
|
2188
|
+
throw new Error("A server with a serverTimeout needs fixture.statements.slowRead.");
|
|
2189
|
+
return read(seconds);
|
|
2190
|
+
}
|
|
2191
|
+
/** Runs `sql` to its last row, so a limit that strikes while rows stream counts too. */
|
|
2192
|
+
async function runToEnd(session, sql, serverTimeoutMs) {
|
|
2193
|
+
const options = serverTimeoutMs === undefined ? undefined : { serverTimeoutMs };
|
|
2194
|
+
await collect((await session.execute(sql, undefined, options)).rows);
|
|
2195
|
+
}
|
|
2196
|
+
/** Runs `use` on a session whose server enforces a limit; a server without one has nothing to check. */
|
|
2197
|
+
async function withLimitedSession(use) {
|
|
2198
|
+
await withSession(plugin, fixture, async (session) => {
|
|
2199
|
+
if (session.serverTimeout === null || session.serverTimeout === undefined)
|
|
2200
|
+
return;
|
|
2201
|
+
await use(session);
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
it("givenSession_whenInspected_thenReportsServerTimeout", async () => {
|
|
2205
|
+
await withSession(plugin, fixture, (session) => {
|
|
2206
|
+
expect(session.serverTimeout).not.toBeUndefined();
|
|
2207
|
+
if (session.serverTimeout !== null && session.serverTimeout !== undefined) {
|
|
2208
|
+
expect(session.serverTimeout.setting).toMatch(/\S/);
|
|
2209
|
+
}
|
|
2210
|
+
return Promise.resolve();
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
it("givenServerTimeout_whenSlowReadPastLimit_thenRejectsTimedOut", async () => {
|
|
2214
|
+
await withLimitedSession(async (session) => {
|
|
2215
|
+
const started = Date.now();
|
|
2216
|
+
const code = await rejectionCode(runToEnd(session, slowRead(SLOW_READ_SECONDS), SERVER_LIMIT_MS));
|
|
2217
|
+
expect(code).toBe("timedOut");
|
|
2218
|
+
expect(Date.now() - started).toBeLessThan(SERVER_LIMIT_DEADLINE_MS);
|
|
2219
|
+
await expectAlive(session);
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
it("givenServerTimeoutThenNone_whenSlowRead_thenRunsPastIt", async () => {
|
|
2223
|
+
await withLimitedSession(async (session) => {
|
|
2224
|
+
await runToEnd(session, selectOne, SERVER_LIMIT_MS);
|
|
2225
|
+
const started = Date.now();
|
|
2226
|
+
await runToEnd(session, slowRead(SHORT_READ_SECONDS));
|
|
2227
|
+
expect(Date.now() - started).toBeGreaterThan(SERVER_LIMIT_MS);
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
it("givenOwnLowerLimit_whenHigherAsked_thenOwnStops", async () => {
|
|
2231
|
+
await withLimitedSession(async (session) => {
|
|
2232
|
+
const own = fixture.statements?.sessionTimeout;
|
|
2233
|
+
if (own === undefined)
|
|
2234
|
+
throw new Error("A server with a serverTimeout needs fixture.statements.sessionTimeout.");
|
|
2235
|
+
await runToEnd(session, own(SERVER_LIMIT_MS));
|
|
2236
|
+
const started = Date.now();
|
|
2237
|
+
const code = await rejectionCode(runToEnd(session, slowRead(SLOW_READ_SECONDS), UNREACHED_LIMIT_MS));
|
|
2238
|
+
expect(code).toBe("timedOut");
|
|
2239
|
+
expect(Date.now() - started).toBeLessThan(SERVER_LIMIT_DEADLINE_MS);
|
|
2240
|
+
});
|
|
2241
|
+
});
|
|
2242
|
+
it("givenServerTimeout_whenCancelled_thenCancelledNotTimedOut", async () => {
|
|
2243
|
+
await withLimitedSession(async (session) => {
|
|
2244
|
+
const started = Date.now();
|
|
2245
|
+
const running = rejectionCode(runToEnd(session, slowRead(SLOW_READ_SECONDS), UNREACHED_LIMIT_MS));
|
|
2246
|
+
await delay(CANCEL_DELAY_MS);
|
|
2247
|
+
await session.cancel?.();
|
|
2248
|
+
expect(await running).toBe("cancelled");
|
|
2249
|
+
expect(Date.now() - started).toBeLessThan(CANCEL_DEADLINE_MS);
|
|
2250
|
+
await expectAlive(session);
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2145
2253
|
});
|
|
2146
2254
|
describe.runIf(plugin.capabilities.includes("keepAlive"))("keepAlive", () => {
|
|
2147
2255
|
it("givenKeepAlive_whenConnectedWithDelay_thenPingResolvesAndSessionWorks", async () => {
|
package/dist/version.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The plugin contract's version, by semver (decision 106): a host loads a plugin built for the same
|
|
3
3
|
* major and an older or equal minor. Typed `string` so a bump does not change the API report.
|
|
4
4
|
*/
|
|
5
|
-
export const API_VERSION = "1.
|
|
5
|
+
export const API_VERSION = "1.7.0";
|
|
6
6
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
7
7
|
/** `major.minor.patch` and nothing else; pre-release and build tags are not accepted. */
|
|
8
8
|
export function parseVersion(text) {
|