@noormdev/sdk 1.0.0-alpha.17 → 1.0.0-alpha.18

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/index.js CHANGED
@@ -1,19 +1,22 @@
1
- import { processFile, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES, GZIP_THRESHOLD, GZIP_RATIO_THRESHOLD } from './chunk-BTXUEWYQ.js';
1
+ import { processFile, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES } from './chunk-KF7GE2VM.js';
2
2
  import { initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey } from './chunk-LJDHQEWW.js';
3
3
  import { observer } from './chunk-4HW4CS2C.js';
4
4
  import { constants, existsSync, readFileSync, mkdirSync, writeFileSync, createWriteStream } from 'fs';
5
- import path, { join, dirname } from 'path';
5
+ import path, { join, dirname, resolve } from 'path';
6
6
  import { makeNestedConfig, gigabytes, attempt, retry, clone, merge, attemptSync } from '@logosdx/utils';
7
- import { homedir, userInfo } from 'os';
7
+ import { homedir, availableParallelism, userInfo } from 'os';
8
8
  import { stat, mkdir, writeFile, unlink, rename, rm, readdir, readFile, access } from 'fs/promises';
9
- import { createHash, createDecipheriv, randomBytes, createCipheriv, hkdfSync } from 'crypto';
9
+ import { createHash, createDecipheriv, randomBytes, createCipheriv, randomUUID, hkdfSync } from 'crypto';
10
10
  import { z } from 'zod';
11
11
  import { parse, stringify } from 'yaml';
12
12
  import { execSync } from 'child_process';
13
13
  import { sql } from 'kysely';
14
14
  import 'dayjs';
15
15
  import ansis from 'ansis';
16
- import { createGzip, gunzipSync, gzipSync } from 'zlib';
16
+ import { Worker, parentPort, workerData } from 'worker_threads';
17
+ import { ObserverRelay } from '@logosdx/observer';
18
+ import { fileURLToPath } from 'url';
19
+ import { createGzip } from 'zlib';
17
20
  import { pipeline } from 'stream/promises';
18
21
  import { PassThrough } from 'stream';
19
22
  import JSON5 from 'json5';
@@ -1672,6 +1675,7 @@ async function loadExistingIdentity() {
1672
1675
  var ConnectionManager = class {
1673
1676
  #cached = /* @__PURE__ */ new Map();
1674
1677
  #tracked = /* @__PURE__ */ new Map();
1678
+ #bridges = /* @__PURE__ */ new Map();
1675
1679
  #nextId = 1;
1676
1680
  #shuttingDown = false;
1677
1681
  #unsubscribe = null;
@@ -1755,7 +1759,7 @@ var ConnectionManager = class {
1755
1759
  for (const [id, entry] of trackedEntries) {
1756
1760
  const destroyWithTimeout = Promise.race([
1757
1761
  entry.conn.destroy(),
1758
- new Promise((resolve) => setTimeout(resolve, CLOSE_TIMEOUT))
1762
+ new Promise((resolve2) => setTimeout(resolve2, CLOSE_TIMEOUT))
1759
1763
  ]);
1760
1764
  const [, err] = await attempt(() => destroyWithTimeout);
1761
1765
  this.#tracked.delete(id);
@@ -1765,6 +1769,28 @@ var ConnectionManager = class {
1765
1769
  observer.emit("connection:close", { configName: entry.configName });
1766
1770
  }
1767
1771
  }
1772
+ const bridgeNames = Array.from(this.#bridges.keys());
1773
+ for (const name of bridgeNames) {
1774
+ await this.closeBridge(name);
1775
+ }
1776
+ }
1777
+ /**
1778
+ * Track a WorkerBridge connection for a config.
1779
+ *
1780
+ * The bridge is shut down alongside regular connections on app:shutdown.
1781
+ */
1782
+ trackBridge(configName, bridge) {
1783
+ this.#bridges.set(configName, bridge);
1784
+ }
1785
+ /**
1786
+ * Close a specific bridge by config name.
1787
+ */
1788
+ async closeBridge(configName) {
1789
+ const bridge = this.#bridges.get(configName);
1790
+ if (bridge) {
1791
+ await bridge.shutdown();
1792
+ this.#bridges.delete(configName);
1793
+ }
1768
1794
  }
1769
1795
  /**
1770
1796
  * Check if a cached connection exists for a config name.
@@ -1776,7 +1802,7 @@ var ConnectionManager = class {
1776
1802
  * Get total number of active connections.
1777
1803
  */
1778
1804
  get size() {
1779
- return this.#cached.size + this.#tracked.size;
1805
+ return this.#cached.size + this.#tracked.size + this.#bridges.size;
1780
1806
  }
1781
1807
  /**
1782
1808
  * Check if shutdown is in progress.
@@ -4546,7 +4572,7 @@ function getLockManager() {
4546
4572
  return instance3;
4547
4573
  }
4548
4574
  function sleep(ms) {
4549
- return new Promise((resolve) => setTimeout(resolve, ms));
4575
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4550
4576
  }
4551
4577
 
4552
4578
  // src/core/change/executor.ts
@@ -9494,7 +9520,7 @@ async function copyVaultSecrets(sourceConfig, destConfig, keys, identityHash, pr
9494
9520
  throw new Error(`No vault access on source config "${sourceConfig.name}"`);
9495
9521
  }
9496
9522
  const destStatus = await getVaultStatus(ctx.destination.db, identityHash, ctx.destination.dialect);
9497
- let destVaultKey = null;
9523
+ let destVaultKey;
9498
9524
  if (!destStatus.isInitialized) {
9499
9525
  const [newKey, initErr] = await initializeVault(
9500
9526
  ctx.destination.db,
@@ -10681,16 +10707,16 @@ async function planTransfer(ctx, options = {}) {
10681
10707
  const orderedNames = sortErr ? allTables.map((t) => t.name) : sortedNames;
10682
10708
  const tablePlans = [];
10683
10709
  for (const name of orderedNames) {
10684
- const meta = tablesByName[name];
10685
- if (!meta) continue;
10710
+ const meta2 = tablesByName[name];
10711
+ if (!meta2) continue;
10686
10712
  tablePlans.push({
10687
- name: meta.name,
10688
- schema: meta.schema,
10689
- rowCount: meta.rowCount,
10690
- hasIdentity: meta.hasIdentity,
10691
- identityColumn: meta.identityColumn,
10692
- primaryKey: meta.primaryKey,
10693
- columns: meta.columns,
10713
+ name: meta2.name,
10714
+ schema: meta2.schema,
10715
+ rowCount: meta2.rowCount,
10716
+ hasIdentity: meta2.hasIdentity,
10717
+ identityColumn: meta2.identityColumn,
10718
+ primaryKey: meta2.primaryKey,
10719
+ columns: meta2.columns,
10694
10720
  dependsOn: dependencyMap.get(name) ?? []
10695
10721
  });
10696
10722
  }
@@ -11234,8 +11260,8 @@ async function executeTransfer(ctx, plan, options = {}) {
11234
11260
  const strategy = options.onConflict ?? "fail";
11235
11261
  const useSameServer = plan.sameServer && strategy === "fail" && !plan.crossDialect;
11236
11262
  const useCrossDialect = plan.crossDialect && tablePlan.columnTypes;
11237
- let tableResult = null;
11238
- let tableErr = null;
11263
+ let tableResult;
11264
+ let tableErr;
11239
11265
  if (useSameServer) {
11240
11266
  [tableResult, tableErr] = await transferTableSameServer(
11241
11267
  ctx,
@@ -11864,6 +11890,174 @@ var TransferNamespace = class {
11864
11890
  return getTransferPlan(this.#state.config, destConfig, options);
11865
11891
  }
11866
11892
  };
11893
+
11894
+ // src/core/worker-bridge/pool.ts
11895
+ var WorkerPool = class {
11896
+ #workers;
11897
+ #nextIndex = 0;
11898
+ #isShutdown = false;
11899
+ constructor(script, options) {
11900
+ const size = Math.max(1, options.size);
11901
+ this.#workers = Array.from(
11902
+ { length: size },
11903
+ () => new WorkerBridge(script)
11904
+ );
11905
+ }
11906
+ /**
11907
+ * Returns the number of workers in the pool.
11908
+ */
11909
+ get size() {
11910
+ return this.#workers.length;
11911
+ }
11912
+ /**
11913
+ * Returns true if the pool has been shut down.
11914
+ */
11915
+ get isShutdown() {
11916
+ return this.#isShutdown;
11917
+ }
11918
+ /**
11919
+ * Sends a request to the next available worker (round-robin) and awaits its response.
11920
+ *
11921
+ * Injects a correlation ID so concurrent requests on the same event type
11922
+ * can be matched to their originating worker.
11923
+ *
11924
+ * @example
11925
+ * const { values } = await pool.request('serialize', { row, columns, index: 0 })
11926
+ */
11927
+ async request(event, data2, options) {
11928
+ const worker = this.#workers[this.#nextIndex];
11929
+ this.#nextIndex = (this.#nextIndex + 1) % this.#workers.length;
11930
+ return worker.request(event, data2, options);
11931
+ }
11932
+ /**
11933
+ * Registers a listener on all workers for the given event.
11934
+ *
11935
+ * Useful for broadcast-style events that every worker may emit.
11936
+ *
11937
+ * @example
11938
+ * pool.on('serialize:res', ({ data }) => console.log(data.values))
11939
+ */
11940
+ on(event, callback, options) {
11941
+ for (const worker of this.#workers) {
11942
+ worker.on(event, callback, options);
11943
+ }
11944
+ }
11945
+ /**
11946
+ * Terminates all workers in the pool.
11947
+ *
11948
+ * Marks the pool as shut down and waits for every worker thread to exit
11949
+ * before resolving.
11950
+ *
11951
+ * @example
11952
+ * await pool.shutdown()
11953
+ */
11954
+ async shutdown() {
11955
+ this.#isShutdown = true;
11956
+ await Promise.all(this.#workers.map((w) => w.shutdown()));
11957
+ }
11958
+ };
11959
+
11960
+ // src/core/worker-bridge/types.ts
11961
+ function isTransferable(data2) {
11962
+ return !!data2 && typeof data2 === "object" && "__transfer" in data2;
11963
+ }
11964
+
11965
+ // src/core/worker-bridge/bridge.ts
11966
+ var WorkerBridge = class extends ObserverRelay {
11967
+ #port;
11968
+ #ownsWorker;
11969
+ constructor(script, data2) {
11970
+ const isParent = !!script;
11971
+ super({ name: isParent ? "bridge:parent" : "bridge:worker" });
11972
+ if (script) {
11973
+ this.#port = new Worker(script, { workerData: data2 });
11974
+ this.#ownsWorker = true;
11975
+ this.#port.on("exit", (code) => {
11976
+ if (!this.isShutdown && code !== 0) {
11977
+ this.receive("worker:exit", { code, error: `Worker exited with code ${code}` }, {});
11978
+ }
11979
+ });
11980
+ } else {
11981
+ if (!parentPort) {
11982
+ throw new Error(
11983
+ "WorkerBridge: no script provided and not in a worker thread"
11984
+ );
11985
+ }
11986
+ this.#port = parentPort;
11987
+ this.#ownsWorker = false;
11988
+ }
11989
+ this.#port.on("message", (msg) => {
11990
+ this.receive(msg.event, msg.data, {});
11991
+ });
11992
+ }
11993
+ send(event, data2) {
11994
+ if (isTransferable(data2)) {
11995
+ this.#port.postMessage({ event, data: data2 }, data2.__transfer);
11996
+ } else {
11997
+ this.#port.postMessage({ event, data: data2 }, []);
11998
+ }
11999
+ }
12000
+ async request(event, data2, options) {
12001
+ const cid = randomUUID();
12002
+ const pending = this.once(new RegExp(`^${event}:res:${cid}$`), options);
12003
+ this.send(event, { ...data2, __cid: cid });
12004
+ const { data: { data: result } } = await pending;
12005
+ return result;
12006
+ }
12007
+ transfer(event, data2, transferables) {
12008
+ this.#port.postMessage({ event, data: data2 }, transferables);
12009
+ }
12010
+ static pool(script, options) {
12011
+ return new WorkerPool(script, options);
12012
+ }
12013
+ static get workerData() {
12014
+ return workerData;
12015
+ }
12016
+ async shutdown() {
12017
+ super.shutdown();
12018
+ if (this.#ownsWorker && "terminate" in this.#port) {
12019
+ await this.#port.terminate();
12020
+ }
12021
+ }
12022
+ };
12023
+
12024
+ // src/core/worker-bridge/order-buffer.ts
12025
+ var OrderBuffer = class {
12026
+ #buffer = /* @__PURE__ */ new Map();
12027
+ #nextIndex = 0;
12028
+ #flush;
12029
+ constructor(flush) {
12030
+ this.#flush = flush;
12031
+ }
12032
+ get nextIndex() {
12033
+ return this.#nextIndex;
12034
+ }
12035
+ get pending() {
12036
+ return this.#buffer.size;
12037
+ }
12038
+ add(index, item) {
12039
+ this.#buffer.set(index, item);
12040
+ this.#drain();
12041
+ }
12042
+ #drain() {
12043
+ while (this.#buffer.has(this.#nextIndex)) {
12044
+ const item = this.#buffer.get(this.#nextIndex);
12045
+ this.#buffer.delete(this.#nextIndex);
12046
+ this.#nextIndex++;
12047
+ this.#flush(item);
12048
+ }
12049
+ }
12050
+ };
12051
+ var IS_COMPILED = import.meta.url.includes("$bunfs");
12052
+ var meta = import.meta;
12053
+ var MODULE_DIR = typeof meta.dir === "string" ? meta.dir : dirname(fileURLToPath(import.meta.url));
12054
+ var WORKER_DIR = resolve(MODULE_DIR, "../../workers");
12055
+ function resolveWorker(name) {
12056
+ if (IS_COMPILED) {
12057
+ return new URL(`./workers/${name}.js`, import.meta.url);
12058
+ }
12059
+ return resolve(WORKER_DIR, `${name}.js`);
12060
+ }
11867
12061
  var DtWriter = class {
11868
12062
  #filepath;
11869
12063
  #schema;
@@ -11967,8 +12161,8 @@ var DtWriter = class {
11967
12161
  this.#passthrough.end();
11968
12162
  await this.#pipelinePromise;
11969
12163
  } else if (this.#stream) {
11970
- await new Promise((resolve, reject) => {
11971
- this.#stream.end(() => resolve());
12164
+ await new Promise((resolve2, reject) => {
12165
+ this.#stream.end(() => resolve2());
11972
12166
  this.#stream.on("error", reject);
11973
12167
  });
11974
12168
  }
@@ -11985,211 +12179,26 @@ var DtWriter = class {
11985
12179
  this.#stream.write(buf);
11986
12180
  }
11987
12181
  };
11988
- function serializeRow(options) {
11989
- const { row, columns } = options;
11990
- const result = [];
11991
- for (const col of columns) {
11992
- const value = row[col.name];
11993
- result.push(serializeValue(value, col));
11994
- }
11995
- return result;
11996
- }
11997
- function serializeValue(value, column) {
11998
- if (value === null || value === void 0) {
11999
- return null;
12000
- }
12001
- if (isEncodedType(column.type)) {
12002
- return encodeValue(value, column.type);
12003
- }
12004
- return serializeSimple(value, column.type);
12005
- }
12006
- function serializeSimple(value, type) {
12007
- switch (type) {
12008
- case "bigint":
12009
- return String(value);
12010
- case "decimal":
12011
- return String(value);
12012
- case "bool":
12013
- if (typeof value === "number") return value !== 0;
12014
- if (typeof value === "string") return value === "1" || value.toLowerCase() === "true";
12015
- return Boolean(value);
12016
- case "timestamp": {
12017
- if (value instanceof Date) return value.toISOString();
12018
- return String(value);
12019
- }
12020
- case "date": {
12021
- if (value instanceof Date) return value.toISOString().split("T")[0];
12022
- return String(value);
12023
- }
12024
- case "uuid":
12025
- return String(value);
12026
- case "int":
12027
- case "float":
12028
- return typeof value === "string" ? Number(value) : value;
12029
- case "string":
12030
- default:
12031
- return String(value);
12032
- }
12033
- }
12034
- function encodeValue(value, type) {
12035
- if (type === "binary") {
12036
- return encodeBinary(value);
12037
- }
12038
- return encodeJsonLike(value);
12039
- }
12040
- function encodeBinary(value) {
12041
- const buf = toBuffer(value);
12042
- const byteLength = buf.length;
12043
- if (byteLength < GZIP_THRESHOLD) {
12044
- return [buf.toString("base64"), "b64"];
12045
- }
12046
- const compressed = gzipSync(buf);
12047
- if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
12048
- return [compressed.toString("base64"), "gz64"];
12049
- }
12050
- return [buf.toString("base64"), "b64"];
12051
- }
12052
- function encodeJsonLike(value) {
12053
- const jsonStr = JSON.stringify(value);
12054
- const byteLength = Buffer.byteLength(jsonStr, "utf8");
12055
- if (byteLength < GZIP_THRESHOLD) {
12056
- return [value, "raw"];
12057
- }
12058
- const buf = Buffer.from(jsonStr, "utf8");
12059
- const compressed = gzipSync(buf);
12060
- if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
12061
- return [compressed.toString("base64"), "gz64"];
12062
- }
12063
- return [value, "raw"];
12064
- }
12065
- function toBuffer(value) {
12066
- if (Buffer.isBuffer(value)) return value;
12067
- if (value instanceof Uint8Array) return Buffer.from(value);
12068
- if (typeof value === "string") return Buffer.from(value, "base64");
12069
- return Buffer.from(String(value), "utf8");
12070
- }
12071
- function deserializeRow(options) {
12072
- const { values, columns, targetDialect, targetVersion } = options;
12073
- const result = {};
12074
- for (let i = 0; i < columns.length; i++) {
12075
- const col = columns[i];
12076
- const value = values[i];
12077
- result[col.name] = deserializeValue(value, col, targetDialect, targetVersion);
12078
- }
12079
- return result;
12080
- }
12081
- function deserializeValue(value, column, targetDialect, targetVersion) {
12082
- if (value === null || value === void 0) {
12083
- return null;
12084
- }
12085
- if (isEncodedType(column.type)) {
12086
- const decoded = decodeTuple(value);
12087
- return convertEncodedForTarget(decoded, column.type, targetDialect, targetVersion);
12088
- }
12089
- return convertSimpleForTarget(value, column.type, targetDialect);
12090
- }
12091
- function decodeTuple(tuple) {
12092
- if (!Array.isArray(tuple) || tuple.length < 2) {
12093
- return tuple;
12094
- }
12095
- const [value, encoding] = tuple;
12096
- switch (encoding) {
12097
- case "raw":
12098
- return value;
12099
- case "b64":
12100
- return Buffer.from(value, "base64");
12101
- case "gz64": {
12102
- const compressed = Buffer.from(value, "base64");
12103
- const decompressed = gunzipSync(compressed);
12104
- const str = decompressed.toString("utf8");
12105
- if (/^[[{"\d]/.test(str)) {
12106
- return JSON.parse(str);
12107
- }
12108
- return decompressed;
12109
- }
12110
- default:
12111
- return value;
12112
- }
12113
- }
12114
- function convertEncodedForTarget(decoded, type, targetDialect, targetVersion) {
12115
- switch (type) {
12116
- case "json":
12117
- return convertJsonForTarget(decoded, targetDialect, targetVersion);
12118
- case "text":
12119
- return decoded;
12120
- case "binary":
12121
- return decoded;
12122
- case "vector":
12123
- return convertVectorForTarget(decoded, targetDialect, targetVersion);
12124
- case "array":
12125
- return convertArrayForTarget(decoded, targetDialect);
12126
- case "custom":
12127
- if (typeof decoded === "object") return JSON.stringify(decoded);
12128
- return decoded;
12129
- default:
12130
- return decoded;
12131
- }
12132
- }
12133
- function convertJsonForTarget(decoded, targetDialect, targetVersion) {
12134
- if (targetDialect === "mssql") {
12135
- const major = targetVersion?.major ?? 2022;
12136
- if (major < 2025) {
12137
- return typeof decoded === "string" ? decoded : JSON.stringify(decoded);
12138
- }
12139
- }
12140
- return decoded;
12141
- }
12142
- function convertVectorForTarget(decoded, targetDialect, targetVersion) {
12143
- const arr = Array.isArray(decoded) ? decoded : decoded;
12144
- if (targetDialect === "postgres") {
12145
- return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
12146
- }
12147
- if (targetDialect === "mysql") {
12148
- const major = targetVersion?.major ?? 8;
12149
- if (major >= 9) {
12150
- return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
12151
- }
12152
- return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
12153
- }
12154
- return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
12155
- }
12156
- function convertArrayForTarget(decoded, targetDialect) {
12157
- if (targetDialect === "postgres") {
12158
- return decoded;
12159
- }
12160
- return Array.isArray(decoded) ? JSON.stringify(decoded) : String(decoded);
12182
+
12183
+ // src/core/dt/index.ts
12184
+ var CONNECTION_WORKER = resolveWorker("connection");
12185
+ var COMPUTE_WORKER = resolveWorker("compute");
12186
+ function createDefaultComputePool() {
12187
+ return WorkerBridge.pool(COMPUTE_WORKER, {
12188
+ size: Math.max(1, availableParallelism() - 2)
12189
+ });
12161
12190
  }
12162
- function convertSimpleForTarget(value, type, targetDialect) {
12163
- switch (type) {
12164
- case "bigint":
12165
- if (typeof value === "string") {
12166
- const num = Number(value);
12167
- if (Number.isSafeInteger(num)) return num;
12168
- }
12169
- return value;
12170
- case "decimal":
12171
- return value;
12172
- case "bool":
12173
- if (targetDialect === "mssql") return value ? 1 : 0;
12174
- if (targetDialect === "mysql") return value ? 1 : 0;
12175
- return value;
12176
- case "timestamp":
12177
- if (typeof value === "string") return new Date(value);
12178
- return value;
12179
- case "date":
12180
- return value;
12181
- case "uuid":
12182
- return value;
12183
- case "int":
12184
- case "float":
12185
- return value;
12186
- case "string":
12187
- default:
12188
- return value;
12191
+ async function createDefaultConnectionBridge(dialect, connectionString) {
12192
+ const bridge = new WorkerBridge(CONNECTION_WORKER);
12193
+ const [, connectErr] = await attempt(
12194
+ () => bridge.request("connect", { dialect, connectionString })
12195
+ );
12196
+ if (connectErr) {
12197
+ await attempt(() => bridge.shutdown());
12198
+ return [null, connectErr];
12189
12199
  }
12200
+ return [bridge, null];
12190
12201
  }
12191
-
12192
- // src/core/dt/index.ts
12193
12202
  async function exportTable(options) {
12194
12203
  const startTime = Date.now();
12195
12204
  const { db, dialect, tableName, filepath, passphrase, schema: schemaName } = options;
@@ -12215,58 +12224,178 @@ async function exportTable(options) {
12215
12224
  if (openErr) {
12216
12225
  return [null, openErr];
12217
12226
  }
12218
- let offset = 0;
12227
+ let ownedConnectionBridge = null;
12228
+ let ownedComputePool = null;
12229
+ let connectionBridge = options.connectionBridge;
12230
+ let computePool = options.computePool;
12231
+ if (!connectionBridge && options.connectionString) {
12232
+ const [bridge, bridgeErr] = await createDefaultConnectionBridge(dialect, options.connectionString);
12233
+ if (bridgeErr) {
12234
+ return [null, bridgeErr];
12235
+ }
12236
+ connectionBridge = bridge;
12237
+ ownedConnectionBridge = bridge;
12238
+ }
12239
+ if (!computePool) {
12240
+ computePool = createDefaultComputePool();
12241
+ ownedComputePool = computePool;
12242
+ }
12243
+ const [result, workerErr] = await exportTableWithWorkers({
12244
+ writer,
12245
+ dtSchema,
12246
+ tableName,
12247
+ filepath,
12248
+ dialect,
12249
+ batchSize,
12250
+ connectionBridge,
12251
+ computePool,
12252
+ kyselyDb
12253
+ });
12254
+ if (ownedComputePool) {
12255
+ await attempt(() => ownedComputePool.shutdown());
12256
+ }
12257
+ if (ownedConnectionBridge) {
12258
+ await attempt(() => ownedConnectionBridge.shutdown());
12259
+ }
12260
+ if (workerErr) {
12261
+ return [null, workerErr];
12262
+ }
12263
+ const durationMs = Date.now() - startTime;
12264
+ observer.emit("dt:export:complete", {
12265
+ filepath,
12266
+ table: tableName,
12267
+ rowsWritten: result.rowsWritten,
12268
+ bytesWritten: result.bytesWritten,
12269
+ durationMs
12270
+ });
12271
+ return [result, null];
12272
+ }
12273
+ function getQuoteIdent(dialect) {
12274
+ if (dialect === "mssql") return (c) => `[${c}]`;
12275
+ if (dialect === "mysql") return (c) => `\`${c}\``;
12276
+ return (c) => `"${c}"`;
12277
+ }
12278
+ function buildBatchSql(dialect, tableName, columnList, orderCol, batchSize, offset) {
12279
+ if (dialect === "mssql") {
12280
+ return `SELECT ${columnList} FROM [${tableName}] ORDER BY ${orderCol} OFFSET ${offset} ROWS FETCH NEXT ${batchSize} ROWS ONLY`;
12281
+ }
12282
+ return `SELECT ${columnList} FROM "${tableName}" LIMIT ${batchSize} OFFSET ${offset}`;
12283
+ }
12284
+ async function exportTableWithWorkers(ctx) {
12285
+ const { writer, dtSchema, tableName, filepath, dialect, batchSize } = ctx;
12286
+ const { connectionBridge, computePool, kyselyDb } = ctx;
12287
+ const backpressureLimit = batchSize * 3;
12288
+ let totalRows = 0;
12289
+ if (connectionBridge) {
12290
+ const countSql = dialect === "mssql" ? `SELECT COUNT(*) AS cnt FROM [${tableName}]` : `SELECT COUNT(*) AS cnt FROM "${tableName}"`;
12291
+ const [countResult, countErr] = await attempt(
12292
+ () => connectionBridge.request("query", { sql: countSql })
12293
+ );
12294
+ if (countErr) {
12295
+ return [null, countErr];
12296
+ }
12297
+ const firstRow = countResult.rows[0];
12298
+ totalRows = Number(firstRow?.["cnt"] ?? 0);
12299
+ }
12219
12300
  const columns = dtSchema.columns.map((c) => c.name);
12220
- const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
12301
+ const quoteIdent4 = getQuoteIdent(dialect);
12221
12302
  const columnList = columns.map(quoteIdent4).join(", ");
12303
+ const orderCol = quoteIdent4(columns[0]);
12304
+ let globalIndex = 0;
12305
+ let loaded = 0;
12306
+ let processed = 0;
12307
+ let saved = 0;
12308
+ let inFlight = 0;
12309
+ let pipelineError = null;
12310
+ const orderBuffer = new OrderBuffer((values) => {
12311
+ writer.writeRow(values);
12312
+ saved++;
12313
+ inFlight--;
12314
+ observer.emit("dt:export:saved", { table: tableName, saved, totalRows });
12315
+ });
12316
+ let offset = 0;
12222
12317
  while (true) {
12223
- const [rows, fetchErr] = await attempt(() => {
12224
- if (dialect === "mssql") {
12225
- const orderCol = quoteIdent4(columns[0]);
12318
+ while (inFlight >= backpressureLimit) {
12319
+ await new Promise((resolve2) => setTimeout(resolve2, 1));
12320
+ }
12321
+ let batchRows;
12322
+ if (connectionBridge) {
12323
+ const batchSql = buildBatchSql(dialect, tableName, columnList, orderCol, batchSize, offset);
12324
+ const [queryResult, queryErr] = await attempt(
12325
+ () => connectionBridge.request("query", { sql: batchSql })
12326
+ );
12327
+ if (queryErr) {
12328
+ pipelineError = queryErr;
12329
+ break;
12330
+ }
12331
+ if (queryResult.error) {
12332
+ pipelineError = new Error(queryResult.error);
12333
+ break;
12334
+ }
12335
+ batchRows = queryResult.rows;
12336
+ } else {
12337
+ const [rows, fetchErr] = await attempt(() => {
12338
+ if (dialect === "mssql") {
12339
+ return sql`
12340
+ SELECT ${sql.raw(columnList)}
12341
+ FROM ${sql.table(tableName)}
12342
+ ORDER BY ${sql.raw(orderCol)}
12343
+ OFFSET ${offset} ROWS
12344
+ FETCH NEXT ${batchSize} ROWS ONLY
12345
+ `.execute(kyselyDb);
12346
+ }
12226
12347
  return sql`
12227
12348
  SELECT ${sql.raw(columnList)}
12228
12349
  FROM ${sql.table(tableName)}
12229
- ORDER BY ${sql.raw(orderCol)}
12230
- OFFSET ${offset} ROWS
12231
- FETCH NEXT ${batchSize} ROWS ONLY
12350
+ LIMIT ${batchSize}
12351
+ OFFSET ${offset}
12232
12352
  `.execute(kyselyDb);
12353
+ });
12354
+ if (fetchErr) {
12355
+ pipelineError = fetchErr;
12356
+ break;
12233
12357
  }
12234
- return sql`
12235
- SELECT ${sql.raw(columnList)}
12236
- FROM ${sql.table(tableName)}
12237
- LIMIT ${batchSize}
12238
- OFFSET ${offset}
12239
- `.execute(kyselyDb);
12240
- });
12241
- if (fetchErr) {
12242
- return [null, fetchErr];
12243
- }
12244
- if (rows.rows.length === 0) break;
12245
- for (const row of rows.rows) {
12246
- const values = serializeRow({ row, columns: dtSchema.columns });
12247
- writer.writeRow(values);
12358
+ batchRows = rows.rows;
12359
+ }
12360
+ if (batchRows.length === 0) break;
12361
+ loaded += batchRows.length;
12362
+ observer.emit("dt:export:loaded", { table: tableName, loaded, totalRows });
12363
+ for (const row of batchRows) {
12364
+ const rowIndex = globalIndex++;
12365
+ inFlight++;
12366
+ computePool.request("serialize", {
12367
+ row,
12368
+ columns: dtSchema.columns,
12369
+ index: rowIndex
12370
+ }).then((result) => {
12371
+ processed++;
12372
+ observer.emit("dt:export:processed", { table: tableName, processed, totalRows });
12373
+ if (result.error) {
12374
+ pipelineError = pipelineError ?? new Error(result.error);
12375
+ return;
12376
+ }
12377
+ orderBuffer.add(result.index, result.values);
12378
+ });
12248
12379
  }
12249
- offset += rows.rows.length;
12380
+ offset += batchRows.length;
12250
12381
  observer.emit("dt:export:progress", {
12251
12382
  filepath,
12252
12383
  table: tableName,
12253
12384
  rowsWritten: writer.rowsWritten,
12254
12385
  bytesWritten: writer.bytesWritten
12255
12386
  });
12256
- if (rows.rows.length < batchSize) break;
12387
+ if (batchRows.length < batchSize) break;
12388
+ }
12389
+ while (inFlight > 0) {
12390
+ await new Promise((resolve2) => setTimeout(resolve2, 1));
12391
+ }
12392
+ if (pipelineError) {
12393
+ return [null, pipelineError];
12257
12394
  }
12258
12395
  const [, closeErr] = await attempt(() => writer.close());
12259
12396
  if (closeErr) {
12260
12397
  return [null, closeErr];
12261
12398
  }
12262
- const durationMs = Date.now() - startTime;
12263
- observer.emit("dt:export:complete", {
12264
- filepath,
12265
- table: tableName,
12266
- rowsWritten: writer.rowsWritten,
12267
- bytesWritten: writer.bytesWritten,
12268
- durationMs
12269
- });
12270
12399
  return [{ rowsWritten: writer.rowsWritten, bytesWritten: writer.bytesWritten }, null];
12271
12400
  }
12272
12401
  async function importDtFile(options) {
@@ -12326,70 +12455,158 @@ async function importDtFile(options) {
12326
12455
  return [null, err];
12327
12456
  }
12328
12457
  }
12458
+ let ownedComputePool = null;
12459
+ let computePool = options.computePool;
12460
+ if (!computePool) {
12461
+ computePool = createDefaultComputePool();
12462
+ ownedComputePool = computePool;
12463
+ }
12464
+ const [result, workerErr] = await importFileWithWorkers({
12465
+ reader,
12466
+ dtSchema,
12467
+ tableName,
12468
+ filepath,
12469
+ dialect,
12470
+ version,
12471
+ batchSize,
12472
+ onConflict: options.onConflict ?? "fail",
12473
+ computePool,
12474
+ kyselyDb
12475
+ });
12476
+ if (ownedComputePool) {
12477
+ await attempt(() => ownedComputePool.shutdown());
12478
+ }
12479
+ if (workerErr) {
12480
+ reader.close();
12481
+ observer.emit("error", { source: "dt:import", error: workerErr, context: { filepath, table: tableName } });
12482
+ return [null, workerErr];
12483
+ }
12484
+ reader.close();
12485
+ const durationMs = Date.now() - startTime;
12486
+ observer.emit("dt:import:complete", {
12487
+ filepath,
12488
+ table: tableName,
12489
+ rowsImported: result.rowsImported,
12490
+ rowsSkipped: result.rowsSkipped,
12491
+ durationMs
12492
+ });
12493
+ return [result, null];
12494
+ }
12495
+ async function importFileWithWorkers(ctx) {
12496
+ const { reader, dtSchema, tableName, filepath, dialect, version, batchSize } = ctx;
12497
+ const { onConflict, computePool, kyselyDb } = ctx;
12329
12498
  let rowsImported = 0;
12330
12499
  let rowsSkipped = 0;
12331
- let batch = [];
12332
- for await (const values of reader.rows()) {
12333
- const row = deserializeRow({
12334
- values,
12335
- columns: dtSchema.columns,
12336
- targetDialect: dialect,
12337
- targetVersion: version
12338
- });
12339
- batch.push(row);
12340
- if (batch.length >= batchSize) {
12341
- const columnNames = dtSchema.columns.map((c) => c.name);
12342
- const [batchResult, insertErr] = await insertImportBatch(
12343
- kyselyDb,
12344
- tableName,
12345
- batch,
12346
- options.onConflict ?? "fail",
12347
- dialect,
12348
- columnNames
12349
- );
12350
- if (insertErr) {
12351
- reader.close();
12352
- observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
12353
- return [null, insertErr];
12354
- }
12355
- rowsImported += batchResult.inserted + batchResult.updated;
12356
- rowsSkipped += batchResult.skipped;
12357
- batch = [];
12358
- observer.emit("dt:import:progress", {
12359
- filepath,
12360
- table: tableName,
12361
- rowsImported,
12362
- rowsSkipped
12363
- });
12364
- }
12365
- }
12366
- if (batch.length > 0) {
12367
- const columnNames = dtSchema.columns.map((c) => c.name);
12500
+ let loaded = 0;
12501
+ let processed = 0;
12502
+ let saved = 0;
12503
+ let globalIndex = 0;
12504
+ let inFlight = 0;
12505
+ let pipelineError = null;
12506
+ let totalRows = 0;
12507
+ let insertBatch2 = [];
12508
+ const columnNames = dtSchema.columns.map((c) => c.name);
12509
+ const flushInsertBatch = async () => {
12510
+ if (insertBatch2.length === 0) return null;
12368
12511
  const [batchResult, insertErr] = await insertImportBatch(
12369
12512
  kyselyDb,
12370
12513
  tableName,
12371
- batch,
12372
- options.onConflict ?? "fail",
12514
+ insertBatch2,
12515
+ onConflict,
12373
12516
  dialect,
12374
12517
  columnNames
12375
12518
  );
12376
12519
  if (insertErr) {
12377
- reader.close();
12378
- observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
12379
- return [null, insertErr];
12520
+ return insertErr;
12380
12521
  }
12381
12522
  rowsImported += batchResult.inserted + batchResult.updated;
12382
12523
  rowsSkipped += batchResult.skipped;
12383
- }
12384
- reader.close();
12385
- const durationMs = Date.now() - startTime;
12386
- observer.emit("dt:import:complete", {
12387
- filepath,
12388
- table: tableName,
12389
- rowsImported,
12390
- rowsSkipped,
12391
- durationMs
12524
+ insertBatch2 = [];
12525
+ observer.emit("dt:import:progress", {
12526
+ filepath,
12527
+ table: tableName,
12528
+ rowsImported,
12529
+ rowsSkipped
12530
+ });
12531
+ return null;
12532
+ };
12533
+ const orderBuffer = new OrderBuffer((record) => {
12534
+ insertBatch2.push(record);
12535
+ saved++;
12536
+ inFlight--;
12537
+ observer.emit("dt:import:saved", { table: tableName, saved, totalRows });
12392
12538
  });
12539
+ let readBatch = [];
12540
+ for await (const values of reader.rows()) {
12541
+ readBatch.push(values);
12542
+ loaded++;
12543
+ totalRows = Math.max(totalRows, loaded);
12544
+ if (readBatch.length >= batchSize) {
12545
+ observer.emit("dt:import:loaded", { table: tableName, loaded, totalRows });
12546
+ for (const rowValues of readBatch) {
12547
+ const rowIndex = globalIndex++;
12548
+ inFlight++;
12549
+ computePool.request("deserialize", {
12550
+ values: rowValues,
12551
+ columns: dtSchema.columns,
12552
+ targetDialect: dialect,
12553
+ targetVersion: version ? `${version.major}.${version.minor}` : void 0,
12554
+ index: rowIndex
12555
+ }).then((result) => {
12556
+ processed++;
12557
+ observer.emit("dt:import:processed", { table: tableName, processed, totalRows });
12558
+ if (result.error) {
12559
+ pipelineError = pipelineError ?? new Error(result.error);
12560
+ return;
12561
+ }
12562
+ orderBuffer.add(result.index, result.record);
12563
+ });
12564
+ }
12565
+ readBatch = [];
12566
+ while (inFlight > 0) {
12567
+ await new Promise((resolve2) => setTimeout(resolve2, 1));
12568
+ }
12569
+ if (pipelineError) {
12570
+ return [null, pipelineError];
12571
+ }
12572
+ const flushErr = await flushInsertBatch();
12573
+ if (flushErr) {
12574
+ return [null, flushErr];
12575
+ }
12576
+ }
12577
+ }
12578
+ if (readBatch.length > 0) {
12579
+ observer.emit("dt:import:loaded", { table: tableName, loaded, totalRows });
12580
+ for (const rowValues of readBatch) {
12581
+ const rowIndex = globalIndex++;
12582
+ inFlight++;
12583
+ computePool.request("deserialize", {
12584
+ values: rowValues,
12585
+ columns: dtSchema.columns,
12586
+ targetDialect: dialect,
12587
+ targetVersion: version ? `${version.major}.${version.minor}` : void 0,
12588
+ index: rowIndex
12589
+ }).then((result) => {
12590
+ processed++;
12591
+ observer.emit("dt:import:processed", { table: tableName, processed, totalRows });
12592
+ if (result.error) {
12593
+ pipelineError = pipelineError ?? new Error(result.error);
12594
+ return;
12595
+ }
12596
+ orderBuffer.add(result.index, result.record);
12597
+ });
12598
+ }
12599
+ while (inFlight > 0) {
12600
+ await new Promise((resolve2) => setTimeout(resolve2, 1));
12601
+ }
12602
+ if (pipelineError) {
12603
+ return [null, pipelineError];
12604
+ }
12605
+ const flushErr = await flushInsertBatch();
12606
+ if (flushErr) {
12607
+ return [null, flushErr];
12608
+ }
12609
+ }
12393
12610
  return [{ rowsImported, rowsSkipped }, null];
12394
12611
  }
12395
12612
  async function insertImportBatch(db, table, rows, onConflict, dialect, columns) {