@spfn/core 0.2.0-beta.6 → 0.2.0-beta.64

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 (73) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +181 -1281
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +415 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-gXhgctn6.d.ts} +40 -0
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +55 -8
  10. package/dist/codegen/index.js +183 -7
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +585 -6
  13. package/dist/config/index.js +116 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/db/index.d.ts +270 -4
  16. package/dist/db/index.js +404 -60
  17. package/dist/db/index.js.map +1 -1
  18. package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
  19. package/dist/env/index.d.ts +26 -2
  20. package/dist/env/index.js +15 -5
  21. package/dist/env/index.js.map +1 -1
  22. package/dist/env/loader.d.ts +26 -19
  23. package/dist/env/loader.js +32 -25
  24. package/dist/env/loader.js.map +1 -1
  25. package/dist/errors/index.d.ts +10 -0
  26. package/dist/errors/index.js +20 -2
  27. package/dist/errors/index.js.map +1 -1
  28. package/dist/event/index.d.ts +33 -3
  29. package/dist/event/index.js +24 -3
  30. package/dist/event/index.js.map +1 -1
  31. package/dist/event/sse/client.d.ts +42 -3
  32. package/dist/event/sse/client.js +128 -45
  33. package/dist/event/sse/client.js.map +1 -1
  34. package/dist/event/sse/index.d.ts +12 -5
  35. package/dist/event/sse/index.js +271 -32
  36. package/dist/event/sse/index.js.map +1 -1
  37. package/dist/event/ws/client.d.ts +59 -0
  38. package/dist/event/ws/client.js +273 -0
  39. package/dist/event/ws/client.js.map +1 -0
  40. package/dist/event/ws/index.d.ts +94 -0
  41. package/dist/event/ws/index.js +272 -0
  42. package/dist/event/ws/index.js.map +1 -0
  43. package/dist/job/index.d.ts +2 -2
  44. package/dist/job/index.js +155 -42
  45. package/dist/job/index.js.map +1 -1
  46. package/dist/logger/index.d.ts +5 -0
  47. package/dist/logger/index.js +14 -0
  48. package/dist/logger/index.js.map +1 -1
  49. package/dist/middleware/index.d.ts +243 -2
  50. package/dist/middleware/index.js +1323 -13
  51. package/dist/middleware/index.js.map +1 -1
  52. package/dist/nextjs/index.d.ts +2 -2
  53. package/dist/nextjs/index.js +77 -31
  54. package/dist/nextjs/index.js.map +1 -1
  55. package/dist/nextjs/server.d.ts +53 -23
  56. package/dist/nextjs/server.js +197 -66
  57. package/dist/nextjs/server.js.map +1 -1
  58. package/dist/route/index.d.ts +138 -146
  59. package/dist/route/index.js +238 -22
  60. package/dist/route/index.js.map +1 -1
  61. package/dist/security/index.d.ts +83 -0
  62. package/dist/security/index.js +173 -0
  63. package/dist/security/index.js.map +1 -0
  64. package/dist/server/index.d.ts +450 -17
  65. package/dist/server/index.js +1756 -277
  66. package/dist/server/index.js.map +1 -1
  67. package/dist/{router-Di7ENoah.d.ts → token-manager-jKD_EsSE.d.ts} +121 -1
  68. package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
  69. package/dist/types-BFB72jbM.d.ts +282 -0
  70. package/dist/types-DVjf37yO.d.ts +205 -0
  71. package/docs/file-upload.md +717 -0
  72. package/package.json +235 -208
  73. package/dist/types-B-e_f2dQ.d.ts +0 -121
package/dist/db/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import { env } from '@spfn/core/config';
3
3
  import { logger } from '@spfn/core/logger';
4
+ import net from 'net';
4
5
  import postgres from 'postgres';
5
6
  import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, DatabaseError } from '@spfn/core/errors';
6
7
  import { parseNumber, parseBoolean } from '@spfn/core/env';
7
8
  import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
8
9
  import { join, dirname, basename } from 'path';
9
- import { bigserial, timestamp, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
10
+ import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
10
11
  import { AsyncLocalStorage } from 'async_hooks';
11
12
  import { createMiddleware } from 'hono/factory';
12
13
  import { randomUUID } from 'crypto';
13
- import { count as count$1, sql, eq, and } from 'drizzle-orm';
14
+ import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
14
15
 
15
16
  // src/db/manager/factory.ts
16
17
  function parseUniqueViolation(message) {
@@ -129,7 +130,16 @@ function fromPostgresError(error) {
129
130
  }
130
131
 
131
132
  // src/db/manager/connection.ts
133
+ function getSocketFamily() {
134
+ const family = process.env.DATABASE_SOCKET_FAMILY;
135
+ if (family === "4") return 4;
136
+ if (family === "6") return 6;
137
+ return void 0;
138
+ }
132
139
  var dbLogger = logger.child("@spfn/core:database");
140
+ function isTransactionPooler(connectionString) {
141
+ return /:6543(?:[/?]|$)|[?&]pgbouncer=true\b|pooler\.supabase\.com/i.test(connectionString);
142
+ }
133
143
  var DEFAULT_CONNECT_TIMEOUT = 10;
134
144
  function delay(ms) {
135
145
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -189,10 +199,23 @@ async function createDatabaseConnection(connectionString, poolConfig, retryConfi
189
199
  let client;
190
200
  for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
191
201
  try {
202
+ const socketFamily = getSocketFamily();
203
+ const prepare = poolConfig.prepare ?? !isTransactionPooler(connectionString);
192
204
  client = postgres(connectionString, {
193
205
  max: poolConfig.max,
194
206
  idle_timeout: poolConfig.idleTimeout,
195
- connect_timeout: DEFAULT_CONNECT_TIMEOUT
207
+ connect_timeout: DEFAULT_CONNECT_TIMEOUT,
208
+ prepare,
209
+ ...socketFamily && {
210
+ socket: ({ host, port }) => new Promise((resolve, reject) => {
211
+ const socket = new net.Socket();
212
+ socket.on("error", reject);
213
+ socket.connect(
214
+ { port: port[0], host: host[0], family: socketFamily },
215
+ () => resolve(socket)
216
+ );
217
+ })
218
+ }
196
219
  });
197
220
  await client`SELECT 1 as test`;
198
221
  if (attempt > 0) {
@@ -325,10 +348,27 @@ function parseEnvBoolean(key, defaultValue) {
325
348
  throw new Error(`${key}: ${message}`);
326
349
  }
327
350
  }
351
+ function parseEnvBooleanOptional(key) {
352
+ const value = process.env[key];
353
+ if (value === void 0) {
354
+ return void 0;
355
+ }
356
+ try {
357
+ return parseBoolean(value);
358
+ } catch (error) {
359
+ const message = error instanceof Error ? error.message : String(error);
360
+ throw new Error(`${key}: ${message}`);
361
+ }
362
+ }
328
363
  function getPoolConfig(options) {
364
+ const max = options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10);
329
365
  return {
330
- max: options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10),
331
- idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20)
366
+ max,
367
+ idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20),
368
+ // Defaults to the write `max`; DB_POOL_READ_MAX lets ops size the replica pool separately
369
+ readMax: options?.readMax ?? parseEnvNumber("DB_POOL_READ_MAX", max, max),
370
+ // Left undefined for connection-layer auto-detection unless explicitly overridden.
371
+ prepare: options?.prepare ?? parseEnvBooleanOptional("SPFN_DB_PREPARE")
332
372
  };
333
373
  }
334
374
  function getRetryConfig() {
@@ -397,8 +437,9 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
397
437
  dbLogger2.error("Failed to connect to write database", errorObj);
398
438
  throw new Error(`Write database connection failed: ${errorObj.message}`, { cause: error });
399
439
  }
440
+ const readPoolConfig = { ...poolConfig, max: poolConfig.readMax ?? poolConfig.max };
400
441
  try {
401
- readClient = await createDatabaseConnection(readUrl, poolConfig, retryConfig);
442
+ readClient = await createDatabaseConnection(readUrl, readPoolConfig, retryConfig);
402
443
  return {
403
444
  write: drizzle(writeClient),
404
445
  read: drizzle(readClient),
@@ -502,7 +543,20 @@ var setHealthCheckInterval = (interval) => {
502
543
  var setMonitoringConfig = (config) => {
503
544
  globalThis.__SPFN_DB_MONITORING__ = config;
504
545
  };
546
+ var getInitOptions = () => globalThis.__SPFN_DB_INIT_OPTIONS__;
547
+ var setInitOptions = (options) => {
548
+ globalThis.__SPFN_DB_INIT_OPTIONS__ = options;
549
+ };
550
+ var getIsClosing = () => globalThis.__SPFN_DB_CLOSING__ === true;
551
+ var setIsClosing = (closing) => {
552
+ globalThis.__SPFN_DB_CLOSING__ = closing;
553
+ };
505
554
  var dbLogger3 = logger.child("@spfn/core:database");
555
+ var CLIENT_CLOSE_TIMEOUT = 5;
556
+ var isReconnecting = false;
557
+ function isReconnectingNow() {
558
+ return isReconnecting;
559
+ }
506
560
  async function testDatabaseConnection(db) {
507
561
  await db.execute("SELECT 1");
508
562
  }
@@ -514,8 +568,17 @@ async function performHealthCheck(getDatabase2) {
514
568
  await testDatabaseConnection(read);
515
569
  }
516
570
  }
517
- async function reconnectAndRestore(options, closeDatabase2) {
518
- await closeDatabase2();
571
+ async function closeClient(client) {
572
+ try {
573
+ await client.end({ timeout: CLIENT_CLOSE_TIMEOUT });
574
+ } catch {
575
+ }
576
+ }
577
+ async function reconnectAndRestore(options) {
578
+ if (getIsClosing()) {
579
+ dbLogger3.debug("reconnectAndRestore aborted: database is closing");
580
+ return false;
581
+ }
519
582
  const result = await createDatabaseFromEnv(options);
520
583
  if (!result.write) {
521
584
  return false;
@@ -524,15 +587,33 @@ async function reconnectAndRestore(options, closeDatabase2) {
524
587
  if (result.read && result.read !== result.write) {
525
588
  await testDatabaseConnection(result.read);
526
589
  }
590
+ if (getIsClosing()) {
591
+ dbLogger3.warn("reconnectAndRestore: close started mid-rebuild, discarding new pool");
592
+ if (result.writeClient) {
593
+ await closeClient(result.writeClient);
594
+ }
595
+ if (result.readClient && result.readClient !== result.writeClient) {
596
+ await closeClient(result.readClient);
597
+ }
598
+ return false;
599
+ }
600
+ const oldWriteClient = getWriteClient();
601
+ const oldReadClient = getReadClient();
527
602
  setWriteInstance(result.write);
528
603
  setReadInstance(result.read);
529
604
  setWriteClient(result.writeClient);
530
605
  setReadClient(result.readClient);
531
606
  const monConfig = buildMonitoringConfig(options?.monitoring);
532
607
  setMonitoringConfig(monConfig);
608
+ if (oldWriteClient) {
609
+ closeClient(oldWriteClient);
610
+ }
611
+ if (oldReadClient && oldReadClient !== oldWriteClient) {
612
+ closeClient(oldReadClient);
613
+ }
533
614
  return true;
534
615
  }
535
- function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
616
+ function startHealthCheck(config, options, getDatabase2) {
536
617
  const healthCheck = getHealthCheckInterval();
537
618
  if (healthCheck) {
538
619
  dbLogger3.debug("Health check already running");
@@ -543,48 +624,77 @@ function startHealthCheck(config, options, getDatabase2, closeDatabase2) {
543
624
  reconnect: config.reconnect
544
625
  });
545
626
  const interval = setInterval(async () => {
627
+ if (isReconnecting) {
628
+ dbLogger3.debug("Health check skipped: reconnection in progress");
629
+ return;
630
+ }
546
631
  try {
547
632
  await performHealthCheck(getDatabase2);
548
633
  } catch (error) {
549
634
  const message = error instanceof Error ? error.message : "Unknown error";
550
635
  dbLogger3.error("Database health check failed", { error: message });
551
636
  if (config.reconnect) {
552
- await attemptReconnection(config, options, closeDatabase2);
637
+ await attemptReconnection(config, options, "health_check_failed");
553
638
  }
554
639
  }
555
640
  }, config.interval);
556
641
  setHealthCheckInterval(interval);
557
642
  }
558
- async function attemptReconnection(config, options, closeDatabase2) {
643
+ async function triggerForceReconnect(reason) {
644
+ if (!getWriteInstance()) {
645
+ dbLogger3.warn("Force reconnect skipped: database not initialized", { reason });
646
+ return false;
647
+ }
648
+ if (getIsClosing()) {
649
+ dbLogger3.debug("Force reconnect skipped: database is closing", { reason });
650
+ return false;
651
+ }
652
+ const options = getInitOptions();
653
+ const config = buildHealthCheckConfig(options?.healthCheck);
654
+ dbLogger3.warn("Force reconnect triggered", { reason });
655
+ return await attemptReconnection(config, options, reason);
656
+ }
657
+ async function attemptReconnection(config, options, reason) {
658
+ if (isReconnecting) {
659
+ dbLogger3.debug("Reconnection coalesced: attempt already in progress", { reason });
660
+ return false;
661
+ }
662
+ isReconnecting = true;
559
663
  dbLogger3.warn("Attempting database reconnection", {
664
+ reason,
560
665
  maxRetries: config.maxRetries,
561
666
  retryInterval: `${config.retryInterval}ms`
562
667
  });
563
- for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
564
- try {
565
- dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
566
- if (attempt > 1) {
567
- await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
668
+ try {
669
+ for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
670
+ try {
671
+ dbLogger3.debug(`Reconnection attempt ${attempt}/${config.maxRetries}`);
672
+ if (attempt > 1) {
673
+ await new Promise((resolve) => setTimeout(resolve, config.retryInterval));
674
+ }
675
+ const success = await reconnectAndRestore(options);
676
+ if (success) {
677
+ dbLogger3.info("Database reconnection successful", { attempt });
678
+ return true;
679
+ } else {
680
+ dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
681
+ }
682
+ } catch (error) {
683
+ const message = error instanceof Error ? error.message : "Unknown error";
684
+ dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
685
+ error: message,
686
+ attempt,
687
+ maxRetries: config.maxRetries
688
+ });
568
689
  }
569
- const success = await reconnectAndRestore(options, closeDatabase2);
570
- if (success) {
571
- dbLogger3.info("Database reconnection successful", { attempt });
572
- return;
573
- } else {
574
- dbLogger3.error(`Reconnection attempt ${attempt} failed: No write database instance created`);
690
+ if (attempt === config.maxRetries) {
691
+ dbLogger3.error("Max reconnection attempts reached, will retry on next health check");
575
692
  }
576
- } catch (error) {
577
- const message = error instanceof Error ? error.message : "Unknown error";
578
- dbLogger3.error(`Reconnection attempt ${attempt} failed`, {
579
- error: message,
580
- attempt,
581
- maxRetries: config.maxRetries
582
- });
583
- }
584
- if (attempt === config.maxRetries) {
585
- dbLogger3.error("Max reconnection attempts reached, giving up");
586
693
  }
694
+ } finally {
695
+ isReconnecting = false;
587
696
  }
697
+ return true;
588
698
  }
589
699
  function stopHealthCheck() {
590
700
  const healthCheck = getHealthCheckInterval();
@@ -593,6 +703,7 @@ function stopHealthCheck() {
593
703
  setHealthCheckInterval(void 0);
594
704
  dbLogger3.info("Database health check stopped");
595
705
  }
706
+ isReconnecting = false;
596
707
  }
597
708
 
598
709
  // src/db/manager/manager.ts
@@ -604,7 +715,6 @@ var STACK_TRACE_PATTERNS = {
604
715
  withoutParens: /at (.+):(\d+):(\d+)/
605
716
  };
606
717
  var initPromise = null;
607
- var isClosing = false;
608
718
  async function cleanupDatabaseConnections(writeClient, readClient) {
609
719
  const cleanupPromises = [];
610
720
  if (writeClient) {
@@ -705,7 +815,7 @@ function setDatabase(write, read) {
705
815
  setReadInstance(read ?? write);
706
816
  }
707
817
  async function initDatabase(options) {
708
- if (isClosing) {
818
+ if (getIsClosing()) {
709
819
  throw new Error("Cannot initialize database while closing");
710
820
  }
711
821
  const writeInst = getWriteInstance();
@@ -727,7 +837,7 @@ async function initDatabase(options) {
727
837
  const message = error instanceof Error ? error.message : "Unknown error";
728
838
  throw new Error(`Database connection test failed: ${message}`);
729
839
  }
730
- if (isClosing) {
840
+ if (getIsClosing()) {
731
841
  dbLogger4.warn("Database closed during initialization, cleaning up...");
732
842
  await cleanupDatabaseConnections(result.writeClient, result.readClient);
733
843
  throw new Error("Database closed during initialization");
@@ -736,13 +846,14 @@ async function initDatabase(options) {
736
846
  setReadInstance(result.read);
737
847
  setWriteClient(result.writeClient);
738
848
  setReadClient(result.readClient);
849
+ setInitOptions(options);
739
850
  const hasReplica = result.read && result.read !== result.write;
740
851
  dbLogger4.info(
741
852
  hasReplica ? "Database connected (Primary + Replica)" : "Database connected"
742
853
  );
743
854
  const healthCheckConfig = buildHealthCheckConfig(options?.healthCheck);
744
855
  if (healthCheckConfig.enabled) {
745
- startHealthCheck(healthCheckConfig, options, getDatabase, closeDatabase);
856
+ startHealthCheck(healthCheckConfig, options, getDatabase);
746
857
  }
747
858
  const monConfig = buildMonitoringConfig(options?.monitoring);
748
859
  setMonitoringConfig(monConfig);
@@ -760,11 +871,11 @@ async function initDatabase(options) {
760
871
  return await initPromise;
761
872
  }
762
873
  async function closeDatabase() {
763
- if (isClosing) {
874
+ if (getIsClosing()) {
764
875
  dbLogger4.debug("Database close already in progress");
765
876
  return;
766
877
  }
767
- isClosing = true;
878
+ setIsClosing(true);
768
879
  if (initPromise) {
769
880
  dbLogger4.debug("Waiting for database initialization to complete before closing...");
770
881
  try {
@@ -777,7 +888,7 @@ async function closeDatabase() {
777
888
  const readInst = getReadInstance();
778
889
  if (!writeInst && !readInst) {
779
890
  dbLogger4.debug("No database connections to close");
780
- isClosing = false;
891
+ setIsClosing(false);
781
892
  return;
782
893
  }
783
894
  try {
@@ -799,9 +910,13 @@ async function closeDatabase() {
799
910
  setWriteClient(void 0);
800
911
  setReadClient(void 0);
801
912
  setMonitoringConfig(void 0);
802
- isClosing = false;
913
+ setInitOptions(void 0);
914
+ setIsClosing(false);
803
915
  }
804
916
  }
917
+ async function forceReconnectDatabase(reason = "manual") {
918
+ return await triggerForceReconnect(reason);
919
+ }
805
920
  function getDatabaseInfo() {
806
921
  const writeInst = getWriteInstance();
807
922
  const readInst = getReadInstance();
@@ -811,7 +926,123 @@ function getDatabaseInfo() {
811
926
  isReplica: !!(readInst && readInst !== writeInst)
812
927
  };
813
928
  }
814
- var INDEX_FILE_PATTERNS = [
929
+ var dbLogger5 = logger.child("@spfn/core:database");
930
+ var POSTGRES_JS_CONNECTION_CODES = /* @__PURE__ */ new Set([
931
+ "CONNECTION_ENDED",
932
+ "CONNECTION_CLOSED",
933
+ "CONNECTION_DESTROYED",
934
+ "CONNECT_TIMEOUT",
935
+ "CONNECTION_CONNECT_TIMEOUT"
936
+ ]);
937
+ var NODE_NET_ERROR_CODES = /* @__PURE__ */ new Set([
938
+ "ECONNRESET",
939
+ "ECONNREFUSED",
940
+ "EPIPE",
941
+ "ETIMEDOUT",
942
+ "EHOSTUNREACH",
943
+ "ENETUNREACH",
944
+ "ENOTFOUND"
945
+ ]);
946
+ function isConnectionSqlState(code) {
947
+ if (code.startsWith("08")) return true;
948
+ if (code === "53300") return true;
949
+ if (code === "57P01" || code === "57P02" || code === "57P03") return true;
950
+ return false;
951
+ }
952
+ function* unwrap(error) {
953
+ const seen = /* @__PURE__ */ new Set();
954
+ const stack = [error];
955
+ while (stack.length > 0) {
956
+ const current = stack.pop();
957
+ if (!current || typeof current !== "object" || seen.has(current)) {
958
+ continue;
959
+ }
960
+ seen.add(current);
961
+ const obj = current;
962
+ yield obj;
963
+ for (const key of ["cause", "original", "error", "err", "inner"]) {
964
+ const nested = obj[key];
965
+ if (nested && typeof nested === "object" && !seen.has(nested)) {
966
+ stack.push(nested);
967
+ }
968
+ }
969
+ }
970
+ }
971
+ function isConnectionLevelError(error) {
972
+ if (!error) return false;
973
+ if (error instanceof ConnectionError) return true;
974
+ for (const candidate of unwrap(error)) {
975
+ if (candidate instanceof ConnectionError) return true;
976
+ const code = candidate.code;
977
+ if (typeof code === "string") {
978
+ if (POSTGRES_JS_CONNECTION_CODES.has(code)) return true;
979
+ if (NODE_NET_ERROR_CODES.has(code)) return true;
980
+ if (isConnectionSqlState(code)) return true;
981
+ }
982
+ }
983
+ return false;
984
+ }
985
+ var DEFAULT_THRESHOLD = 3;
986
+ var DEFAULT_WINDOW_MS = 1e4;
987
+ var MIN_WINDOW_MS = 1e3;
988
+ function readPositiveIntEnv(key, defaultValue, min) {
989
+ const raw = process.env[key];
990
+ if (raw === void 0) return defaultValue;
991
+ try {
992
+ return parseNumber(raw, { min, integer: true });
993
+ } catch {
994
+ return defaultValue;
995
+ }
996
+ }
997
+ var ERROR_THRESHOLD = readPositiveIntEnv("DB_RECONNECT_ERROR_THRESHOLD", DEFAULT_THRESHOLD, 1);
998
+ var ERROR_WINDOW_MS = readPositiveIntEnv("DB_RECONNECT_ERROR_WINDOW_MS", DEFAULT_WINDOW_MS, MIN_WINDOW_MS);
999
+ var errorTimestamps = [];
1000
+ var reportedErrors = /* @__PURE__ */ new WeakSet();
1001
+ function resetConnectionErrorCounter() {
1002
+ errorTimestamps.length = 0;
1003
+ }
1004
+ function checkAndMarkReported(error) {
1005
+ let alreadySeen = false;
1006
+ const toMark = [];
1007
+ for (const candidate of unwrap(error)) {
1008
+ if (reportedErrors.has(candidate)) {
1009
+ alreadySeen = true;
1010
+ }
1011
+ toMark.push(candidate);
1012
+ }
1013
+ if (alreadySeen) return true;
1014
+ for (const obj of toMark) {
1015
+ reportedErrors.add(obj);
1016
+ }
1017
+ return false;
1018
+ }
1019
+ function reportDatabaseError(error) {
1020
+ try {
1021
+ if (!isConnectionLevelError(error)) return;
1022
+ if (isReconnectingNow()) return;
1023
+ if (checkAndMarkReported(error)) return;
1024
+ const now = Date.now();
1025
+ errorTimestamps.push(now);
1026
+ const cutoff = now - ERROR_WINDOW_MS;
1027
+ while (errorTimestamps.length > 0 && errorTimestamps[0] < cutoff) {
1028
+ errorTimestamps.shift();
1029
+ }
1030
+ if (errorTimestamps.length < ERROR_THRESHOLD) return;
1031
+ errorTimestamps.length = 0;
1032
+ dbLogger5.error("Connection-error threshold crossed, forcing pool rebuild", {
1033
+ threshold: ERROR_THRESHOLD,
1034
+ windowMs: ERROR_WINDOW_MS
1035
+ });
1036
+ triggerForceReconnect("query_error_threshold").catch((err) => {
1037
+ const message = err instanceof Error ? err.message : String(err);
1038
+ dbLogger5.error("Forced reconnect after error threshold failed", { error: message });
1039
+ });
1040
+ } catch (err) {
1041
+ const message = err instanceof Error ? err.message : String(err);
1042
+ dbLogger5.debug("reportDatabaseError itself threw, ignoring", { error: message });
1043
+ }
1044
+ }
1045
+ var BARREL_FILE_PATTERNS = [
815
1046
  "/index",
816
1047
  "/index.ts",
817
1048
  "/index.js",
@@ -819,11 +1050,14 @@ var INDEX_FILE_PATTERNS = [
819
1050
  "\\index",
820
1051
  "\\index.ts",
821
1052
  "\\index.js",
822
- "\\index.mjs"
1053
+ "\\index.mjs",
1054
+ "\\config.ts",
1055
+ "\\config.js",
1056
+ "\\config.mjs"
823
1057
  ];
824
1058
  var SUPPORTED_EXTENSIONS = [".ts", ".js", ".mjs"];
825
- function isIndexFile(filePath) {
826
- return INDEX_FILE_PATTERNS.some((pattern) => filePath.endsWith(pattern));
1059
+ function isBarrelFile(filePath) {
1060
+ return BARREL_FILE_PATTERNS.some((pattern) => filePath.endsWith(pattern));
827
1061
  }
828
1062
  function isAbsolutePath(path) {
829
1063
  if (path.startsWith("/")) return true;
@@ -833,8 +1067,8 @@ function hasSupportedExtension(filePath) {
833
1067
  if (filePath.endsWith(".d.ts")) return false;
834
1068
  return SUPPORTED_EXTENSIONS.some((ext) => filePath.endsWith(ext));
835
1069
  }
836
- function filterIndexFiles(files) {
837
- return files.filter((file) => !isIndexFile(file));
1070
+ function filterBarrelFiles(files) {
1071
+ return files.filter((file) => !isBarrelFile(file));
838
1072
  }
839
1073
  function scanDirectoryRecursive(dir, extension) {
840
1074
  const files = [];
@@ -943,7 +1177,7 @@ function discoverPackageSchemas(cwd) {
943
1177
  for (const schema of packageSchemas) {
944
1178
  const absolutePath = join(pkgPath, schema);
945
1179
  const expandedFiles = expandGlobPattern(absolutePath);
946
- const schemaFiles = filterIndexFiles(expandedFiles);
1180
+ const schemaFiles = filterBarrelFiles(expandedFiles);
947
1181
  schemas.push(...schemaFiles);
948
1182
  }
949
1183
  }
@@ -1005,7 +1239,10 @@ function getDrizzleConfig(options = {}) {
1005
1239
  schema: schema2,
1006
1240
  out,
1007
1241
  dialect,
1008
- dbCredentials: getDbCredentials(dialect, databaseUrl)
1242
+ dbCredentials: getDbCredentials(dialect, databaseUrl),
1243
+ migrations: {
1244
+ prefix: options.migrationPrefix ?? "timestamp"
1245
+ }
1009
1246
  };
1010
1247
  }
1011
1248
  const userSchema = options.schema ?? "./src/server/entities/**/*.ts";
@@ -1018,7 +1255,7 @@ function getDrizzleConfig(options = {}) {
1018
1255
  for (const schema2 of allSchemas) {
1019
1256
  const absoluteSchema = isAbsolutePath(schema2) ? schema2 : join(cwd, schema2);
1020
1257
  const expanded = expandGlobPattern(absoluteSchema);
1021
- const filtered = filterIndexFiles(expanded);
1258
+ const filtered = filterBarrelFiles(expanded);
1022
1259
  expandedFiles.push(...filtered);
1023
1260
  }
1024
1261
  allSchemas = expandedFiles;
@@ -1037,7 +1274,10 @@ function getDrizzleConfig(options = {}) {
1037
1274
  out,
1038
1275
  dialect,
1039
1276
  dbCredentials: getDbCredentials(dialect, databaseUrl),
1040
- schemaFilter
1277
+ schemaFilter,
1278
+ migrations: {
1279
+ prefix: options.migrationPrefix ?? "timestamp"
1280
+ }
1041
1281
  };
1042
1282
  }
1043
1283
  function getDbCredentials(dialect, url) {
@@ -1066,13 +1306,15 @@ function generateDrizzleConfigFile(options = {}) {
1066
1306
  ]` : `'${normalizeSchemaPath(config.schema)}'`;
1067
1307
  const schemaFilterLine = config.schemaFilter && config.schemaFilter.length > 0 ? `
1068
1308
  schemaFilter: ${JSON.stringify(config.schemaFilter)},` : "";
1309
+ const migrationsLine = config.migrations ? `
1310
+ migrations: ${JSON.stringify(config.migrations)},` : "";
1069
1311
  return `import { defineConfig } from 'drizzle-kit';
1070
1312
 
1071
1313
  export default defineConfig({
1072
1314
  schema: ${schemaValue},
1073
1315
  out: '${config.out}',
1074
1316
  dialect: '${config.dialect}',
1075
- dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}
1317
+ dbCredentials: ${JSON.stringify(config.dbCredentials, null, 4)},${schemaFilterLine}${migrationsLine}
1076
1318
  });
1077
1319
  `;
1078
1320
  }
@@ -1086,10 +1328,10 @@ function timestamps() {
1086
1328
  };
1087
1329
  }
1088
1330
  function foreignKey(name, reference, options) {
1089
- return bigserial(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
1331
+ return bigint(`${name}_id`, { mode: "number" }).notNull().references(reference, { onDelete: options?.onDelete ?? "cascade" });
1090
1332
  }
1091
1333
  function optionalForeignKey(name, reference, options) {
1092
- return bigserial(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
1334
+ return bigint(`${name}_id`, { mode: "number" }).references(reference, { onDelete: options?.onDelete ?? "set null" });
1093
1335
  }
1094
1336
  function uuid() {
1095
1337
  return uuid$1("id").defaultRandom().primaryKey();
@@ -1168,7 +1410,20 @@ function runWithTransaction(tx, txId, callback) {
1168
1410
  } else {
1169
1411
  txLogger.debug("Root transaction context set", { txId, level: newLevel });
1170
1412
  }
1171
- return asyncContext.run({ tx, txId, level: newLevel }, callback);
1413
+ const afterCommitCallbacks = existingContext ? existingContext.afterCommitCallbacks : [];
1414
+ return asyncContext.run({ tx, txId, level: newLevel, afterCommitCallbacks }, callback);
1415
+ }
1416
+ function onAfterCommit(callback) {
1417
+ const context = getTransactionContext();
1418
+ if (!context) {
1419
+ Promise.resolve().then(callback).catch((err) => {
1420
+ txLogger.error("afterCommit callback failed (no transaction)", {
1421
+ error: err instanceof Error ? err.message : String(err)
1422
+ });
1423
+ });
1424
+ return;
1425
+ }
1426
+ context.afterCommitCallbacks.push(callback);
1172
1427
  }
1173
1428
  var MAX_TIMEOUT_MS = 2147483647;
1174
1429
  var txLogger2 = logger.child("@spfn/core:transaction");
@@ -1180,6 +1435,7 @@ async function runInTransaction(callback, options = {}) {
1180
1435
  context = "transaction"
1181
1436
  } = options;
1182
1437
  const timeout = options.timeout ?? defaultTimeout;
1438
+ const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
1183
1439
  const txId = `tx_${randomUUID()}`;
1184
1440
  const validateAndThrow = (condition, message, logMessage, metadata) => {
1185
1441
  if (condition) {
@@ -1220,6 +1476,24 @@ async function runInTransaction(callback, options = {}) {
1220
1476
  "Timeout exceeds maximum",
1221
1477
  { txId, context, timeout, maxTimeout: MAX_TIMEOUT_MS }
1222
1478
  );
1479
+ validateAndThrow(
1480
+ !Number.isInteger(idleTimeout),
1481
+ `Invalid idleTimeout value: ${idleTimeout}. Must be an integer.`,
1482
+ "Invalid idleTimeout type",
1483
+ { txId, context, idleTimeout }
1484
+ );
1485
+ validateAndThrow(
1486
+ idleTimeout < 0,
1487
+ `Invalid idleTimeout value: ${idleTimeout}. Must be non-negative (0 to disable).`,
1488
+ "Invalid idleTimeout range",
1489
+ { txId, context, idleTimeout }
1490
+ );
1491
+ validateAndThrow(
1492
+ idleTimeout > MAX_TIMEOUT_MS,
1493
+ `Invalid idleTimeout value: ${idleTimeout}. Maximum is ${MAX_TIMEOUT_MS}ms.`,
1494
+ "idleTimeout exceeds maximum",
1495
+ { txId, context, idleTimeout, maxTimeout: MAX_TIMEOUT_MS }
1496
+ );
1223
1497
  const writeDb = getDatabase("write");
1224
1498
  if (!writeDb) {
1225
1499
  const error = new TransactionError({
@@ -1251,13 +1525,24 @@ async function runInTransaction(callback, options = {}) {
1251
1525
  txLogger2.debug("Transaction started", { txId, context });
1252
1526
  }
1253
1527
  const startTime = Date.now();
1528
+ let afterCommitCallbacks = [];
1254
1529
  try {
1255
1530
  const result = await writeDb.transaction(async (tx) => {
1256
1531
  if (timeout > 0 && !isNested) {
1257
1532
  await tx.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
1258
1533
  }
1534
+ if (idleTimeout > 0 && !isNested) {
1535
+ await tx.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
1536
+ }
1259
1537
  return await runWithTransaction(tx, txId, async () => {
1260
- return await callback(tx);
1538
+ const innerResult = await callback(tx);
1539
+ if (!isNested) {
1540
+ const ctx = getTransactionContext();
1541
+ if (ctx) {
1542
+ afterCommitCallbacks = [...ctx.afterCommitCallbacks];
1543
+ }
1544
+ }
1545
+ return innerResult;
1261
1546
  });
1262
1547
  });
1263
1548
  const duration = Date.now() - startTime;
@@ -1267,7 +1552,8 @@ async function runInTransaction(callback, options = {}) {
1267
1552
  txId,
1268
1553
  context,
1269
1554
  duration: `${duration}ms`,
1270
- threshold: `${slowThreshold}ms`
1555
+ threshold: `${slowThreshold}ms`,
1556
+ hint: "A transaction holds a pooled connection (and row locks) for its whole duration. If this is slow because of non-DB work (external API, etc.) inside the transaction, move that work out \u2014 it starves the connection pool."
1271
1557
  });
1272
1558
  } else {
1273
1559
  txLogger2.debug("Transaction committed", {
@@ -1277,6 +1563,24 @@ async function runInTransaction(callback, options = {}) {
1277
1563
  });
1278
1564
  }
1279
1565
  }
1566
+ if (!isNested && afterCommitCallbacks.length > 0) {
1567
+ if (enableLogging) {
1568
+ txLogger2.debug("Executing afterCommit callbacks", {
1569
+ txId,
1570
+ context,
1571
+ count: afterCommitCallbacks.length
1572
+ });
1573
+ }
1574
+ for (const cb of afterCommitCallbacks) {
1575
+ Promise.resolve().then(cb).catch((err) => {
1576
+ txLogger2.error("afterCommit callback failed", {
1577
+ txId,
1578
+ context,
1579
+ error: err instanceof Error ? err.message : String(err)
1580
+ });
1581
+ });
1582
+ }
1583
+ }
1280
1584
  return result;
1281
1585
  } catch (error) {
1282
1586
  const duration = Date.now() - startTime;
@@ -1288,7 +1592,8 @@ async function runInTransaction(callback, options = {}) {
1288
1592
  duration: `${duration}ms`,
1289
1593
  threshold: `${slowThreshold}ms`,
1290
1594
  error: error instanceof Error ? error.message : String(error),
1291
- errorType: error instanceof Error ? error.name : "Unknown"
1595
+ errorType: error instanceof Error ? error.name : "Unknown",
1596
+ hint: "If the error is an idle-in-transaction timeout, the transaction held a pooled connection while awaiting non-DB work (external API, etc.). Move that work out of the transaction."
1292
1597
  });
1293
1598
  } else {
1294
1599
  txLogger2.error("Transaction rolled back", {
@@ -1323,6 +1628,7 @@ function Transactional(options = {}) {
1323
1628
  }
1324
1629
  );
1325
1630
  } catch (error) {
1631
+ reportDatabaseError(error);
1326
1632
  if (error instanceof DatabaseError) {
1327
1633
  throw error;
1328
1634
  }
@@ -1570,6 +1876,7 @@ var BaseRepository = class {
1570
1876
  try {
1571
1877
  return await queryFn();
1572
1878
  } catch (error) {
1879
+ reportDatabaseError(error);
1573
1880
  const err = error instanceof Error ? error : new Error(String(error));
1574
1881
  const repositoryName = this.constructor.name;
1575
1882
  throw new RepositoryError(
@@ -1636,14 +1943,51 @@ var BaseRepository = class {
1636
1943
  const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
1637
1944
  query = query.orderBy(...orderByArray);
1638
1945
  }
1639
- if (options?.limit) {
1640
- query = query.limit(options.limit);
1946
+ const maxRows = env.DB_MAX_ROWS;
1947
+ const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
1948
+ const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
1949
+ if (effectiveLimit) {
1950
+ query = query.limit(effectiveLimit);
1641
1951
  }
1642
1952
  if (options?.offset) {
1643
1953
  query = query.offset(options.offset);
1644
1954
  }
1645
1955
  return query;
1646
1956
  }
1957
+ /**
1958
+ * Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
1959
+ *
1960
+ * Pages by a strictly-ordered, unique column instead of a numeric offset, so a
1961
+ * deep page doesn't scan and discard everything before it. Pass the cursor
1962
+ * column's value from the last row of the previous page as `after`; omit it for
1963
+ * the first page. The column must be unique and the sole sort key (e.g. an
1964
+ * auto-increment id or a ULID).
1965
+ *
1966
+ * @example
1967
+ * ```typescript
1968
+ * const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
1969
+ * const page2 = await this._findManyKeyset(users, {
1970
+ * cursorColumn: users.id,
1971
+ * after: page1.at(-1)?.id,
1972
+ * limit: 20,
1973
+ * });
1974
+ * ```
1975
+ */
1976
+ async _findManyKeyset(table, options) {
1977
+ const { cursorColumn, after, order = "asc", where } = options;
1978
+ const maxRows = env.DB_MAX_ROWS;
1979
+ const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
1980
+ const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
1981
+ const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
1982
+ const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
1983
+ let query = this.readDb.select().from(table);
1984
+ if (whereClause) {
1985
+ query = query.where(whereClause);
1986
+ }
1987
+ query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
1988
+ query = query.limit(limit);
1989
+ return query;
1990
+ }
1647
1991
  /**
1648
1992
  * Create a new record
1649
1993
  *
@@ -1822,6 +2166,6 @@ var BaseRepository = class {
1822
2166
  }
1823
2167
  };
1824
2168
 
1825
- export { BaseRepository, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, count, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, enumText, findMany, findOne, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, id, initDatabase, optionalForeignKey, packageNameToSchema, publishingFields, runWithTransaction, setDatabase, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
2169
+ export { BaseRepository, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, count, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, enumText, findMany, findOne, forceReconnectDatabase, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, id, initDatabase, isConnectionLevelError, onAfterCommit, optionalForeignKey, packageNameToSchema, publishingFields, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
1826
2170
  //# sourceMappingURL=index.js.map
1827
2171
  //# sourceMappingURL=index.js.map