@mandujs/core 0.53.0 → 0.53.2
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/package.json +38 -2
- package/src/a11y/run-audit.ts +6 -6
- package/src/brain/adapters/oauth-flow.ts +1 -1
- package/src/brain/architecture/analyzer.ts +0 -2
- package/src/brain/credentials.ts +2 -2
- package/src/bundler/__tests__/build-runner.ts +13 -1
- package/src/bundler/__tests__/extended-watch.test.ts +3 -2
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -12
- package/src/bundler/build.test.ts +30 -33
- package/src/bundler/build.ts +56 -27
- package/src/bundler/dev.ts +44 -39
- package/src/bundler/prerender.ts +63 -0
- package/src/bundler/safe-build.test.ts +2 -1
- package/src/bundler/types.ts +13 -6
- package/src/change/history.ts +1 -1
- package/src/change/snapshot.ts +18 -9
- package/src/client/Link.tsx +9 -9
- package/src/client/globals.ts +3 -2
- package/src/client/hooks.ts +0 -4
- package/src/client/hydrate.ts +1 -1
- package/src/client/router.ts +12 -12
- package/src/client/rpc.ts +1 -1
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-sse.ts +5 -5
- package/src/config/mcp-status.ts +0 -1
- package/src/config/validate.ts +4 -4
- package/src/config/watcher.ts +1 -1
- package/src/content/collection.ts +8 -12
- package/src/content/content-layer.ts +5 -5
- package/src/content/data-store.ts +2 -2
- package/src/content/loader-context.ts +0 -1
- package/src/content/loaders/file.ts +1 -1
- package/src/content/loaders/glob.ts +4 -4
- package/src/content/loaders/types.ts +1 -1
- package/src/content/meta-store.ts +1 -1
- package/src/content/types.ts +1 -1
- package/src/contract/define.ts +11 -12
- package/src/contract/index.ts +3 -3
- package/src/contract/protection.ts +2 -2
- package/src/contract/types.ts +1 -1
- package/src/contract/validator.ts +0 -1
- package/src/db/__tests__/db.test.ts +4 -7
- package/src/db/index.ts +138 -51
- package/src/db/migrations/index.ts +3 -0
- package/src/db/migrations/lock.ts +78 -23
- package/src/db/migrations/runner.ts +118 -101
- package/src/deploy/cache.ts +1 -0
- package/src/design/extract.ts +2 -1
- package/src/design/patch.ts +0 -1
- package/src/devtools/ai/mcp-connector.ts +24 -24
- package/src/devtools/client/components/kitchen-root.tsx +1 -1
- package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
- package/src/devtools/client/state-manager.ts +1 -1
- package/src/devtools/init.ts +2 -3
- package/src/devtools/server/source-context.ts +1 -1
- package/src/devtools/worker/redaction-worker.ts +8 -4
- package/src/devtools/worker/worker-manager.ts +4 -3
- package/src/error/classifier.ts +1 -1
- package/src/error/domains.ts +1 -1
- package/src/filling/sse.ts +5 -5
- package/src/generator/generate.ts +2 -2
- package/src/guard/analyzer.ts +1 -1
- package/src/guard/ast-analyzer.ts +7 -7
- package/src/guard/auto-correct.ts +2 -2
- package/src/guard/config-guard.ts +0 -2
- package/src/guard/contract-guard.ts +1 -1
- package/src/guard/decision-memory.ts +2 -2
- package/src/guard/design-inline-class.ts +2 -1
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -41
- package/src/guard/negotiation.ts +3 -3
- package/src/guard/reporter.ts +1 -4
- package/src/guard/semantic-slots.ts +2 -2
- package/src/guard/suggestions.ts +2 -2
- package/src/guard/watcher.ts +5 -4
- package/src/index.ts +5 -5
- package/src/intent/index.ts +1 -1
- package/src/kitchen/api/agent-devtools-api.ts +544 -0
- package/src/kitchen/api/contract-api.ts +1 -1
- package/src/kitchen/api/file-api.ts +3 -3
- package/src/kitchen/kitchen-handler.ts +63 -38
- package/src/kitchen/kitchen-ui.ts +346 -62
- package/src/lockfile/validate.ts +0 -1
- package/src/openapi/generator.ts +2 -1
- package/src/openapi/openapi.test.ts +43 -14
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/registry.ts +1 -2
- package/src/plugins/types.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -15
- package/src/resource/ddl/__tests__/emit.test.ts +24 -0
- package/src/resource/ddl/diff.ts +0 -1
- package/src/resource/ddl/emit.ts +12 -1
- package/src/resource/generator-repo.ts +40 -20
- package/src/resource/generator.ts +1 -1
- package/src/resource/generators/contract.ts +1 -1
- package/src/resource/schema.ts +2 -1
- package/src/router/fs-routes.ts +6 -6
- package/src/router/fs-scanner.ts +1 -1
- package/src/runtime/env.ts +1 -1
- package/src/runtime/image-handler.ts +2 -1
- package/src/runtime/middleware.ts +2 -1
- package/src/runtime/server.ts +67 -60
- package/src/runtime/ssr.ts +3 -3
- package/src/runtime/streaming-ssr.ts +25 -27
- package/src/scheduler/validate.ts +1 -1
- package/src/seo/integration/ssr.ts +2 -3
- package/src/seo/render/opengraph.ts +1 -1
- package/src/seo/resolve/index.ts +0 -4
- package/src/slot/corrector.ts +1 -1
- package/src/slot/validator.ts +4 -4
- package/src/watcher/reporter.ts +1 -1
- package/src/watcher/watcher.ts +19 -19
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
import type { Loader, GlobLoaderOptions, ParsedMarkdown } from "./types";
|
|
14
14
|
import type { LoaderContext, DataEntry } from "../types";
|
|
15
|
-
import {
|
|
15
|
+
import { ParseError } from "../types";
|
|
16
16
|
import { generateFileDigest, combineDigests } from "../digest";
|
|
17
|
-
import { inferParser
|
|
17
|
+
import { inferParser } from "./types";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
20
|
|
|
@@ -108,7 +108,7 @@ export function glob(options: GlobLoaderOptions): Loader {
|
|
|
108
108
|
name: "glob",
|
|
109
109
|
|
|
110
110
|
async load(context: LoaderContext): Promise<void> {
|
|
111
|
-
const { store, config, logger, parseData, renderMarkdown, watcher } = context;
|
|
111
|
+
const { store, config, logger, parseData: _parseData, renderMarkdown: _renderMarkdown, watcher } = context;
|
|
112
112
|
|
|
113
113
|
// 기본 디렉토리
|
|
114
114
|
const baseDir = base
|
|
@@ -181,7 +181,7 @@ async function processFile(
|
|
|
181
181
|
context: LoaderContext,
|
|
182
182
|
filePath: string,
|
|
183
183
|
id: string,
|
|
184
|
-
|
|
184
|
+
_baseDir: string
|
|
185
185
|
): Promise<void> {
|
|
186
186
|
const { store, parseData, renderMarkdown, logger } = context;
|
|
187
187
|
|
package/src/content/types.ts
CHANGED
package/src/contract/define.ts
CHANGED
|
@@ -27,16 +27,15 @@
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
getZodTypeName,
|
|
33
|
-
getZodInnerType,
|
|
34
|
-
getZodArrayElementType,
|
|
35
|
-
getZodObjectShape,
|
|
36
|
-
getZodEnumValues,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from './zod-utils';
|
|
30
|
+
import type { ZodType } from 'zod';
|
|
31
|
+
import {
|
|
32
|
+
getZodTypeName,
|
|
33
|
+
getZodInnerType,
|
|
34
|
+
getZodArrayElementType,
|
|
35
|
+
getZodObjectShape,
|
|
36
|
+
getZodEnumValues,
|
|
37
|
+
isZodRequired,
|
|
38
|
+
} from './zod-utils';
|
|
40
39
|
|
|
41
40
|
// ============================================================================
|
|
42
41
|
// Types
|
|
@@ -167,7 +166,7 @@ export function generateApiHandler<T extends ContractDefinition>(
|
|
|
167
166
|
throw new Error(`Endpoint "${String(endpointName)}" not found in contract`);
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
const { method, path, input,
|
|
169
|
+
const { method, path, input, description } = endpoint;
|
|
171
170
|
|
|
172
171
|
return `
|
|
173
172
|
import { Mandu } from '@mandujs/core';
|
|
@@ -345,7 +344,7 @@ export function generateOpenAPISpec<T extends ContractDefinition>(
|
|
|
345
344
|
const paths: Record<string, Record<string, unknown>> = {};
|
|
346
345
|
|
|
347
346
|
for (const [name, endpoint] of Object.entries(contract.__endpoints)) {
|
|
348
|
-
const { method, path, input,
|
|
347
|
+
const { method, path, input, description, tags, errors } = endpoint;
|
|
349
348
|
|
|
350
349
|
if (!paths[path]) {
|
|
351
350
|
paths[path] = {};
|
package/src/contract/index.ts
CHANGED
|
@@ -37,10 +37,10 @@ export {
|
|
|
37
37
|
type RpcWireError,
|
|
38
38
|
} from "./rpc";
|
|
39
39
|
|
|
40
|
-
import type { ContractDefinition, ContractInstance
|
|
41
|
-
|
|
40
|
+
import type { ContractDefinition, ContractInstance } from "./schema";
|
|
41
|
+
|
|
42
42
|
import { defineHandler, defineRoute } from "./handler";
|
|
43
|
-
import { createClient, contractFetch
|
|
43
|
+
import { createClient, contractFetch } from "./client";
|
|
44
44
|
import { createClientContract } from "./client-safe";
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -102,7 +102,7 @@ export function extractProtectedFields(
|
|
|
102
102
|
}
|
|
103
103
|
// 민감 필드도 보호 대상
|
|
104
104
|
else if (isSensitiveField(value)) {
|
|
105
|
-
const
|
|
105
|
+
const _meta = getMetadata(value, SENSITIVE_FIELD) as SensitiveFieldMetadata | undefined;
|
|
106
106
|
fields.push({
|
|
107
107
|
path: currentPath,
|
|
108
108
|
reason: "Sensitive field - redacted in logs",
|
|
@@ -187,7 +187,7 @@ export function extractContractProtectedFields(
|
|
|
187
187
|
/**
|
|
188
188
|
* 객체에서 경로로 값 가져오기
|
|
189
189
|
*/
|
|
190
|
-
function
|
|
190
|
+
function _getValueByPath(obj: unknown, path: string): unknown {
|
|
191
191
|
const parts = path.split(".");
|
|
192
192
|
let current: unknown = obj;
|
|
193
193
|
|
package/src/contract/types.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
/**
|
|
15
15
|
* Extract inferred type from Zod schema
|
|
16
16
|
*/
|
|
17
|
-
type
|
|
17
|
+
type _InferZod<T> = T extends z.ZodTypeAny ? z.infer<T> : never;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Extract inferred type from response schema (supports ResponseSchemaWithExamples)
|
|
@@ -107,13 +107,10 @@ function createFakeCtor(): { Ctor: BunSqlCtor; state: FakeState } {
|
|
|
107
107
|
return Object.assign(call, methods);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// non-this value from a constructor replaces the default return.
|
|
115
|
-
return makeFakeSqlInstance(false) as FakeBunSql;
|
|
116
|
-
}
|
|
110
|
+
function FakeBunSql(config: Record<string, unknown>) {
|
|
111
|
+
state.ctorCalls.push(config);
|
|
112
|
+
// Return the callable instance — NOT the default constructed object.
|
|
113
|
+
return makeFakeSqlInstance(false);
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
return {
|
package/src/db/index.ts
CHANGED
|
@@ -115,7 +115,7 @@ export interface DbConfig {
|
|
|
115
115
|
* returns the full result array. The attached methods support one-shot
|
|
116
116
|
* reads, transactions, and shutdown.
|
|
117
117
|
*/
|
|
118
|
-
export interface Db {
|
|
118
|
+
export interface Db {
|
|
119
119
|
/**
|
|
120
120
|
* Tagged-template query. Values are bound as parameters, never
|
|
121
121
|
* interpolated as SQL text.
|
|
@@ -158,8 +158,17 @@ export interface Db {
|
|
|
158
158
|
* Closes the connection pool. Subsequent queries reject with a clear
|
|
159
159
|
* "pool closed" error. Calling `close()` twice is a no-op (idempotent).
|
|
160
160
|
*/
|
|
161
|
-
close(): Promise<void>;
|
|
162
|
-
}
|
|
161
|
+
close(options?: DbCloseOptions): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Options forwarded to Bun.SQL pool shutdown. */
|
|
165
|
+
export interface DbCloseOptions {
|
|
166
|
+
/**
|
|
167
|
+
* Maximum seconds to wait for in-flight queries before closing the pool.
|
|
168
|
+
* `0` asks Bun.SQL to close immediately.
|
|
169
|
+
*/
|
|
170
|
+
timeout?: number;
|
|
171
|
+
}
|
|
163
172
|
|
|
164
173
|
// ─── Bun runtime surface (structural; no `any`) ─────────────────────────────
|
|
165
174
|
|
|
@@ -181,14 +190,14 @@ interface BunSqlOptions {
|
|
|
181
190
|
* A Bun.SQL instance — itself a callable tagged-template function with
|
|
182
191
|
* methods attached. We only model the subset we actually use.
|
|
183
192
|
*/
|
|
184
|
-
interface BunSqlInstance {
|
|
185
|
-
<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<
|
|
186
|
-
T[] & { count?: number; command?: string }
|
|
187
|
-
>;
|
|
188
|
-
begin<R>(fn: (tx: BunSqlInstance) => Promise<R>): Promise<R>;
|
|
189
|
-
close(): Promise<void>;
|
|
190
|
-
readonly options?: BunSqlOptions;
|
|
191
|
-
}
|
|
193
|
+
interface BunSqlInstance {
|
|
194
|
+
<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<
|
|
195
|
+
T[] & { count?: number; command?: string }
|
|
196
|
+
>;
|
|
197
|
+
begin<R>(fn: (tx: BunSqlInstance) => Promise<R>): Promise<R>;
|
|
198
|
+
close(options?: DbCloseOptions): Promise<void>;
|
|
199
|
+
readonly options?: BunSqlOptions;
|
|
200
|
+
}
|
|
192
201
|
|
|
193
202
|
/**
|
|
194
203
|
* Constructor surface — `Bun.SQL` is a class; we only need the `new`
|
|
@@ -250,13 +259,35 @@ function getBunSqlCtor(): BunSqlCtor {
|
|
|
250
259
|
|
|
251
260
|
// ─── Error helpers ──────────────────────────────────────────────────────────
|
|
252
261
|
|
|
253
|
-
const POOL_CLOSED_MESSAGE =
|
|
254
|
-
"[@mandujs/core/db] pool closed — query issued after Db.close().";
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
const POOL_CLOSED_MESSAGE =
|
|
263
|
+
"[@mandujs/core/db] pool closed — query issued after Db.close().";
|
|
264
|
+
|
|
265
|
+
const PIN_DB_HANDLE = Symbol.for("@mandujs/core/db/pin-handle");
|
|
266
|
+
|
|
267
|
+
interface DbHandlePin {
|
|
268
|
+
[PIN_DB_HANDLE]?: <R>(fn: () => Promise<R>) => Promise<R>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Internal helper for code paths that must keep a MySQL session alive across
|
|
273
|
+
* transaction boundaries, such as named advisory locks. Public Db callers do
|
|
274
|
+
* not need this; normal MySQL transactions recycle the handle afterwards.
|
|
275
|
+
*
|
|
276
|
+
* @internal
|
|
277
|
+
*/
|
|
278
|
+
export async function withPinnedDbHandle<R>(
|
|
279
|
+
db: Db,
|
|
280
|
+
fn: () => Promise<R>,
|
|
281
|
+
): Promise<R> {
|
|
282
|
+
const pin = (db as DbHandlePin)[PIN_DB_HANDLE];
|
|
283
|
+
if (!pin) return await fn();
|
|
284
|
+
return await pin(fn);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Structural check for "connection/pool closed" errors that Bun.SQL raises
|
|
289
|
+
* after `.close()`. Bun surfaces these as `SQLiteError` / `PostgresError` /
|
|
290
|
+
* `MySQLError` with a `code` ending in `CLOSED`.
|
|
260
291
|
*/
|
|
261
292
|
function isPoolClosedError(err: unknown): boolean {
|
|
262
293
|
if (!err || typeof err !== "object") return false;
|
|
@@ -427,12 +458,14 @@ function buildDbHandle(bunSql: BunSqlInstance, provider: SqlProvider): Db {
|
|
|
427
458
|
});
|
|
428
459
|
};
|
|
429
460
|
|
|
430
|
-
(db as { close: Db["close"] }).close = async function close(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
461
|
+
(db as { close: Db["close"] }).close = async function close(
|
|
462
|
+
options?: DbCloseOptions,
|
|
463
|
+
): Promise<void> {
|
|
464
|
+
if (closed) return; // idempotent
|
|
465
|
+
closed = true;
|
|
466
|
+
try {
|
|
467
|
+
await bunSql.close(options);
|
|
468
|
+
} catch (err) {
|
|
436
469
|
// Bun.SQL can throw if already-closed under the hood (mostly from a
|
|
437
470
|
// racing concurrent close). We already flipped our flag so subsequent
|
|
438
471
|
// queries reject cleanly — swallow this one.
|
|
@@ -451,7 +484,7 @@ function buildDbHandle(bunSql: BunSqlInstance, provider: SqlProvider): Db {
|
|
|
451
484
|
*
|
|
452
485
|
* @throws {TypeError} when `config.url` is missing or empty.
|
|
453
486
|
*/
|
|
454
|
-
export function createDb(config: DbConfig): Db {
|
|
487
|
+
export function createDb(config: DbConfig): Db {
|
|
455
488
|
// Up-front config validation. We check `url` here (not only in
|
|
456
489
|
// `_createDbWith`) so the error fires at construction time — matches
|
|
457
490
|
// the TypeError contract the public API documents.
|
|
@@ -469,18 +502,43 @@ export function createDb(config: DbConfig): Db {
|
|
|
469
502
|
// fires on the first real call, with a version-specific message.
|
|
470
503
|
//
|
|
471
504
|
// We still need to *return* a Db now, so call through a forwarding
|
|
472
|
-
// function that probes on demand.
|
|
473
|
-
let real: Db | null = null;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
505
|
+
// function that probes on demand.
|
|
506
|
+
let real: Db | null = null;
|
|
507
|
+
let closed = false;
|
|
508
|
+
let pinDepth = 0;
|
|
509
|
+
let pendingMysqlRecycle = false;
|
|
510
|
+
function materialize(): Db {
|
|
511
|
+
if (closed) {
|
|
512
|
+
throw new Error(POOL_CLOSED_MESSAGE);
|
|
513
|
+
}
|
|
514
|
+
if (real) return real;
|
|
515
|
+
real = _createDbWith(getBunSqlCtor(), config);
|
|
516
|
+
return real;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function flushPendingMysqlRecycle(): Promise<void> {
|
|
520
|
+
if (provider !== "mysql" || pinDepth > 0 || !pendingMysqlRecycle) return;
|
|
521
|
+
pendingMysqlRecycle = false;
|
|
522
|
+
if (closed || !real) return;
|
|
523
|
+
const db = real;
|
|
524
|
+
real = null;
|
|
525
|
+
await db.close({ timeout: 0 });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function recycleMysqlHandle(db: Db): Promise<void> {
|
|
529
|
+
if (provider !== "mysql" || real !== db || closed) return;
|
|
530
|
+
if (pinDepth > 0) {
|
|
531
|
+
pendingMysqlRecycle = true;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
real = null;
|
|
535
|
+
await db.close({ timeout: 0 });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const forward = async function forwardCall<T extends Row = Row>(
|
|
539
|
+
strings: TemplateStringsArray,
|
|
540
|
+
...values: unknown[]
|
|
541
|
+
): Promise<T[]> {
|
|
484
542
|
return await materialize()<T>(strings, ...values);
|
|
485
543
|
} as unknown as Db;
|
|
486
544
|
|
|
@@ -497,17 +555,46 @@ export function createDb(config: DbConfig): Db {
|
|
|
497
555
|
...values: unknown[]
|
|
498
556
|
): Promise<T | null> {
|
|
499
557
|
return await materialize().one<T>(strings, ...values);
|
|
500
|
-
};
|
|
501
|
-
(forward as { transaction: Db["transaction"] }).transaction =
|
|
502
|
-
async function transaction<R>(fn: (tx: Db) => Promise<R>): Promise<R> {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
558
|
+
};
|
|
559
|
+
(forward as { transaction: Db["transaction"] }).transaction =
|
|
560
|
+
async function transaction<R>(fn: (tx: Db) => Promise<R>): Promise<R> {
|
|
561
|
+
const hadMaterializedHandle = real !== null;
|
|
562
|
+
let db = materialize();
|
|
563
|
+
if (provider === "mysql" && hadMaterializedHandle) {
|
|
564
|
+
await recycleMysqlHandle(db);
|
|
565
|
+
db = materialize();
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
return await db.transaction(fn);
|
|
569
|
+
} finally {
|
|
570
|
+
await recycleMysqlHandle(db);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
(forward as { close: Db["close"] }).close = async function close(
|
|
574
|
+
options?: DbCloseOptions,
|
|
575
|
+
): Promise<void> {
|
|
576
|
+
if (closed) return;
|
|
577
|
+
closed = true;
|
|
578
|
+
// If no query ever ran, materialize() was never called — nothing to close.
|
|
579
|
+
if (!real) return;
|
|
580
|
+
const db = real;
|
|
581
|
+
real = null;
|
|
582
|
+
await db.close(options);
|
|
583
|
+
};
|
|
584
|
+
Object.defineProperty(forward, PIN_DB_HANDLE, {
|
|
585
|
+
value: async function withPinnedHandle<R>(fn: () => Promise<R>): Promise<R> {
|
|
586
|
+
pinDepth += 1;
|
|
587
|
+
try {
|
|
588
|
+
return await fn();
|
|
589
|
+
} finally {
|
|
590
|
+
pinDepth -= 1;
|
|
591
|
+
await flushPendingMysqlRecycle();
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
enumerable: false,
|
|
595
|
+
writable: false,
|
|
596
|
+
configurable: false,
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return forward;
|
|
600
|
+
}
|
|
@@ -102,11 +102,11 @@ async function acquirePgAdvisoryLock(
|
|
|
102
102
|
// hash-function reproduction.
|
|
103
103
|
await db`SELECT pg_advisory_lock(hashtext(${lockId})::bigint)`;
|
|
104
104
|
|
|
105
|
-
let
|
|
105
|
+
let _released = false;
|
|
106
106
|
return {
|
|
107
107
|
async release(): Promise<void> {
|
|
108
|
-
if (
|
|
109
|
-
|
|
108
|
+
if (_released) return;
|
|
109
|
+
_released = true;
|
|
110
110
|
try {
|
|
111
111
|
await db`SELECT pg_advisory_unlock(hashtext(${lockId})::bigint)`;
|
|
112
112
|
} catch (err) {
|
|
@@ -126,6 +126,7 @@ async function acquireMysqlLock(
|
|
|
126
126
|
db: Db,
|
|
127
127
|
lockId: string,
|
|
128
128
|
): Promise<MigrationLock> {
|
|
129
|
+
const processLock = await acquireMysqlProcessMutex(lockId);
|
|
129
130
|
// `GET_LOCK` returns:
|
|
130
131
|
// 1 — lock granted
|
|
131
132
|
// 0 — timeout (still blocked after the timeout)
|
|
@@ -133,25 +134,30 @@ async function acquireMysqlLock(
|
|
|
133
134
|
//
|
|
134
135
|
// Bun.SQL returns the single row as `[{ acquired: 1 }]`; we destructure
|
|
135
136
|
// defensively to handle any driver-side aliasing.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
try {
|
|
138
|
+
const timeoutSec = MYSQL_LOCK_TIMEOUT_SECONDS;
|
|
139
|
+
const rows = await db<{ acquired: number | bigint | null }>`
|
|
140
|
+
SELECT GET_LOCK(${lockId}, ${timeoutSec}) AS acquired
|
|
141
|
+
`;
|
|
142
|
+
const first = rows[0];
|
|
143
|
+
const value = first ? Number(first.acquired) : NaN;
|
|
144
|
+
if (value !== 1) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`[@mandujs/core/db/migrations] GET_LOCK(${JSON.stringify(lockId)}, ${timeoutSec}) ` +
|
|
147
|
+
`returned ${value === 0 ? "0 (timeout)" : "NULL (error)"}; ` +
|
|
148
|
+
`another migration runner may be holding the lock.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
await processLock.release();
|
|
153
|
+
throw err;
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
let
|
|
156
|
+
let _released = false;
|
|
151
157
|
return {
|
|
152
158
|
async release(): Promise<void> {
|
|
153
|
-
if (
|
|
154
|
-
|
|
159
|
+
if (_released) return;
|
|
160
|
+
_released = true;
|
|
155
161
|
try {
|
|
156
162
|
await db`SELECT RELEASE_LOCK(${lockId})`;
|
|
157
163
|
} catch (err) {
|
|
@@ -159,6 +165,55 @@ async function acquireMysqlLock(
|
|
|
159
165
|
console.warn(
|
|
160
166
|
`[@mandujs/core/db/migrations] RELEASE_LOCK failed: ${msg}`,
|
|
161
167
|
);
|
|
168
|
+
} finally {
|
|
169
|
+
await processLock.release();
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Same-process MySQL runners are serialised before hitting GET_LOCK.
|
|
177
|
+
* Cross-process safety still comes from MySQL's named lock; this mutex only
|
|
178
|
+
* avoids concurrent GET_LOCK calls from separate Bun.SQL pools in one runtime.
|
|
179
|
+
*/
|
|
180
|
+
const MYSQL_LOCK_REGISTRY_SYMBOL = Symbol.for(
|
|
181
|
+
"@mandujs/core/db/migrations/mysql-locks",
|
|
182
|
+
);
|
|
183
|
+
interface MysqlLockRegistry {
|
|
184
|
+
chains: Map<string, Promise<void>>;
|
|
185
|
+
}
|
|
186
|
+
function getMysqlLockRegistry(): MysqlLockRegistry {
|
|
187
|
+
const g = globalThis as unknown as Record<symbol, unknown>;
|
|
188
|
+
let reg = g[MYSQL_LOCK_REGISTRY_SYMBOL] as MysqlLockRegistry | undefined;
|
|
189
|
+
if (!reg) {
|
|
190
|
+
reg = { chains: new Map() };
|
|
191
|
+
g[MYSQL_LOCK_REGISTRY_SYMBOL] = reg;
|
|
192
|
+
}
|
|
193
|
+
return reg;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function acquireMysqlProcessMutex(lockId: string): Promise<MigrationLock> {
|
|
197
|
+
const registry = getMysqlLockRegistry();
|
|
198
|
+
const previous = registry.chains.get(lockId) ?? Promise.resolve();
|
|
199
|
+
|
|
200
|
+
let release!: () => void;
|
|
201
|
+
const nextPromise = new Promise<void>((resolve) => {
|
|
202
|
+
release = resolve;
|
|
203
|
+
});
|
|
204
|
+
const tail = previous.then(() => nextPromise);
|
|
205
|
+
registry.chains.set(lockId, tail);
|
|
206
|
+
|
|
207
|
+
await previous;
|
|
208
|
+
|
|
209
|
+
let _released = false;
|
|
210
|
+
return {
|
|
211
|
+
async release(): Promise<void> {
|
|
212
|
+
if (_released) return;
|
|
213
|
+
_released = true;
|
|
214
|
+
release();
|
|
215
|
+
if (registry.chains.get(lockId) === tail) {
|
|
216
|
+
registry.chains.delete(lockId);
|
|
162
217
|
}
|
|
163
218
|
},
|
|
164
219
|
};
|
|
@@ -229,11 +284,11 @@ async function acquireSqliteImmediate(
|
|
|
229
284
|
// Wait for the previous holder to release.
|
|
230
285
|
await previous;
|
|
231
286
|
|
|
232
|
-
let
|
|
287
|
+
let _released = false;
|
|
233
288
|
return {
|
|
234
289
|
async release(): Promise<void> {
|
|
235
|
-
if (
|
|
236
|
-
|
|
290
|
+
if (_released) return;
|
|
291
|
+
_released = true;
|
|
237
292
|
release();
|
|
238
293
|
// If no one queued behind us, drop the entry so the registry
|
|
239
294
|
// doesn't grow unbounded across many apply() cycles.
|
|
@@ -245,10 +300,10 @@ async function acquireSqliteImmediate(
|
|
|
245
300
|
}
|
|
246
301
|
|
|
247
302
|
function makeNoopLock(): MigrationLock {
|
|
248
|
-
let
|
|
303
|
+
let _released = false;
|
|
249
304
|
return {
|
|
250
305
|
async release(): Promise<void> {
|
|
251
|
-
|
|
306
|
+
_released = true;
|
|
252
307
|
},
|
|
253
308
|
};
|
|
254
309
|
}
|