@jagilber-org/index-server 1.22.1 → 1.26.4
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/CHANGELOG.md +91 -2
- package/CODE_OF_CONDUCT.md +2 -0
- package/CONTRIBUTING.md +32 -2
- package/README.md +82 -19
- package/SECURITY.md +17 -5
- package/dist/config/dashboardConfig.d.ts +3 -0
- package/dist/config/dashboardConfig.js +3 -0
- package/dist/config/defaultValues.d.ts +1 -1
- package/dist/config/defaultValues.js +1 -1
- package/dist/config/featureConfig.d.ts +2 -0
- package/dist/config/featureConfig.js +6 -1
- package/dist/config/runtimeConfig.d.ts +1 -1
- package/dist/config/runtimeConfig.js +8 -9
- package/dist/dashboard/client/admin.html +170 -53
- package/dist/dashboard/client/css/admin.css +132 -0
- package/dist/dashboard/client/js/admin.auth.js +25 -11
- package/dist/dashboard/client/js/admin.config.js +1 -1
- package/dist/dashboard/client/js/admin.feedback.js +328 -0
- package/dist/dashboard/client/js/admin.graph.js +120 -18
- package/dist/dashboard/client/js/admin.instructions.js +27 -13
- package/dist/dashboard/client/js/admin.logs.js +1 -5
- package/dist/dashboard/client/js/admin.maintenance.js +53 -8
- package/dist/dashboard/client/js/admin.messaging.js +1 -4
- package/dist/dashboard/client/js/admin.overview.js +5 -1
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/client/js/admin.utils.js +43 -1
- package/dist/dashboard/client/js/mermaid.min.js +813 -537
- package/dist/dashboard/export/DataExporter.js +2 -1
- package/dist/dashboard/server/AdminPanel.d.ts +3 -0
- package/dist/dashboard/server/AdminPanel.js +132 -35
- package/dist/dashboard/server/ApiRoutes.js +40 -9
- package/dist/dashboard/server/DashboardServer.js +1 -1
- package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
- package/dist/dashboard/server/FileMetricsStorage.js +52 -5
- package/dist/dashboard/server/HttpTransport.js +6 -0
- package/dist/dashboard/server/InstanceManager.js +7 -2
- package/dist/dashboard/server/KnowledgeStore.js +7 -2
- package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
- package/dist/dashboard/server/MetricsCollector.js +113 -17
- package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
- package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
- package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
- package/dist/dashboard/server/routes/admin.routes.js +35 -27
- package/dist/dashboard/server/routes/alerts.routes.js +4 -3
- package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
- package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
- package/dist/dashboard/server/routes/graph.routes.js +10 -13
- package/dist/dashboard/server/routes/index.d.ts +1 -0
- package/dist/dashboard/server/routes/index.js +74 -39
- package/dist/dashboard/server/routes/instances.routes.js +2 -1
- package/dist/dashboard/server/routes/instructions.routes.js +46 -27
- package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
- package/dist/dashboard/server/routes/logs.routes.js +5 -4
- package/dist/dashboard/server/routes/messaging.routes.js +15 -14
- package/dist/dashboard/server/routes/metrics.routes.js +14 -13
- package/dist/dashboard/server/routes/scripts.routes.js +6 -3
- package/dist/dashboard/server/routes/status.routes.js +5 -4
- package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
- package/dist/dashboard/server/routes/usage.routes.js +2 -1
- package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
- package/dist/dashboard/server/utils/escapeHtml.js +11 -0
- package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
- package/dist/dashboard/server/utils/pathContainment.js +15 -0
- package/dist/dashboard/server/wsInit.js +2 -2
- package/dist/lib/mcpStdioLogging.d.ts +165 -0
- package/dist/lib/mcpStdioLogging.js +287 -0
- package/dist/schemas/index.d.ts +37 -2
- package/dist/schemas/index.js +27 -3
- package/dist/server/backgroundServicesStartup.d.ts +7 -1
- package/dist/server/backgroundServicesStartup.js +25 -8
- package/dist/server/certInit.d.ts +97 -0
- package/dist/server/certInit.js +359 -0
- package/dist/server/certInit.types.d.ts +92 -0
- package/dist/server/certInit.types.js +34 -0
- package/dist/server/handshake/fallbackFrames.d.ts +31 -0
- package/dist/server/handshake/fallbackFrames.js +38 -0
- package/dist/server/handshake/initializeDetector.d.ts +31 -0
- package/dist/server/handshake/initializeDetector.js +88 -0
- package/dist/server/handshake/protocol.d.ts +15 -0
- package/dist/server/handshake/protocol.js +37 -0
- package/dist/server/handshake/readyEmitter.d.ts +6 -0
- package/dist/server/handshake/readyEmitter.js +88 -0
- package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
- package/dist/server/handshake/safetyFallbacks.js +134 -0
- package/dist/server/handshake/stdinSniffer.d.ts +1 -0
- package/dist/server/handshake/stdinSniffer.js +260 -0
- package/dist/server/handshake/tracing.d.ts +16 -0
- package/dist/server/handshake/tracing.js +95 -0
- package/dist/server/handshakeManager.d.ts +23 -23
- package/dist/server/handshakeManager.js +36 -466
- package/dist/server/index-server.d.ts +23 -0
- package/dist/server/index-server.js +194 -9
- package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
- package/dist/server/mcpReadOnlySurfaces.js +297 -0
- package/dist/server/sdkServer.js +69 -7
- package/dist/server/transport.d.ts +5 -6
- package/dist/server/transport.js +46 -64
- package/dist/server/transportFactory.d.ts +3 -9
- package/dist/server/transportFactory.js +18 -380
- package/dist/services/atomicFs.d.ts +3 -0
- package/dist/services/atomicFs.js +171 -13
- package/dist/services/auditLog.d.ts +17 -2
- package/dist/services/auditLog.js +75 -14
- package/dist/services/bootstrapGating.js +1 -1
- package/dist/services/categoryRules.d.ts +10 -0
- package/dist/services/categoryRules.js +17 -0
- package/dist/services/classificationService.js +7 -5
- package/dist/services/embeddingService.d.ts +27 -11
- package/dist/services/embeddingService.js +51 -14
- package/dist/services/feedbackStorage.d.ts +39 -0
- package/dist/services/feedbackStorage.js +88 -0
- package/dist/services/handlers/instructions.add.js +429 -317
- package/dist/services/handlers/instructions.groom.js +128 -31
- package/dist/services/handlers/instructions.import.js +56 -23
- package/dist/services/handlers/instructions.patch.js +43 -32
- package/dist/services/handlers/instructions.query.js +20 -29
- package/dist/services/handlers/instructions.shared.d.ts +54 -0
- package/dist/services/handlers/instructions.shared.js +126 -1
- package/dist/services/handlers.activation.js +83 -81
- package/dist/services/handlers.dashboardConfig.d.ts +2 -2
- package/dist/services/handlers.dashboardConfig.js +1 -2
- package/dist/services/handlers.diagnostics.js +75 -54
- package/dist/services/handlers.feedback.d.ts +4 -11
- package/dist/services/handlers.feedback.js +11 -333
- package/dist/services/handlers.gates.js +69 -37
- package/dist/services/handlers.graph.js +2 -2
- package/dist/services/handlers.help.js +2 -2
- package/dist/services/handlers.instructionSchema.js +4 -2
- package/dist/services/handlers.integrity.js +42 -22
- package/dist/services/handlers.messaging.js +1 -1
- package/dist/services/handlers.metrics.js +51 -6
- package/dist/services/handlers.prompt.js +10 -2
- package/dist/services/handlers.search.js +94 -44
- package/dist/services/handlers.trace.js +1 -1
- package/dist/services/handlers.usage.js +38 -7
- package/dist/services/indexContext.d.ts +21 -1
- package/dist/services/indexContext.js +263 -78
- package/dist/services/indexLoader.d.ts +1 -0
- package/dist/services/indexLoader.js +28 -8
- package/dist/services/instructionRecordValidation.d.ts +39 -0
- package/dist/services/instructionRecordValidation.js +388 -0
- package/dist/services/instructions.dispatcher.js +4 -4
- package/dist/services/loaderSchemaValidator.d.ts +15 -0
- package/dist/services/loaderSchemaValidator.js +69 -0
- package/dist/services/logger.js +11 -2
- package/dist/services/mcpLogBridge.d.ts +49 -0
- package/dist/services/mcpLogBridge.js +83 -0
- package/dist/services/ownershipService.js +18 -8
- package/dist/services/performanceBaseline.js +23 -22
- package/dist/services/promptReviewService.d.ts +3 -1
- package/dist/services/promptReviewService.js +41 -13
- package/dist/services/regexSafety.d.ts +6 -0
- package/dist/services/regexSafety.js +46 -0
- package/dist/services/seedBootstrap.js +1 -1
- package/dist/services/storage/factory.d.ts +14 -1
- package/dist/services/storage/factory.js +61 -1
- package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
- package/dist/services/storage/jsonEmbeddingStore.js +83 -0
- package/dist/services/storage/jsonFileStore.d.ts +3 -1
- package/dist/services/storage/jsonFileStore.js +8 -6
- package/dist/services/storage/migrationEngine.d.ts +13 -0
- package/dist/services/storage/migrationEngine.js +31 -0
- package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
- package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
- package/dist/services/storage/sqliteStore.d.ts +3 -1
- package/dist/services/storage/sqliteStore.js +2 -2
- package/dist/services/storage/types.d.ts +48 -1
- package/dist/services/toolRegistry.js +77 -67
- package/dist/services/toolRegistry.zod.js +89 -86
- package/dist/services/tracing.js +5 -4
- package/dist/utils/envUtils.d.ts +4 -0
- package/dist/utils/envUtils.js +7 -0
- package/dist/utils/memoryMonitor.js +11 -10
- package/package.json +12 -4
- package/schemas/instruction.schema.json +38 -1
- package/scripts/copy-dashboard-assets.mjs +1 -1
- package/scripts/dist/README.md +1 -1
- package/scripts/generate-certs.mjs +201 -0
- package/scripts/setup-wizard.mjs +781 -0
- package/server.json +20 -0
- package/dist/externalClientLib.d.ts +0 -1
- package/dist/externalClientLib.js +0 -2
- package/dist/portableClientWrapper.d.ts +0 -1
- package/dist/portableClientWrapper.js +0 -2
- package/dist/services/indexingService.d.ts +0 -1
- package/dist/services/indexingService.js +0 -2
|
@@ -3,407 +3,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.dynamicImport = void 0;
|
|
4
4
|
exports.setupStdoutDiagnostics = setupStdoutDiagnostics;
|
|
5
5
|
exports.setupDispatcherOverride = setupDispatcherOverride;
|
|
6
|
-
exports.wrapTransportSend = wrapTransportSend;
|
|
7
6
|
exports.setupKeepalive = setupKeepalive;
|
|
8
7
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
9
8
|
/**
|
|
10
|
-
* Transport
|
|
11
|
-
* wrappers for the MCP server.
|
|
9
|
+
* Transport bootstrap helpers for the MCP server.
|
|
12
10
|
*/
|
|
13
11
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
14
|
-
const handshakeManager_1 = require("./handshakeManager");
|
|
15
12
|
// Helper to perform a true dynamic ESM import that TypeScript won't down-level to require()
|
|
16
13
|
const dynamicImport = (specifier) => (Function('m', 'return import(m);'))(specifier);
|
|
17
14
|
exports.dynamicImport = dynamicImport;
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
16
|
+
* Emit a lightweight diagnostic marker without intercepting stdout writes.
|
|
20
17
|
* Enabled via INDEX_SERVER_TRACE=healthMixed.
|
|
21
18
|
*/
|
|
22
19
|
async function setupStdoutDiagnostics() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
process.stderr.write(`[diag] ${Date.now()} ${msg}\n`);
|
|
27
|
-
}
|
|
28
|
-
catch { /* ignore */ }
|
|
29
|
-
} };
|
|
30
|
-
// Emit a one-time version marker so tests can assert the newer diagnostic wrapper code is actually loaded.
|
|
31
|
-
if (__diagEnabled) {
|
|
32
|
-
try {
|
|
33
|
-
const buildMarker = 'sdkServerDiagV1';
|
|
34
|
-
// Include a coarse content hash surrogate: file size + mtime if available
|
|
35
|
-
let fsMeta = '';
|
|
36
|
-
try {
|
|
37
|
-
const fsMod = await import('fs');
|
|
38
|
-
const stat = fsMod.statSync(__filename);
|
|
39
|
-
fsMeta = ` size=${stat.size} mtimeMs=${Math.trunc(stat.mtimeMs)}`;
|
|
40
|
-
}
|
|
41
|
-
catch { /* ignore meta */ }
|
|
42
|
-
process.stderr.write(`[diag] ${Date.now()} diag_start marker=${buildMarker}${fsMeta}\n`);
|
|
43
|
-
}
|
|
44
|
-
catch { /* ignore */ }
|
|
45
|
-
}
|
|
46
|
-
if (__diagEnabled) {
|
|
47
|
-
try {
|
|
48
|
-
const origWrite = process.stdout.write.bind(process.stdout);
|
|
49
|
-
let backpressureEvents = 0;
|
|
50
|
-
let bytesTotal = 0;
|
|
51
|
-
let lastReportAt = Date.now();
|
|
52
|
-
process.stdout.on?.('drain', () => { __diag('stdout_drain'); });
|
|
53
|
-
process.stdout.write = function (chunk, encoding, cb) {
|
|
54
|
-
try {
|
|
55
|
-
const size = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(String(chunk));
|
|
56
|
-
bytesTotal += size;
|
|
57
|
-
const ret = origWrite(chunk, encoding, cb);
|
|
58
|
-
if (!ret) {
|
|
59
|
-
backpressureEvents++;
|
|
60
|
-
__diag(`stdout_backpressure size=${size} backpressureEvents=${backpressureEvents}`);
|
|
61
|
-
}
|
|
62
|
-
const now = Date.now();
|
|
63
|
-
if (now - lastReportAt > 2000) {
|
|
64
|
-
__diag(`stdout_summary bytesTotal=${bytesTotal} backpressureEvents=${backpressureEvents}`);
|
|
65
|
-
lastReportAt = now;
|
|
66
|
-
}
|
|
67
|
-
return ret;
|
|
68
|
-
}
|
|
69
|
-
catch (e) {
|
|
70
|
-
try {
|
|
71
|
-
__diag(`stdout_write_wrapper_error ${e?.message || String(e)}`);
|
|
72
|
-
}
|
|
73
|
-
catch { /* ignore */ }
|
|
74
|
-
return origWrite(chunk, encoding, cb);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
catch { /* ignore */ }
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Robust semantic error preservation: deep scan for JSON-RPC code/message
|
|
82
|
-
function deepScan(obj, depth = 0, seen = new Set()) {
|
|
83
|
-
if (!obj || typeof obj !== 'object' || depth > 4 || seen.has(obj))
|
|
84
|
-
return undefined;
|
|
85
|
-
seen.add(obj);
|
|
86
|
-
if (Number.isSafeInteger(obj.code)) {
|
|
87
|
-
const c = obj.code;
|
|
88
|
-
if (c === -32601 || c === -32602)
|
|
89
|
-
return c; // prioritize semantic validation codes
|
|
90
|
-
}
|
|
91
|
-
// Prefer specific well-known nesting keys first
|
|
92
|
-
const keys = ['error', 'original', 'cause', 'data'];
|
|
93
|
-
for (const k of keys) {
|
|
94
|
-
try {
|
|
95
|
-
const child = obj[k];
|
|
96
|
-
const found = deepScan(child, depth + 1, seen);
|
|
97
|
-
if (found !== undefined)
|
|
98
|
-
return found;
|
|
99
|
-
}
|
|
100
|
-
catch { /* ignore */ }
|
|
101
|
-
}
|
|
102
|
-
// Fallback: generic property iteration (shallow) to catch unexpected wrappers
|
|
103
|
-
if (depth < 2) {
|
|
104
|
-
try {
|
|
105
|
-
for (const v of Object.values(obj)) {
|
|
106
|
-
const found = deepScan(v, depth + 1, seen);
|
|
107
|
-
if (found !== undefined)
|
|
108
|
-
return found;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch { /* ignore */ }
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
// Categorize request for diagnostics
|
|
116
|
-
function categorizeRequest(request) {
|
|
117
|
-
const metaName = request.method === 'tools/call' ? request?.params?.name : '';
|
|
118
|
-
if (request.method === 'initialize')
|
|
119
|
-
return 'init';
|
|
120
|
-
if (request.method === 'health_check' || metaName === 'health_check')
|
|
121
|
-
return 'health';
|
|
122
|
-
if (request.method === 'metrics_snapshot' || metaName === 'metrics_snapshot')
|
|
123
|
-
return 'metrics';
|
|
124
|
-
if (metaName === 'meta_tools')
|
|
125
|
-
return 'meta';
|
|
126
|
-
return 'other';
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Override the internal request dispatcher to retain error.data & emit diagnostics.
|
|
130
|
-
* The upstream SDK has used both `_onRequest` (camel) and `_onrequest` (lower) across versions;
|
|
131
|
-
* we defensively hook whichever exists and assign our wrapper to BOTH names.
|
|
132
|
-
*/
|
|
133
|
-
function setupDispatcherOverride(server) {
|
|
134
|
-
const existingLower = server._onrequest;
|
|
135
|
-
const existingCamel = server._onRequest;
|
|
136
|
-
const originalOnRequest = (existingCamel || existingLower) ? (existingCamel || existingLower).bind(server) : undefined;
|
|
137
|
-
let __diagQueueDepth = 0;
|
|
138
|
-
if (originalOnRequest) {
|
|
139
|
-
const wrapped = function (request) {
|
|
140
|
-
const diagEnabled = (0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed');
|
|
141
|
-
let startedAt;
|
|
142
|
-
if (diagEnabled) {
|
|
143
|
-
const category = categorizeRequest(request);
|
|
144
|
-
if (category === 'health' || category === 'meta' || category === 'metrics' || category === 'init') {
|
|
145
|
-
startedAt = Date.now();
|
|
146
|
-
try {
|
|
147
|
-
__diagQueueDepth++;
|
|
148
|
-
process.stderr.write(`[diag] ${startedAt} rq_enqueue method=${request.method} cat=${category} id=${request.id} qdepth=${__diagQueueDepth}\n`);
|
|
149
|
-
if (category === 'health') {
|
|
150
|
-
if (!server.__firstHealthEnqueueAt) {
|
|
151
|
-
server.__firstHealthEnqueueAt = startedAt;
|
|
152
|
-
}
|
|
153
|
-
if (request.id === 1 && !server.__healthId1EnqueueAt) {
|
|
154
|
-
server.__healthId1EnqueueAt = startedAt;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (!server.__activeDiagRequests) {
|
|
158
|
-
server.__activeDiagRequests = new Map();
|
|
159
|
-
}
|
|
160
|
-
server.__activeDiagRequests.set(request.id, { id: request.id, method: request.method, cat: category, start: startedAt });
|
|
161
|
-
// Mis-order detection: health/metrics/meta before initialize observed
|
|
162
|
-
if ((category === 'health' || category === 'metrics' || category === 'meta') && !server.__sawInitializeRequest) {
|
|
163
|
-
try {
|
|
164
|
-
process.stderr.write(`[diag] ${Date.now()} rq_misorder_before_init method=${request.method} id=${request.id} cat=${category} qdepth=${__diagQueueDepth}\n`);
|
|
165
|
-
}
|
|
166
|
-
catch { /* ignore */ }
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch { /* ignore */ }
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
|
|
173
|
-
if (handler === undefined) {
|
|
174
|
-
return this._transport?.send({ jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found', data: { method: request.method } } }).catch(() => { });
|
|
175
|
-
}
|
|
176
|
-
const abortController = new AbortController();
|
|
177
|
-
this._requestHandlerAbortControllers.set(request.id, abortController);
|
|
178
|
-
// IMPORTANT: We intentionally never early-return without sending a response
|
|
179
|
-
Promise.resolve()
|
|
180
|
-
.then(() => handler(request, { signal: abortController.signal }))
|
|
181
|
-
.then((result) => {
|
|
182
|
-
if (startedAt !== undefined) {
|
|
183
|
-
try {
|
|
184
|
-
const dur = Date.now() - startedAt;
|
|
185
|
-
const category = categorizeRequest(request);
|
|
186
|
-
if (category === 'health' || category === 'meta' || category === 'metrics' || category === 'init') {
|
|
187
|
-
__diagQueueDepth = Math.max(0, __diagQueueDepth - 1);
|
|
188
|
-
process.stderr.write(`[diag] ${Date.now()} rq_complete method=${request.method} cat=${category} id=${request.id} dur_ms=${dur} qdepth=${__diagQueueDepth}\n`);
|
|
189
|
-
try {
|
|
190
|
-
server.__activeDiagRequests?.delete(request.id);
|
|
191
|
-
}
|
|
192
|
-
catch { /* ignore */ }
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
catch { /* ignore */ }
|
|
196
|
-
}
|
|
197
|
-
if (abortController.signal.aborted) {
|
|
198
|
-
try {
|
|
199
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
200
|
-
process.stderr.write(`[rpc] aborted-but-sending method=${request.method} id=${request.id}\n`);
|
|
201
|
-
}
|
|
202
|
-
catch { /* ignore */ }
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
try {
|
|
206
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
207
|
-
process.stderr.write(`[rpc] response method=${request.method} id=${request.id} ok\n`);
|
|
208
|
-
}
|
|
209
|
-
catch { /* ignore */ }
|
|
210
|
-
}
|
|
211
|
-
const sendPromise = this._transport?.send({ jsonrpc: '2.0', id: request.id, result });
|
|
212
|
-
if (request.method === 'initialize') {
|
|
213
|
-
(0, handshakeManager_1.initFrameLog)('dispatcher_before_send', { id: request.id });
|
|
214
|
-
(sendPromise?.then?.(() => {
|
|
215
|
-
(0, handshakeManager_1.initFrameLog)('dispatcher_send_resolved', { id: request.id });
|
|
216
|
-
server.__initResponseSent = true;
|
|
217
|
-
setTimeout(() => (0, handshakeManager_1.emitReadyGlobal)(server, 'transport-send-hook'), 0);
|
|
218
|
-
}))?.catch(() => { });
|
|
219
|
-
}
|
|
220
|
-
return sendPromise;
|
|
221
|
-
}, (error) => {
|
|
222
|
-
if (startedAt !== undefined) {
|
|
223
|
-
try {
|
|
224
|
-
const dur = Date.now() - startedAt;
|
|
225
|
-
const category = categorizeRequest(request);
|
|
226
|
-
if (category === 'health' || category === 'meta' || category === 'metrics' || category === 'init') {
|
|
227
|
-
__diagQueueDepth = Math.max(0, __diagQueueDepth - 1);
|
|
228
|
-
process.stderr.write(`[diag] ${Date.now()} rq_error method=${request.method} cat=${category} id=${request.id} dur_ms=${dur} qdepth=${__diagQueueDepth}\n`);
|
|
229
|
-
try {
|
|
230
|
-
server.__activeDiagRequests?.delete(request.id);
|
|
231
|
-
}
|
|
232
|
-
catch { /* ignore */ }
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
catch { /* ignore */ }
|
|
236
|
-
}
|
|
237
|
-
if (abortController.signal.aborted) {
|
|
238
|
-
try {
|
|
239
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
240
|
-
process.stderr.write(`[rpc] aborted-error-path method=${request.method} id=${request.id}\n`);
|
|
241
|
-
}
|
|
242
|
-
catch { /* ignore */ }
|
|
243
|
-
}
|
|
244
|
-
// Robust semantic error preservation: search multiple nests for a JSON-RPC code/message
|
|
245
|
-
let errCode = error?.code;
|
|
246
|
-
if (!Number.isSafeInteger(errCode))
|
|
247
|
-
errCode = error?.data?.code;
|
|
248
|
-
if (!Number.isSafeInteger(errCode))
|
|
249
|
-
errCode = error?.original?.code;
|
|
250
|
-
if (!Number.isSafeInteger(errCode))
|
|
251
|
-
errCode = error?.cause?.code;
|
|
252
|
-
if (!Number.isSafeInteger(errCode))
|
|
253
|
-
errCode = error?.error?.code;
|
|
254
|
-
const rawBeforeDeep = errCode;
|
|
255
|
-
if (!Number.isSafeInteger(errCode) || errCode === -32603) {
|
|
256
|
-
const deep = deepScan(error);
|
|
257
|
-
if (Number.isSafeInteger(deep))
|
|
258
|
-
errCode = deep;
|
|
259
|
-
}
|
|
260
|
-
const safeCode = Number.isSafeInteger(errCode) ? errCode : undefined;
|
|
261
|
-
let errMessage = error?.message;
|
|
262
|
-
if (!errMessage)
|
|
263
|
-
errMessage = error?.data?.message;
|
|
264
|
-
if (!errMessage)
|
|
265
|
-
errMessage = error?.original?.message;
|
|
266
|
-
if (!errMessage)
|
|
267
|
-
errMessage = error?.cause?.message;
|
|
268
|
-
if (!errMessage)
|
|
269
|
-
errMessage = error?.error?.message;
|
|
270
|
-
if (typeof errMessage !== 'string' || !errMessage.trim())
|
|
271
|
-
errMessage = 'Internal error';
|
|
272
|
-
let data = error?.data;
|
|
273
|
-
if (data && typeof data === 'object') {
|
|
274
|
-
if (typeof data.message !== 'string')
|
|
275
|
-
data = { ...data, message: errMessage };
|
|
276
|
-
}
|
|
277
|
-
else if (error && typeof error === 'object') {
|
|
278
|
-
data = { message: errMessage, ...(error.method ? { method: error.method } : {}) };
|
|
279
|
-
}
|
|
280
|
-
let finalCode = (safeCode !== undefined) ? safeCode : -32603;
|
|
281
|
-
if (finalCode === -32603 && data && typeof data === 'object') {
|
|
282
|
-
try {
|
|
283
|
-
const reason = data.reason || data.data?.reason;
|
|
284
|
-
if (reason === 'missing_action')
|
|
285
|
-
finalCode = -32602;
|
|
286
|
-
else if (reason === 'unknown_action' || reason === 'mutation_disabled' || reason === 'unknown_handler')
|
|
287
|
-
finalCode = -32601;
|
|
288
|
-
}
|
|
289
|
-
catch { /* ignore */ }
|
|
290
|
-
}
|
|
291
|
-
try {
|
|
292
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose) {
|
|
293
|
-
const before = Number.isSafeInteger(rawBeforeDeep) ? rawBeforeDeep : 'n/a';
|
|
294
|
-
if ((before === 'n/a' || before === -32603) && (finalCode === -32601 || finalCode === -32602)) {
|
|
295
|
-
const reasonHint = data?.reason || data?.data?.reason;
|
|
296
|
-
process.stderr.write(`[rpc] deep_recover_semantic code=${finalCode} from=${before} reasonHint=${reasonHint || ''}\n`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch { /* ignore */ }
|
|
301
|
-
try {
|
|
302
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
|
|
303
|
-
process.stderr.write(`[rpc] response method=${request.method} id=${request.id} error=${errMessage} code=${finalCode}\n`);
|
|
304
|
-
}
|
|
305
|
-
catch { /* ignore */ }
|
|
306
|
-
return this._transport?.send({ jsonrpc: '2.0', id: request.id, error: { code: finalCode, message: errMessage, data } });
|
|
307
|
-
})
|
|
308
|
-
.catch(() => { })
|
|
309
|
-
.finally(() => { this._requestHandlerAbortControllers.delete(request.id); });
|
|
310
|
-
};
|
|
311
|
-
// Attach wrapper to BOTH potential internal symbols to guarantee interception.
|
|
312
|
-
server._onRequest = wrapped;
|
|
313
|
-
server._onrequest = wrapped;
|
|
314
|
-
server.__dispatcherOverrideActive = true;
|
|
315
|
-
try {
|
|
316
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed'))
|
|
317
|
-
process.stderr.write(`[diag] ${Date.now()} dispatcher_override applied props=${[existingCamel ? '_onRequest(original)' : '', existingLower ? '_onrequest(original)' : ''].filter(Boolean).join(',') || 'none'}\n`);
|
|
318
|
-
}
|
|
319
|
-
catch { /* ignore */ }
|
|
320
|
-
// Starvation watchdog
|
|
321
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed') && !server.__starvationWatchdogStarted) {
|
|
322
|
-
server.__starvationWatchdogStarted = true;
|
|
323
|
-
let ticks = 0;
|
|
324
|
-
const iv = setInterval(() => {
|
|
325
|
-
try {
|
|
326
|
-
ticks++;
|
|
327
|
-
const active = server.__activeDiagRequests;
|
|
328
|
-
const firstH = server.__healthId1EnqueueAt;
|
|
329
|
-
if (active && active.size) {
|
|
330
|
-
const pending = Array.from(active.values()).map((r) => ({ id: r.id, cat: r.cat, age: Date.now() - r.start })).sort((a, b) => a.id - b.id).slice(0, 12);
|
|
331
|
-
const hasHealth1 = !!active.get?.(1);
|
|
332
|
-
if (hasHealth1 || (firstH && Date.now() - firstH > 40)) {
|
|
333
|
-
process.stderr.write(`[diag] ${Date.now()} starvation_watchdog tick=${ticks} pending=${pending.length} details=${JSON.stringify(pending)} firstHealthAge=${firstH ? Date.now() - firstH : -1} hasHealth1=${hasHealth1}\n`);
|
|
334
|
-
}
|
|
335
|
-
if (!hasHealth1 && firstH && Date.now() - firstH > 400) {
|
|
336
|
-
process.stderr.write(`[diag] ${Date.now()} starvation_watchdog_health1_missing age=${Date.now() - firstH}\n`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (ticks > 40 || (server.__activeDiagRequests && !server.__activeDiagRequests.get(1))) {
|
|
340
|
-
clearInterval(iv);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
catch { /* ignore */ }
|
|
344
|
-
}, 25);
|
|
345
|
-
iv.unref?.();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
else if ((0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed')) {
|
|
349
|
-
try {
|
|
350
|
-
process.stderr.write(`[diag] ${Date.now()} dispatcher_override_skipped no_original_handler_found`);
|
|
351
|
-
}
|
|
352
|
-
catch { /* ignore */ }
|
|
353
|
-
}
|
|
354
|
-
// Enumerate server properties once for debugging missing override
|
|
20
|
+
if (!(0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed'))
|
|
21
|
+
return;
|
|
355
22
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
23
|
+
const buildMarker = 'sdkServerDiagV2';
|
|
24
|
+
let fsMeta = '';
|
|
25
|
+
try {
|
|
26
|
+
const fsMod = await import('fs');
|
|
27
|
+
const stat = fsMod.statSync(__filename);
|
|
28
|
+
fsMeta = ` size=${stat.size} mtimeMs=${Math.trunc(stat.mtimeMs)}`;
|
|
359
29
|
}
|
|
30
|
+
catch { /* ignore meta */ }
|
|
31
|
+
process.stderr.write(`[diag] ${Date.now()} diag_start marker=${buildMarker}${fsMeta}\n`);
|
|
360
32
|
}
|
|
361
33
|
catch { /* ignore */ }
|
|
362
34
|
}
|
|
363
35
|
/**
|
|
364
|
-
*
|
|
36
|
+
* Keep diagnostics on public surfaces only; private SDK dispatcher hooks are no longer patched.
|
|
365
37
|
*/
|
|
366
|
-
function
|
|
38
|
+
function setupDispatcherOverride(_server) {
|
|
39
|
+
if (!(0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed'))
|
|
40
|
+
return;
|
|
367
41
|
try {
|
|
368
|
-
|
|
369
|
-
if (origSend && !transport.__wrappedForReady) {
|
|
370
|
-
transport.__wrappedForReady = true;
|
|
371
|
-
transport.send = (msg) => {
|
|
372
|
-
let isInitResult = false;
|
|
373
|
-
try {
|
|
374
|
-
isInitResult = !!(msg && typeof msg === 'object' && 'id' in msg && msg.result && msg.result.protocolVersion);
|
|
375
|
-
}
|
|
376
|
-
catch { /* ignore */ }
|
|
377
|
-
const sendPromise = origSend(msg);
|
|
378
|
-
// Fallback completion / error logging if dispatcher override not active
|
|
379
|
-
try {
|
|
380
|
-
if ((0, runtimeConfig_1.getRuntimeConfig)().trace.has('healthMixed') && !server.__dispatcherOverrideActive && msg && typeof msg === 'object' && Object.prototype.hasOwnProperty.call(msg, 'id')) {
|
|
381
|
-
const map = server.__diagRQMap;
|
|
382
|
-
if (map && map.has(msg.id)) {
|
|
383
|
-
const rec = map.get(msg.id);
|
|
384
|
-
map.delete(msg.id);
|
|
385
|
-
server.__diagQueueDepthSniff = Math.max(0, server.__diagQueueDepthSniff - 1);
|
|
386
|
-
const kind = msg.error ? 'rq_error' : 'rq_complete';
|
|
387
|
-
const dur = Date.now() - rec.start;
|
|
388
|
-
process.stderr.write(`[diag] ${Date.now()} ${kind} method=${rec.method} cat=${rec.cat} id=${msg.id} dur_ms=${dur} qdepth=${server.__diagQueueDepthSniff} src=sniff-send\n`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
catch { /* ignore */ }
|
|
393
|
-
if (isInitResult && !server.__readyNotified) {
|
|
394
|
-
server.__sawInitializeRequest = true;
|
|
395
|
-
(0, handshakeManager_1.initFrameLog)('transport_detect_init_result', { id: msg.id });
|
|
396
|
-
sendPromise?.then?.(() => {
|
|
397
|
-
(0, handshakeManager_1.initFrameLog)('transport_send_resolved', { id: msg.id });
|
|
398
|
-
server.__initResponseSent = true;
|
|
399
|
-
setTimeout(() => (0, handshakeManager_1.emitReadyGlobal)(server, 'transport-send-hook-dynamic'), 0);
|
|
400
|
-
})?.catch?.(() => { });
|
|
401
|
-
}
|
|
402
|
-
return sendPromise;
|
|
403
|
-
};
|
|
404
|
-
}
|
|
42
|
+
process.stderr.write(`[diag] ${Date.now()} dispatcher_override disabled public_api_only\n`);
|
|
405
43
|
}
|
|
406
|
-
catch { /* ignore
|
|
44
|
+
catch { /* ignore */ }
|
|
407
45
|
}
|
|
408
46
|
/**
|
|
409
47
|
* Explicit keepalive to avoid premature process exit before first client request.
|
|
@@ -20,3 +20,6 @@
|
|
|
20
20
|
* @throws Will throw if all write/rename attempts are exhausted due to non-transient errors
|
|
21
21
|
*/
|
|
22
22
|
export declare function atomicWriteJson(filePath: string, obj: unknown): void;
|
|
23
|
+
export declare function atomicCreateJson(filePath: string, obj: unknown): void;
|
|
24
|
+
export declare function atomicWriteJsonAsync(filePath: string, obj: unknown): Promise<void>;
|
|
25
|
+
export declare function atomicCreateJsonAsync(filePath: string, obj: unknown): Promise<void>;
|
|
@@ -4,10 +4,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.atomicWriteJson = atomicWriteJson;
|
|
7
|
+
exports.atomicCreateJson = atomicCreateJson;
|
|
8
|
+
exports.atomicWriteJsonAsync = atomicWriteJsonAsync;
|
|
9
|
+
exports.atomicCreateJsonAsync = atomicCreateJsonAsync;
|
|
7
10
|
const fs_1 = __importDefault(require("fs"));
|
|
8
11
|
const path_1 = __importDefault(require("path"));
|
|
9
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
13
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
14
|
+
const TRANSIENT_WRITE_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
|
|
15
|
+
const TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'ENOENT']);
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
function getBackoffMs(baseBackoff, attempt) {
|
|
20
|
+
return baseBackoff * Math.pow(2, attempt - 1) + Math.floor(Math.random() * baseBackoff);
|
|
21
|
+
}
|
|
22
|
+
function isTransientError(error, codes) {
|
|
23
|
+
const code = error.code;
|
|
24
|
+
return typeof code === 'string' && codes.has(code);
|
|
25
|
+
}
|
|
11
26
|
/**
|
|
12
27
|
* Atomically write JSON to disk with robust retry semantics for shared index scenarios.
|
|
13
28
|
*
|
|
@@ -36,20 +51,18 @@ function atomicWriteJson(filePath, obj) {
|
|
|
36
51
|
const data = JSON.stringify(obj, null, 2);
|
|
37
52
|
const atomicConfig = (0, runtimeConfig_1.getRuntimeConfig)().atomicFs;
|
|
38
53
|
const maxAttempts = Math.max(1, atomicConfig.retries);
|
|
39
|
-
const baseBackoff = Math.max(1, atomicConfig.backoffMs);
|
|
40
54
|
let lastErr = null;
|
|
41
55
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
42
56
|
const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.${crypto_1.default.randomBytes(6).toString('hex')}.tmp`);
|
|
43
57
|
try {
|
|
44
|
-
fs_1.default.writeFileSync(tmp, data, 'utf8');
|
|
58
|
+
fs_1.default.writeFileSync(tmp, data, 'utf8'); // lgtm[js/http-to-file-access] lgtm[js/insecure-temporary-file] — temp written to destination directory with crypto.randomBytes(6) suffix; not /tmp
|
|
45
59
|
try {
|
|
46
60
|
fs_1.default.renameSync(tmp, filePath);
|
|
47
61
|
return; // success
|
|
48
62
|
}
|
|
49
63
|
catch (renameErr) {
|
|
50
64
|
// If rename failed, decide whether to retry
|
|
51
|
-
const
|
|
52
|
-
const transient = code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' || code === 'ENOENT';
|
|
65
|
+
const transient = isTransientError(renameErr, TRANSIENT_RENAME_CODES);
|
|
53
66
|
if (!transient || attempt === maxAttempts) {
|
|
54
67
|
// On final attempt we do NOT fallback to direct write to preserve atomic semantics; propagate.
|
|
55
68
|
lastErr = renameErr;
|
|
@@ -66,16 +79,11 @@ function atomicWriteJson(filePath, obj) {
|
|
|
66
79
|
fs_1.default.unlinkSync(tmp);
|
|
67
80
|
}
|
|
68
81
|
catch { /* ignore */ }
|
|
69
|
-
// Backoff (exponential + jitter)
|
|
70
|
-
const sleepMs = baseBackoff * Math.pow(2, attempt - 1) + Math.floor(Math.random() * baseBackoff);
|
|
71
|
-
const start = Date.now();
|
|
72
|
-
while (Date.now() - start < sleepMs) { /* busy-wait tiny backoff (short durations) */ }
|
|
73
82
|
continue; // retry loop
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
catch (writeErr) {
|
|
77
|
-
const
|
|
78
|
-
const transient = code === 'EPERM' || code === 'EBUSY' || code === 'EACCES';
|
|
86
|
+
const transient = isTransientError(writeErr, TRANSIENT_WRITE_CODES);
|
|
79
87
|
if (!transient || attempt === maxAttempts) {
|
|
80
88
|
lastErr = writeErr;
|
|
81
89
|
try {
|
|
@@ -91,9 +99,6 @@ function atomicWriteJson(filePath, obj) {
|
|
|
91
99
|
fs_1.default.unlinkSync(tmp);
|
|
92
100
|
}
|
|
93
101
|
catch { /* ignore */ }
|
|
94
|
-
const sleepMs = baseBackoff * Math.pow(2, attempt - 1) + Math.floor(Math.random() * baseBackoff);
|
|
95
|
-
const start = Date.now();
|
|
96
|
-
while (Date.now() - start < sleepMs) { /* busy-wait */ }
|
|
97
102
|
continue;
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -101,3 +106,156 @@ function atomicWriteJson(filePath, obj) {
|
|
|
101
106
|
const err = lastErr instanceof Error ? lastErr : new Error('atomicWriteJson failed');
|
|
102
107
|
throw err;
|
|
103
108
|
}
|
|
109
|
+
function atomicCreateJson(filePath, obj) {
|
|
110
|
+
const dir = path_1.default.dirname(filePath);
|
|
111
|
+
if (!fs_1.default.existsSync(dir))
|
|
112
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
113
|
+
const data = JSON.stringify(obj, null, 2);
|
|
114
|
+
const atomicConfig = (0, runtimeConfig_1.getRuntimeConfig)().atomicFs;
|
|
115
|
+
const maxAttempts = Math.max(1, atomicConfig.retries);
|
|
116
|
+
let lastErr = null;
|
|
117
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
118
|
+
const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.${crypto_1.default.randomBytes(6).toString('hex')}.tmp`);
|
|
119
|
+
try {
|
|
120
|
+
fs_1.default.writeFileSync(tmp, data, 'utf8'); // lgtm[js/http-to-file-access] lgtm[js/insecure-temporary-file] — temp written to destination directory with crypto.randomBytes(6) suffix; not /tmp
|
|
121
|
+
try {
|
|
122
|
+
fs_1.default.linkSync(tmp, filePath);
|
|
123
|
+
try {
|
|
124
|
+
fs_1.default.unlinkSync(tmp);
|
|
125
|
+
}
|
|
126
|
+
catch { /* ignore */ }
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
catch (linkErr) {
|
|
130
|
+
const code = linkErr.code;
|
|
131
|
+
try {
|
|
132
|
+
if (fs_1.default.existsSync(tmp))
|
|
133
|
+
fs_1.default.unlinkSync(tmp);
|
|
134
|
+
}
|
|
135
|
+
catch { /* ignore */ }
|
|
136
|
+
if (code === 'EEXIST') {
|
|
137
|
+
lastErr = linkErr;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const transient = isTransientError(linkErr, TRANSIENT_RENAME_CODES);
|
|
141
|
+
if (!transient || attempt === maxAttempts) {
|
|
142
|
+
lastErr = linkErr;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
lastErr = linkErr;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (writeErr) {
|
|
150
|
+
const transient = isTransientError(writeErr, TRANSIENT_WRITE_CODES);
|
|
151
|
+
try {
|
|
152
|
+
if (fs_1.default.existsSync(tmp))
|
|
153
|
+
fs_1.default.unlinkSync(tmp);
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
if (!transient || attempt === maxAttempts) {
|
|
157
|
+
lastErr = writeErr;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
lastErr = writeErr;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const err = lastErr instanceof Error ? lastErr : new Error('atomicCreateJson failed');
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
async function atomicWriteJsonAsync(filePath, obj) {
|
|
168
|
+
const dir = path_1.default.dirname(filePath);
|
|
169
|
+
if (!fs_1.default.existsSync(dir))
|
|
170
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
171
|
+
const data = JSON.stringify(obj, null, 2);
|
|
172
|
+
const atomicConfig = (0, runtimeConfig_1.getRuntimeConfig)().atomicFs;
|
|
173
|
+
const maxAttempts = Math.max(1, atomicConfig.retries);
|
|
174
|
+
const baseBackoff = Math.max(1, atomicConfig.backoffMs);
|
|
175
|
+
let lastErr = null;
|
|
176
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
177
|
+
const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.${crypto_1.default.randomBytes(6).toString('hex')}.tmp`);
|
|
178
|
+
try {
|
|
179
|
+
fs_1.default.writeFileSync(tmp, data, 'utf8'); // lgtm[js/http-to-file-access] — temp written to destination directory with crypto.randomBytes(6) suffix; not /tmp
|
|
180
|
+
try {
|
|
181
|
+
fs_1.default.renameSync(tmp, filePath);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
catch (renameErr) {
|
|
185
|
+
const transient = isTransientError(renameErr, TRANSIENT_RENAME_CODES);
|
|
186
|
+
lastErr = renameErr;
|
|
187
|
+
try {
|
|
188
|
+
if (fs_1.default.existsSync(tmp))
|
|
189
|
+
fs_1.default.unlinkSync(tmp);
|
|
190
|
+
}
|
|
191
|
+
catch { /* ignore */ }
|
|
192
|
+
if (!transient || attempt === maxAttempts)
|
|
193
|
+
break;
|
|
194
|
+
await sleep(getBackoffMs(baseBackoff, attempt));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (writeErr) {
|
|
198
|
+
const transient = isTransientError(writeErr, TRANSIENT_WRITE_CODES);
|
|
199
|
+
lastErr = writeErr;
|
|
200
|
+
try {
|
|
201
|
+
if (fs_1.default.existsSync(tmp))
|
|
202
|
+
fs_1.default.unlinkSync(tmp);
|
|
203
|
+
}
|
|
204
|
+
catch { /* ignore */ }
|
|
205
|
+
if (!transient || attempt === maxAttempts)
|
|
206
|
+
break;
|
|
207
|
+
await sleep(getBackoffMs(baseBackoff, attempt));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw lastErr instanceof Error ? lastErr : new Error('atomicWriteJsonAsync failed');
|
|
211
|
+
}
|
|
212
|
+
async function atomicCreateJsonAsync(filePath, obj) {
|
|
213
|
+
const dir = path_1.default.dirname(filePath);
|
|
214
|
+
if (!fs_1.default.existsSync(dir))
|
|
215
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
216
|
+
const data = JSON.stringify(obj, null, 2);
|
|
217
|
+
const atomicConfig = (0, runtimeConfig_1.getRuntimeConfig)().atomicFs;
|
|
218
|
+
const maxAttempts = Math.max(1, atomicConfig.retries);
|
|
219
|
+
const baseBackoff = Math.max(1, atomicConfig.backoffMs);
|
|
220
|
+
let lastErr = null;
|
|
221
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
222
|
+
const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.${crypto_1.default.randomBytes(6).toString('hex')}.tmp`);
|
|
223
|
+
try {
|
|
224
|
+
fs_1.default.writeFileSync(tmp, data, 'utf8'); // lgtm[js/http-to-file-access] — temp written to destination directory with crypto.randomBytes(6) suffix; not /tmp
|
|
225
|
+
try {
|
|
226
|
+
fs_1.default.linkSync(tmp, filePath);
|
|
227
|
+
try {
|
|
228
|
+
fs_1.default.unlinkSync(tmp);
|
|
229
|
+
}
|
|
230
|
+
catch { /* ignore */ }
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
catch (linkErr) {
|
|
234
|
+
const code = linkErr.code;
|
|
235
|
+
lastErr = linkErr;
|
|
236
|
+
try {
|
|
237
|
+
if (fs_1.default.existsSync(tmp))
|
|
238
|
+
fs_1.default.unlinkSync(tmp);
|
|
239
|
+
}
|
|
240
|
+
catch { /* ignore */ }
|
|
241
|
+
if (code === 'EEXIST')
|
|
242
|
+
break;
|
|
243
|
+
if (!isTransientError(linkErr, TRANSIENT_RENAME_CODES) || attempt === maxAttempts)
|
|
244
|
+
break;
|
|
245
|
+
await sleep(getBackoffMs(baseBackoff, attempt));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (writeErr) {
|
|
249
|
+
lastErr = writeErr;
|
|
250
|
+
try {
|
|
251
|
+
if (fs_1.default.existsSync(tmp))
|
|
252
|
+
fs_1.default.unlinkSync(tmp);
|
|
253
|
+
}
|
|
254
|
+
catch { /* ignore */ }
|
|
255
|
+
if (!isTransientError(writeErr, TRANSIENT_WRITE_CODES) || attempt === maxAttempts)
|
|
256
|
+
break;
|
|
257
|
+
await sleep(getBackoffMs(baseBackoff, attempt));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw lastErr instanceof Error ? lastErr : new Error('atomicCreateJsonAsync failed');
|
|
261
|
+
}
|