@spfn/core 0.2.0-beta.53 → 0.2.0-beta.54
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/{boss-Cxqc-Oiw.d.ts → boss-CEik0yq-.d.ts} +7 -0
- package/dist/cache/index.js +10 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/config/index.d.ts +213 -0
- package/dist/config/index.js +43 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +57 -0
- package/dist/db/index.js +55 -8
- package/dist/db/index.js.map +1 -1
- package/dist/define-middleware-DuXD8Hvu.d.ts +167 -0
- package/dist/env/index.js +4 -3
- package/dist/env/index.js.map +1 -1
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.js +20 -2
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +41 -16
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/event/ws/index.js +75 -16
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.d.ts +2 -2
- package/dist/job/index.js +4 -4
- package/dist/job/index.js.map +1 -1
- package/dist/middleware/index.d.ts +64 -2
- package/dist/middleware/index.js +1030 -96
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/server.js +17 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +3 -165
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +125 -39
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-C2Ag5-s8.d.ts → token-manager-jKD_EsSE.d.ts} +0 -1
- package/dist/{types-VpVQIsyB.d.ts → types-BFB72jbM.d.ts} +1 -1
- package/dist/{types-CKsmzaB8.d.ts → types-DVjf37yO.d.ts} +37 -1
- package/package.json +6 -6
package/dist/db/index.d.ts
CHANGED
|
@@ -81,6 +81,13 @@ interface PoolConfig {
|
|
|
81
81
|
max: number;
|
|
82
82
|
/** Idle connection timeout in seconds */
|
|
83
83
|
idleTimeout: number;
|
|
84
|
+
/**
|
|
85
|
+
* Maximum connections for the read-replica pool. Defaults to `max`. Set
|
|
86
|
+
* separately (DB_POOL_READ_MAX) so a master-replica app can size the two
|
|
87
|
+
* pools independently and keep `write.max + read.max` under the server's
|
|
88
|
+
* `max_connections` (otherwise each process opens up to 2 × max).
|
|
89
|
+
*/
|
|
90
|
+
readMax?: number;
|
|
84
91
|
}
|
|
85
92
|
/**
|
|
86
93
|
* Retry configuration for exponential backoff algorithm
|
|
@@ -1039,6 +1046,15 @@ interface TransactionalOptions {
|
|
|
1039
1046
|
* ```
|
|
1040
1047
|
*/
|
|
1041
1048
|
timeout?: number;
|
|
1049
|
+
/**
|
|
1050
|
+
* Idle-in-transaction timeout in milliseconds — Postgres reclaims the pooled
|
|
1051
|
+
* connection if the transaction sits idle (e.g. the handler awaits external
|
|
1052
|
+
* I/O) longer than this. A backstop against pool starvation, not a license
|
|
1053
|
+
* to do non-DB work inside a transaction. `0` disables it.
|
|
1054
|
+
*
|
|
1055
|
+
* @default 30000 (30s) or TRANSACTION_IDLE_TIMEOUT environment variable
|
|
1056
|
+
*/
|
|
1057
|
+
idleTimeout?: number;
|
|
1042
1058
|
}
|
|
1043
1059
|
/**
|
|
1044
1060
|
* Transaction middleware for Hono routes
|
|
@@ -1129,6 +1145,21 @@ interface RunInTransactionOptions {
|
|
|
1129
1145
|
* ```
|
|
1130
1146
|
*/
|
|
1131
1147
|
timeout?: number;
|
|
1148
|
+
/**
|
|
1149
|
+
* Idle-in-transaction timeout in milliseconds (root transactions only).
|
|
1150
|
+
*
|
|
1151
|
+
* Sets PostgreSQL `idle_in_transaction_session_timeout`: if the transaction
|
|
1152
|
+
* sits open without running a query for longer than this — e.g. while the
|
|
1153
|
+
* handler awaits external I/O inside the transaction — Postgres terminates
|
|
1154
|
+
* the session and rolls back, reclaiming the pooled connection instead of
|
|
1155
|
+
* letting one stuck request hold it (and its row locks) indefinitely.
|
|
1156
|
+
*
|
|
1157
|
+
* Do not put external I/O inside a transaction; this is a backstop, not a
|
|
1158
|
+
* license. `0` disables it.
|
|
1159
|
+
*
|
|
1160
|
+
* @default 30000 (30s) or TRANSACTION_IDLE_TIMEOUT environment variable
|
|
1161
|
+
*/
|
|
1162
|
+
idleTimeout?: number;
|
|
1132
1163
|
/**
|
|
1133
1164
|
* Context string for logging (e.g., 'migration:add-user', 'script:cleanup')
|
|
1134
1165
|
* @default 'transaction'
|
|
@@ -1633,6 +1664,32 @@ declare abstract class BaseRepository<TSchema extends Record<string, unknown> =
|
|
|
1633
1664
|
limit?: number;
|
|
1634
1665
|
offset?: number;
|
|
1635
1666
|
}): Promise<T['$inferSelect'][]>;
|
|
1667
|
+
/**
|
|
1668
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
1669
|
+
*
|
|
1670
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
1671
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
1672
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
1673
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
1674
|
+
* auto-increment id or a ULID).
|
|
1675
|
+
*
|
|
1676
|
+
* @example
|
|
1677
|
+
* ```typescript
|
|
1678
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
1679
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
1680
|
+
* cursorColumn: users.id,
|
|
1681
|
+
* after: page1.at(-1)?.id,
|
|
1682
|
+
* limit: 20,
|
|
1683
|
+
* });
|
|
1684
|
+
* ```
|
|
1685
|
+
*/
|
|
1686
|
+
protected _findManyKeyset<T extends PgTable>(table: T, options: {
|
|
1687
|
+
cursorColumn: PgColumn;
|
|
1688
|
+
limit: number;
|
|
1689
|
+
after?: string | number | bigint | Date;
|
|
1690
|
+
order?: 'asc' | 'desc';
|
|
1691
|
+
where?: Record<string, any> | SQL | undefined;
|
|
1692
|
+
}): Promise<T['$inferSelect'][]>;
|
|
1636
1693
|
/**
|
|
1637
1694
|
* Create a new record
|
|
1638
1695
|
*
|
package/dist/db/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } f
|
|
|
11
11
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
12
12
|
import { createMiddleware } from 'hono/factory';
|
|
13
13
|
import { randomUUID } from 'crypto';
|
|
14
|
-
import { sql, count as count$1,
|
|
14
|
+
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
15
15
|
|
|
16
16
|
// src/db/manager/factory.ts
|
|
17
17
|
function parseUniqueViolation(message) {
|
|
@@ -344,9 +344,12 @@ function parseEnvBoolean(key, defaultValue) {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
function getPoolConfig(options) {
|
|
347
|
+
const max = options?.max ?? parseEnvNumber("DB_POOL_MAX", 20, 10);
|
|
347
348
|
return {
|
|
348
|
-
max
|
|
349
|
-
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20)
|
|
349
|
+
max,
|
|
350
|
+
idleTimeout: options?.idleTimeout ?? parseEnvNumber("DB_POOL_IDLE_TIMEOUT", 30, 20),
|
|
351
|
+
// Defaults to the write `max`; DB_POOL_READ_MAX lets ops size the replica pool separately
|
|
352
|
+
readMax: options?.readMax ?? parseEnvNumber("DB_POOL_READ_MAX", max, max)
|
|
350
353
|
};
|
|
351
354
|
}
|
|
352
355
|
function getRetryConfig() {
|
|
@@ -415,8 +418,9 @@ async function createWriteReadClients(writeUrl, readUrl, poolConfig, retryConfig
|
|
|
415
418
|
dbLogger2.error("Failed to connect to write database", errorObj);
|
|
416
419
|
throw new Error(`Write database connection failed: ${errorObj.message}`, { cause: error });
|
|
417
420
|
}
|
|
421
|
+
const readPoolConfig = { ...poolConfig, max: poolConfig.readMax ?? poolConfig.max };
|
|
418
422
|
try {
|
|
419
|
-
readClient = await createDatabaseConnection(readUrl,
|
|
423
|
+
readClient = await createDatabaseConnection(readUrl, readPoolConfig, retryConfig);
|
|
420
424
|
return {
|
|
421
425
|
write: drizzle(writeClient),
|
|
422
426
|
read: drizzle(readClient),
|
|
@@ -1412,6 +1416,7 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1412
1416
|
context = "transaction"
|
|
1413
1417
|
} = options;
|
|
1414
1418
|
const timeout = options.timeout ?? defaultTimeout;
|
|
1419
|
+
const idleTimeout = options.idleTimeout ?? env.TRANSACTION_IDLE_TIMEOUT;
|
|
1415
1420
|
const txId = `tx_${randomUUID()}`;
|
|
1416
1421
|
const validateAndThrow = (condition, message, logMessage, metadata) => {
|
|
1417
1422
|
if (condition) {
|
|
@@ -1489,6 +1494,9 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1489
1494
|
if (timeout > 0 && !isNested) {
|
|
1490
1495
|
await tx.execute(sql.raw(`SET LOCAL statement_timeout = ${timeout}`));
|
|
1491
1496
|
}
|
|
1497
|
+
if (idleTimeout > 0 && !isNested) {
|
|
1498
|
+
await tx.execute(sql.raw(`SET LOCAL idle_in_transaction_session_timeout = ${idleTimeout}`));
|
|
1499
|
+
}
|
|
1492
1500
|
return await runWithTransaction(tx, txId, async () => {
|
|
1493
1501
|
const innerResult = await callback(tx);
|
|
1494
1502
|
if (!isNested) {
|
|
@@ -1507,7 +1515,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1507
1515
|
txId,
|
|
1508
1516
|
context,
|
|
1509
1517
|
duration: `${duration}ms`,
|
|
1510
|
-
threshold: `${slowThreshold}ms
|
|
1518
|
+
threshold: `${slowThreshold}ms`,
|
|
1519
|
+
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."
|
|
1511
1520
|
});
|
|
1512
1521
|
} else {
|
|
1513
1522
|
txLogger2.debug("Transaction committed", {
|
|
@@ -1546,7 +1555,8 @@ async function runInTransaction(callback, options = {}) {
|
|
|
1546
1555
|
duration: `${duration}ms`,
|
|
1547
1556
|
threshold: `${slowThreshold}ms`,
|
|
1548
1557
|
error: error instanceof Error ? error.message : String(error),
|
|
1549
|
-
errorType: error instanceof Error ? error.name : "Unknown"
|
|
1558
|
+
errorType: error instanceof Error ? error.name : "Unknown",
|
|
1559
|
+
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."
|
|
1550
1560
|
});
|
|
1551
1561
|
} else {
|
|
1552
1562
|
txLogger2.error("Transaction rolled back", {
|
|
@@ -1896,14 +1906,51 @@ var BaseRepository = class {
|
|
|
1896
1906
|
const orderByArray = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
1897
1907
|
query = query.orderBy(...orderByArray);
|
|
1898
1908
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1909
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
1910
|
+
const requestedLimit = options?.limit && options.limit > 0 ? options.limit : void 0;
|
|
1911
|
+
const effectiveLimit = maxRows > 0 ? Math.min(requestedLimit ?? maxRows, maxRows) : requestedLimit;
|
|
1912
|
+
if (effectiveLimit) {
|
|
1913
|
+
query = query.limit(effectiveLimit);
|
|
1901
1914
|
}
|
|
1902
1915
|
if (options?.offset) {
|
|
1903
1916
|
query = query.offset(options.offset);
|
|
1904
1917
|
}
|
|
1905
1918
|
return query;
|
|
1906
1919
|
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Keyset (cursor) pagination — O(limit) instead of OFFSET's O(offset).
|
|
1922
|
+
*
|
|
1923
|
+
* Pages by a strictly-ordered, unique column instead of a numeric offset, so a
|
|
1924
|
+
* deep page doesn't scan and discard everything before it. Pass the cursor
|
|
1925
|
+
* column's value from the last row of the previous page as `after`; omit it for
|
|
1926
|
+
* the first page. The column must be unique and the sole sort key (e.g. an
|
|
1927
|
+
* auto-increment id or a ULID).
|
|
1928
|
+
*
|
|
1929
|
+
* @example
|
|
1930
|
+
* ```typescript
|
|
1931
|
+
* const page1 = await this._findManyKeyset(users, { cursorColumn: users.id, limit: 20 });
|
|
1932
|
+
* const page2 = await this._findManyKeyset(users, {
|
|
1933
|
+
* cursorColumn: users.id,
|
|
1934
|
+
* after: page1.at(-1)?.id,
|
|
1935
|
+
* limit: 20,
|
|
1936
|
+
* });
|
|
1937
|
+
* ```
|
|
1938
|
+
*/
|
|
1939
|
+
async _findManyKeyset(table, options) {
|
|
1940
|
+
const { cursorColumn, after, order = "asc", where } = options;
|
|
1941
|
+
const maxRows = env.DB_MAX_ROWS;
|
|
1942
|
+
const limit = maxRows > 0 ? Math.min(options.limit, maxRows) : options.limit;
|
|
1943
|
+
const baseWhere = where ? isSQLWrapper(where) ? where : buildWhereFromObject(table, where) : void 0;
|
|
1944
|
+
const cursorPredicate = after !== void 0 ? order === "desc" ? lt(cursorColumn, after) : gt(cursorColumn, after) : void 0;
|
|
1945
|
+
const whereClause = baseWhere && cursorPredicate ? and(baseWhere, cursorPredicate) : cursorPredicate ?? baseWhere;
|
|
1946
|
+
let query = this.readDb.select().from(table);
|
|
1947
|
+
if (whereClause) {
|
|
1948
|
+
query = query.where(whereClause);
|
|
1949
|
+
}
|
|
1950
|
+
query = query.orderBy(order === "desc" ? desc(cursorColumn) : asc(cursorColumn));
|
|
1951
|
+
query = query.limit(limit);
|
|
1952
|
+
return query;
|
|
1953
|
+
}
|
|
1907
1954
|
/**
|
|
1908
1955
|
* Create a new record
|
|
1909
1956
|
*
|