@gscdump/engine 0.25.13 → 0.26.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.
@@ -0,0 +1,117 @@
1
+ type EngineErrorKind = 'analyzer-not-found' | 'analyzer-capability-missing' | 'invalid-sql-literal' | 'placeholder-arity-mismatch' | 'invalid-search-types' | 'attached-table-missing' | 'manifest-cas-exhausted' | 'invalid-snapshot-filename' | 'unsupported-snapshot-index-version' | 'invalid-schema-identifier' | 'invalid-year-month' | 'missing-attach-url' | 'manifest-cas-round-lost' | 'iceberg-table-op-failed' | 'sink-table-flush-failed' | 'rollup-build-failed' | 'lock-acquire-timeout';
2
+ type EngineError = {
3
+ kind: 'analyzer-not-found';
4
+ tool: string;
5
+ message: string;
6
+ } | {
7
+ kind: 'analyzer-capability-missing';
8
+ tool: string;
9
+ missing: readonly string[];
10
+ message: string;
11
+ } | {
12
+ kind: 'invalid-sql-literal';
13
+ message: string;
14
+ } | {
15
+ kind: 'placeholder-arity-mismatch';
16
+ message: string;
17
+ } | {
18
+ kind: 'invalid-search-types';
19
+ message: string;
20
+ cause?: unknown;
21
+ } | {
22
+ kind: 'attached-table-missing';
23
+ missing: readonly string[];
24
+ message: string;
25
+ } | {
26
+ kind: 'manifest-cas-exhausted';
27
+ message: string;
28
+ siteId: string;
29
+ table: string;
30
+ attempts: number;
31
+ } | {
32
+ kind: 'invalid-snapshot-filename';
33
+ message: string;
34
+ fileName: string;
35
+ } | {
36
+ kind: 'unsupported-snapshot-index-version';
37
+ message: string;
38
+ version: unknown;
39
+ } | {
40
+ kind: 'invalid-schema-identifier';
41
+ message: string;
42
+ schema: string;
43
+ } | {
44
+ kind: 'invalid-year-month';
45
+ message: string;
46
+ value: string;
47
+ } | {
48
+ kind: 'missing-attach-url';
49
+ message: string;
50
+ fileName: string;
51
+ } | {
52
+ kind: 'manifest-cas-round-lost';
53
+ message: string;
54
+ siteId: string;
55
+ table: string;
56
+ attempt: number;
57
+ } | {
58
+ kind: 'iceberg-table-op-failed';
59
+ message: string;
60
+ op: 'create' | 'drop';
61
+ table: string;
62
+ cause?: unknown;
63
+ } | {
64
+ kind: 'sink-table-flush-failed';
65
+ message: string;
66
+ table: string;
67
+ cause?: unknown;
68
+ } | {
69
+ kind: 'rollup-build-failed';
70
+ message: string;
71
+ id: string;
72
+ cause?: unknown;
73
+ } | {
74
+ kind: 'lock-acquire-timeout';
75
+ message: string;
76
+ scope: string;
77
+ timeoutMs: number;
78
+ };
79
+ declare const engineErrors: {
80
+ readonly analyzerNotFound: (tool: string) => EngineError;
81
+ readonly analyzerCapabilityMissing: (tool: string, missing: readonly string[]) => EngineError;
82
+ readonly nonFiniteNumberLiteral: (value: number) => EngineError;
83
+ readonly controlCharsInLiteral: () => EngineError;
84
+ readonly uninlinableLiteralType: (type: string) => EngineError;
85
+ readonly morePlaceholdersThanParams: (have: number) => EngineError;
86
+ readonly dollarPlaceholderOutOfRange: (n: number, have: number) => EngineError;
87
+ readonly mixedPlaceholderStyles: () => EngineError;
88
+ readonly unusedParams: (unused: number) => EngineError;
89
+ readonly searchTypesNotArray: () => EngineError;
90
+ readonly unknownSearchType: (value: unknown) => EngineError;
91
+ readonly searchTypesMissingWeb: () => EngineError;
92
+ readonly attachedTableMissing: (missing: readonly string[]) => EngineError;
93
+ readonly manifestCasExhausted: (siteId: string, table: string, attempts: number) => EngineError;
94
+ readonly invalidSnapshotFilename: (fileName: string) => EngineError;
95
+ readonly unsupportedSnapshotIndexVersion: (version: unknown) => EngineError;
96
+ readonly invalidSchemaIdentifier: (schema: string) => EngineError;
97
+ readonly invalidYearMonth: (value: string) => EngineError;
98
+ readonly missingAttachUrl: (fileName: string) => EngineError;
99
+ readonly manifestCasRoundLost: (siteId: string, table: string, attempt: number) => EngineError;
100
+ readonly icebergTableOpFailed: (op: "create" | "drop", table: string, cause: unknown) => EngineError;
101
+ readonly sinkTableFlushFailed: (table: string, cause: unknown) => EngineError;
102
+ readonly rollupBuildFailed: (id: string, cause: unknown) => EngineError;
103
+ readonly lockAcquireTimeout: (scope: string, timeoutMs: number) => EngineError;
104
+ };
105
+ declare function isEngineError(value: unknown): value is EngineError;
106
+ /** The human-readable rendering of an `EngineError`, for logs and string sinks. */
107
+ declare function formatEngineError(error: EngineError): string;
108
+ /**
109
+ * Re-raises an `EngineError` value as a generic `Error`, stashing the union under
110
+ * `.engineError` for stack-walking. Used by the throwing wrappers over the
111
+ * `Result`-returning cores whose original throws were bare `Error`s (the SQL
112
+ * binder, sync-config). Modules that historically threw a *named* class
113
+ * (`AnalyzerCapabilityError`, `AttachedTableMissingError`) provide their own
114
+ * mapper so the class identity callers match on is preserved.
115
+ */
116
+ declare function engineErrorToException(error: EngineError): Error;
117
+ export { EngineError, EngineErrorKind, engineErrorToException, engineErrors, formatEngineError, isEngineError };
@@ -1,4 +1,5 @@
1
1
  import { Row, SearchType as SearchType$1, StorageEngine, TenantCtx } from "./storage.mjs";
2
+ import { EngineError } from "./errors.mjs";
2
3
  import { AnalysisParams, AnalysisResult } from "./analysis-types.mjs";
3
4
  import { ResolverAdapter } from "./types.mjs";
4
5
  import { AnalysisQuerySource, AnalysisSourceKind, AnalyzerRegistry, FileSet, QueryRow, SourceCapabilities } from "./registry.mjs";
@@ -39,6 +40,7 @@ interface AttachedTableSourceOptions {
39
40
  }
40
41
  declare class AttachedTableMissingError extends Error {
41
42
  readonly missing: readonly string[];
43
+ readonly engineError: EngineError;
42
44
  constructor(missing: readonly string[]);
43
45
  }
44
46
  /**
@@ -2,8 +2,8 @@ import { Row as Row$1 } from "./storage.mjs";
2
2
  import { AnalysisParams } from "./analysis-types.mjs";
3
3
  import { ResolverAdapter } from "./types.mjs";
4
4
  import { PlannerCapabilities } from "gscdump/query/plan";
5
- import { TableName } from "@gscdump/contracts";
6
5
  import { BuilderState } from "gscdump/query";
6
+ import { TableName } from "@gscdump/contracts";
7
7
  type QueryRow = Record<string, unknown>;
8
8
  interface FileSet {
9
9
  table: TableName;
@@ -1,4 +1,6 @@
1
1
  import { Row as Row$1, SearchType, TenantCtx as TenantCtx$1 } from "./storage.mjs";
2
+ import { EngineError } from "./errors.mjs";
3
+ import { Result } from "gscdump/result";
2
4
  import { icebergAppend, restCatalogConnect, s3SignedResolver } from "icebird";
3
5
  import { TableName } from "@gscdump/contracts";
4
6
  /**
@@ -209,12 +211,16 @@ declare function isCommitRateLimited(err: unknown): boolean;
209
211
  * coalescing) is assessed in the Phase-1.5 report.
210
212
  */
211
213
  declare function icebergAppendRetrying(args: Parameters<typeof icebergAppend>[0], options?: CommitRetryOptions): Promise<void>;
212
- /** Outcome of a single table create/drop. */
214
+ /**
215
+ * Outcome of a single table create/drop: the table name plus a `Result` —
216
+ * `Ok(void)` on success, `Err(iceberg-table-op-failed)` carrying the failure
217
+ * message (and original `cause`) when the catalog rejects the op (e.g. "table
218
+ * already exists", a 5xx). Per-table so a partial provisioning run is fully
219
+ * observable; the human-readable string lives on `error.message`.
220
+ */
213
221
  interface IcebergTableOpResult {
214
222
  table: string;
215
- ok: boolean;
216
- /** Present when `ok` is false. */
217
- error?: string;
223
+ outcome: Result<void, EngineError>;
218
224
  }
219
225
  /**
220
226
  * Ensure the catalog namespace exists. Idempotent — an "already exists"
@@ -229,7 +235,13 @@ declare function ensureIcebergNamespace(conn: IcebergConnection): Promise<void>;
229
235
  * as a failed result. Used by the app's one-off provisioning script.
230
236
  */
231
237
  declare function createIcebergTables(conn: IcebergConnection, tables?: readonly IcebergTableName[]): Promise<IcebergTableOpResult[]>;
232
- /** List the table names currently in the catalog namespace. */
238
+ /**
239
+ * List the table names currently in the catalog namespace.
240
+ *
241
+ * A genuinely-empty namespace resolves to `[]`. A LIST *failure* (catalog
242
+ * unreachable, 401/403, 5xx) propagates rather than being masked as an empty
243
+ * list — callers must be able to tell "no tables" from "couldn't ask".
244
+ */
233
245
  declare function listIcebergTables(conn: IcebergConnection): Promise<string[]>;
234
246
  /** A data file in the current snapshot's manifest, scoped to one partition. */
235
247
  interface IcebergListedDataFile {
@@ -321,10 +333,14 @@ interface SinkCapabilities {
321
333
  interface SinkCloseResult {
322
334
  /** Tables whose buffered rows committed durably. */
323
335
  flushed: IcebergTableName[];
324
- /** Tables whose flush failed — their slices must NOT be ledger-recorded. */
336
+ /**
337
+ * Tables whose flush failed — their slices must NOT be ledger-recorded. Each
338
+ * carries a typed `sink-table-flush-failed` `EngineError`; the human-readable
339
+ * cause is on `error.message`.
340
+ */
325
341
  failed: {
326
342
  table: IcebergTableName;
327
- error: string;
343
+ error: EngineError;
328
344
  }[];
329
345
  }
330
346
  interface Sink {
@@ -1,4 +1,5 @@
1
1
  import "./layout.mjs";
2
+ import { engineErrors } from "../errors.mjs";
2
3
  import { assertDimensionsSupported, getFilterDimensions, pgResolverAdapter, resolveToSQL } from "./resolver.mjs";
3
4
  import { runAnalyzerFromSource } from "./dispatch.mjs";
4
5
  function coerceRow(row) {
@@ -16,10 +17,13 @@ function coerceRows(rows) {
16
17
  }
17
18
  var AttachedTableMissingError = class extends Error {
18
19
  missing;
20
+ engineError;
19
21
  constructor(missing) {
20
- super(`attached-table source: required table(s) not attached: ${missing.join(", ")}`);
22
+ const engineError = engineErrors.attachedTableMissing(missing);
23
+ super(engineError.message);
21
24
  this.missing = missing;
22
25
  this.name = "AttachedTableMissingError";
26
+ this.engineError = engineError;
23
27
  }
24
28
  };
25
29
  const ATTACHED_TABLE_CAPABILITIES = {
@@ -1,5 +1,5 @@
1
- import { Grain, Grain as Grain$1, Row, Row as Row$1, TableName, TableName as TableName$1, TenantCtx, TenantCtx as TenantCtx$1 } from "@gscdump/contracts";
2
1
  import { BuilderState, SearchType, SearchType as SearchType$1 } from "gscdump/query";
2
+ import { Grain, Grain as Grain$1, Row, Row as Row$1, TableName, TableName as TableName$1, TenantCtx, TenantCtx as TenantCtx$1 } from "@gscdump/contracts";
3
3
  /**
4
4
  * Per-tier age threshold in days. Default ladder collapses on these gates:
5
5
  * - raw → d7 once a daily file is older than `raw` days (default 7).
@@ -238,7 +238,9 @@ function createFilesystemManifestStore(opts) {
238
238
  factor: 1.5
239
239
  }
240
240
  });
241
- return await fn().finally(() => release().catch(() => {}));
241
+ return await fn().finally(() => release().catch((releaseErr) => {
242
+ console.warn(`[gscdump/engine] failed to release lock ${path}; it will go stale after 30000ms`, releaseErr);
243
+ }));
242
244
  },
243
245
  async purgeTenant(filter) {
244
246
  return enqueue(async () => {
@@ -1,8 +1,10 @@
1
1
  import { DataSource, StorageEngine } from "../_chunks/storage.mjs";
2
2
  import { NodeDuckDBOptions, createNodeDuckDBHandle, resetNodeDuckDB } from "./duckdb-node.mjs";
3
+ import { EngineError } from "../_chunks/errors.mjs";
3
4
  import { SnapshotIndex } from "../_chunks/snapshot.mjs";
4
- import { Row, TableName } from "@gscdump/contracts";
5
+ import { Result } from "gscdump/result";
5
6
  import { SearchType } from "gscdump/query";
7
+ import { Row, TableName } from "@gscdump/contracts";
6
8
  interface NodeHarnessOptions {
7
9
  dataDir: string;
8
10
  /** Tenant user id. Defaults to `'local'` for single-user CLI installs. */
@@ -72,6 +74,14 @@ interface AttachSnapshotResult {
72
74
  * `cold_2024_09`. `hot.duckdb` → `hot`.
73
75
  */
74
76
  declare function snapshotAlias(fileName: string): string;
77
+ /**
78
+ * Errors-as-values core: validates the snapshot index and presigned-URL map,
79
+ * returning a typed `EngineError` for every modelled failure (bad index version,
80
+ * unsafe schema/YYYY-MM identifier, missing attach URL). Underlying DuckDB
81
+ * `runner` IO failures stay defects and propagate. `attachSnapshotIndex` is the
82
+ * throwing wrapper.
83
+ */
84
+ declare function attachSnapshotIndexResult(runner: SnapshotQueryRunner, opts: AttachSnapshotOptions): Promise<Result<AttachSnapshotResult, EngineError>>;
75
85
  declare function attachSnapshotIndex(runner: SnapshotQueryRunner, opts: AttachSnapshotOptions): Promise<AttachSnapshotResult>;
76
86
  interface AttachParquetIndexOptions {
77
87
  /**
@@ -94,4 +104,4 @@ interface AttachParquetIndexResult {
94
104
  tables: string[];
95
105
  }
96
106
  declare function attachParquetIndex(runner: SnapshotQueryRunner, opts: AttachParquetIndexOptions): Promise<AttachParquetIndexResult>;
97
- export { type AttachParquetIndexOptions, type AttachParquetIndexResult, type AttachSnapshotOptions, type AttachSnapshotResult, type NodeDuckDBOptions, type NodeHarness, type NodeHarnessOptions, type SnapshotQueryRunner, attachParquetIndex, attachSnapshotIndex, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
107
+ export { type AttachParquetIndexOptions, type AttachParquetIndexResult, type AttachSnapshotOptions, type AttachSnapshotResult, type NodeDuckDBOptions, type NodeHarness, type NodeHarnessOptions, type SnapshotQueryRunner, attachParquetIndex, attachSnapshotIndex, attachSnapshotIndexResult, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
@@ -1,7 +1,9 @@
1
+ import { engineErrors } from "../errors.mjs";
1
2
  import { createDuckDBCodec, createDuckDBExecutor, createStorageEngine } from "../_chunks/engine.mjs";
2
3
  import { createNodeDuckDBHandle, resetNodeDuckDB } from "./duckdb-node.mjs";
3
4
  import { createFilesystemDataSource, createFilesystemManifestStore } from "./filesystem.mjs";
4
5
  import { encodeSiteId } from "gscdump";
6
+ import { err, ok, unwrapResult } from "gscdump/result";
5
7
  import path from "node:path";
6
8
  function createNodeHarness(opts) {
7
9
  const dataDir = opts.dataDir;
@@ -72,20 +74,31 @@ function snapshotAlias(fileName) {
72
74
  if (!m?.[1]) throw new TypeError(`snapshotAlias: unrecognised filename ${JSON.stringify(fileName)}`);
73
75
  return `cold_${m[1].replace("-", "_")}`;
74
76
  }
75
- async function attachSnapshotIndex(runner, opts) {
77
+ const SNAPSHOT_TYPE_ERROR_KINDS = new Set([
78
+ "invalid-snapshot-filename",
79
+ "unsupported-snapshot-index-version",
80
+ "invalid-schema-identifier",
81
+ "invalid-year-month"
82
+ ]);
83
+ function snapshotAttachErrorToException(error) {
84
+ const exception = SNAPSHOT_TYPE_ERROR_KINDS.has(error.kind) ? new TypeError(error.message) : new Error(error.message);
85
+ exception.engineError = error;
86
+ return exception;
87
+ }
88
+ async function attachSnapshotIndexResult(runner, opts) {
76
89
  const { index, attachUrls } = opts;
77
90
  const schema = opts.schema ?? "main";
78
91
  const forceDownload = opts.forceDownload !== false;
79
- if (index?.version !== 1) throw new TypeError(`attachSnapshotIndex: unsupported snapshot index version ${String(index?.version)}; expected 1`);
80
- if (!SCHEMA_IDENT_RE.test(schema)) throw new TypeError(`attachSnapshotIndex: invalid schema identifier ${JSON.stringify(schema)}`);
81
- for (const ym of index.cold) if (!YEAR_MONTH_RE.test(ym)) throw new TypeError(`attachSnapshotIndex: invalid YYYY-MM entry ${JSON.stringify(ym)} in index.cold`);
92
+ if (index?.version !== 1) return err(engineErrors.unsupportedSnapshotIndexVersion(index?.version));
93
+ if (!SCHEMA_IDENT_RE.test(schema)) return err(engineErrors.invalidSchemaIdentifier(schema));
94
+ for (const ym of index.cold) if (!YEAR_MONTH_RE.test(ym)) return err(engineErrors.invalidYearMonth(ym));
82
95
  await runner("LOAD httpfs").catch(() => void 0);
83
96
  if (forceDownload) await runner("SET force_download=true");
84
97
  const plan = [];
85
98
  for (const ym of index.cold) {
86
99
  const fileName = `cold-${ym}.duckdb`;
87
100
  const url = attachUrls[fileName];
88
- if (!url) throw new Error(`attachSnapshotIndex: attachUrls missing entry for ${fileName}`);
101
+ if (!url) return err(engineErrors.missingAttachUrl(fileName));
89
102
  plan.push({
90
103
  fileName,
91
104
  alias: snapshotAlias(fileName),
@@ -95,7 +108,7 @@ async function attachSnapshotIndex(runner, opts) {
95
108
  if (index.hot) {
96
109
  const fileName = "hot.duckdb";
97
110
  const url = attachUrls[fileName];
98
- if (!url) throw new Error(`attachSnapshotIndex: attachUrls missing entry for ${fileName}`);
111
+ if (!url) return err(engineErrors.missingAttachUrl(fileName));
99
112
  plan.push({
100
113
  fileName,
101
114
  alias: snapshotAlias(fileName),
@@ -125,10 +138,13 @@ async function attachSnapshotIndex(runner, opts) {
125
138
  await runner(`CREATE OR REPLACE VIEW ${schema}.${table} AS ${aliases.filter((a) => dbsSet.has(a)).map((db) => `SELECT * FROM ${db}.${table}`).join(" UNION ALL BY NAME ")}`);
126
139
  tables.push(table);
127
140
  }
128
- return {
141
+ return ok({
129
142
  schema,
130
143
  aliases,
131
144
  tables
132
- };
145
+ });
146
+ }
147
+ async function attachSnapshotIndex(runner, opts) {
148
+ return unwrapResult(await attachSnapshotIndexResult(runner, opts), snapshotAttachErrorToException);
133
149
  }
134
- export { attachParquetIndex, attachSnapshotIndex, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
150
+ export { attachParquetIndex, attachSnapshotIndex, attachSnapshotIndexResult, createNodeDuckDBHandle, createNodeHarness, resetNodeDuckDB, snapshotAlias };
@@ -1,4 +1,6 @@
1
1
  import { inferLegacyTier, inferSearchType } from "../_chunks/layout.mjs";
2
+ import { engineErrorToException, engineErrors } from "../errors.mjs";
3
+ import { err, ok, unwrapResult } from "gscdump/result";
2
4
  const SHARD_RE = /^u_[^/]+\/manifest\/(?<siteId>[^/]+)\/(?<table>[^/]+)\/HEAD$/;
3
5
  const CAS_BACKOFF_BASE_MS = 5;
4
6
  const CAS_BACKOFF_CAP_MS = 250;
@@ -83,14 +85,14 @@ function createR2ManifestStore(opts) {
83
85
  headEtag: head.etag
84
86
  };
85
87
  }
86
- async function writeShard(siteId, table, snapshot, headEtag) {
88
+ async function writeShardResult(siteId, table, snapshot, headEtag, attempt) {
87
89
  const id = newSnapshotId();
88
90
  const snapKey = snapshotKey(userId, siteId, table, id);
89
91
  await bucket.put(snapKey, JSON.stringify(snapshot));
90
92
  const conditional = headEtag ? { onlyIf: { etagMatches: headEtag } } : { onlyIf: { etagDoesNotMatch: "*" } };
91
- return { ok: await bucket.put(headKey(userId, siteId, table), id, conditional) !== null };
93
+ return await bucket.put(headKey(userId, siteId, table), id, conditional) !== null ? ok(void 0) : err(engineErrors.manifestCasRoundLost(siteId, table, attempt));
92
94
  }
93
- async function mutateShard(siteId, table, mutate) {
95
+ async function mutateShardResult(siteId, table, mutate) {
94
96
  let attempt = 0;
95
97
  while (attempt < maxRetries) {
96
98
  onEvent?.({
@@ -101,15 +103,15 @@ function createR2ManifestStore(opts) {
101
103
  });
102
104
  const { snapshot, headEtag } = await readShard(siteId, table);
103
105
  await mutate(snapshot);
104
- const { ok } = await writeShard(siteId, table, snapshot, headEtag);
105
- if (ok) {
106
+ const round = await writeShardResult(siteId, table, snapshot, headEtag, attempt);
107
+ if (round.ok) {
106
108
  onEvent?.({
107
109
  kind: "cas-committed",
108
110
  siteId,
109
111
  table,
110
112
  attempts: attempt + 1
111
113
  });
112
- return;
114
+ return round;
113
115
  }
114
116
  onEvent?.({
115
117
  kind: "cas-rejected",
@@ -120,7 +122,10 @@ function createR2ManifestStore(opts) {
120
122
  attempt++;
121
123
  if (attempt < maxRetries) await casBackoff(attempt);
122
124
  }
123
- throw new Error(`R2 manifest CAS exceeded ${maxRetries} retries for ${siteId}/${table}`);
125
+ return err(engineErrors.manifestCasExhausted(siteId, table, maxRetries));
126
+ }
127
+ async function mutateShard(siteId, table, mutate) {
128
+ return unwrapResult(await mutateShardResult(siteId, table, mutate), engineErrorToException);
124
129
  }
125
130
  async function listShards() {
126
131
  const shards = [];
@@ -1,8 +1,10 @@
1
+ import { EngineError } from "../_chunks/errors.mjs";
1
2
  import { AnalysisParams, AnalysisResult } from "../_chunks/analysis-types.mjs";
2
3
  import { AnalysisQuerySource, Analyzer, AnalyzerRegistry, AnalyzerRegistryInit, AnalyzerVariants, BuildContext, DefineAnalyzerOptions, DefinedAnalyzer, Plan, ReduceContext, ReduceCtx, Reducer, RequiredCapability, RowQueriesPlan, SqlExtraQuery, SqlPlan, SqlPlanSpec, TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, requireAdapter } from "../_chunks/registry.mjs";
3
4
  declare class AnalyzerCapabilityError extends Error {
4
5
  readonly tool: string;
5
6
  readonly missing: readonly RequiredCapability[];
7
+ readonly engineError: EngineError;
6
8
  constructor(tool: string, missing: readonly RequiredCapability[]);
7
9
  }
8
10
  /**