target-supabase-sdk 0.2.12 → 0.2.14
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/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/chunks/enable-C7lFooTG.js +874 -0
- package/dist/chunks/enable-C7lFooTG.js.map +1 -0
- package/dist/chunks/list.api-BzbTJBsM.js.map +1 -1
- package/dist/chunks/{task-queue-H4mpSW8R.js → task-queue-qSnsfiIg.js} +111 -4
- package/dist/chunks/task-queue-qSnsfiIg.js.map +1 -0
- package/dist/node/env/index.d.ts +1 -1
- package/dist/node/env/index.d.ts.map +1 -1
- package/dist/node/node-runtime.base.d.ts +14 -1
- package/dist/node/node-runtime.base.d.ts.map +1 -1
- package/dist/node/node.api.d.ts.map +1 -1
- package/dist/node/node.constant.d.ts +3 -0
- package/dist/node/node.constant.d.ts.map +1 -0
- package/dist/node/node.utils.d.ts +0 -2
- package/dist/node/node.utils.d.ts.map +1 -1
- package/dist/node/preload/run-service-preload.d.ts.map +1 -1
- package/dist/node/preload/service-preload.interface.d.ts +3 -1
- package/dist/node/preload/service-preload.interface.d.ts.map +1 -1
- package/dist/node/process/managed-child-processes.d.ts.map +1 -1
- package/dist/node/readiness/index.d.ts +2 -0
- package/dist/node/readiness/index.d.ts.map +1 -1
- package/dist/node/{service-guard → readiness}/run-readiness-gate.d.ts +1 -1
- package/dist/node/readiness/run-readiness-gate.d.ts.map +1 -0
- package/dist/node/readiness/service-ready-gate.d.ts.map +1 -1
- package/dist/node/runtime-state/create-service-runtime-state-store.d.ts.map +1 -1
- package/dist/node/runtime-state/service-runtime-state.types.d.ts +16 -0
- package/dist/node/runtime-state/service-runtime-state.types.d.ts.map +1 -1
- package/dist/node/service-guard/index.d.ts +4 -4
- package/dist/node/service-guard/index.d.ts.map +1 -1
- package/dist/node/service-guard/register-service-guard-runner.d.ts +1 -2
- package/dist/node/service-guard/register-service-guard-runner.d.ts.map +1 -1
- package/dist/node/service-guard/registry-slot-guard-step.d.ts.map +1 -0
- package/dist/node/service-guard/run-service-guard-tick.d.ts.map +1 -1
- package/dist/node/service-guard/service-guard-node.d.ts +25 -2
- package/dist/node/service-guard/service-guard-node.d.ts.map +1 -1
- package/dist/node/service-guard/service-guard.interface.d.ts +25 -10
- package/dist/node/service-guard/service-guard.interface.d.ts.map +1 -1
- package/dist/node/service-host/claim-registry-slot.d.ts +15 -0
- package/dist/node/service-host/claim-registry-slot.d.ts.map +1 -0
- package/dist/node/service-host/create-l3-child-launcher.d.ts +38 -0
- package/dist/node/service-host/create-l3-child-launcher.d.ts.map +1 -0
- package/dist/node/service-host/index.d.ts +2 -4
- package/dist/node/service-host/index.d.ts.map +1 -1
- package/dist/node/service-host/service-host.d.ts +8 -4
- package/dist/node/service-host/service-host.d.ts.map +1 -1
- package/dist/node/service-host/single-process-service.d.ts +3 -3
- package/dist/node/service-host/single-process-service.d.ts.map +1 -1
- package/dist/node.d.ts +17 -10
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +927 -119
- package/dist/node.js.map +1 -1
- package/dist/preload.js +9 -6
- package/dist/preload.js.map +1 -1
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.d.ts.map +1 -1
- package/dist/service/registry.service.d.ts +19 -0
- package/dist/service/registry.service.d.ts.map +1 -1
- package/dist/shared/log/core/create-logger.d.ts.map +1 -0
- package/dist/shared/log/{log-batch.d.ts → core/log-batch.d.ts} +4 -4
- package/dist/shared/log/core/log-batch.d.ts.map +1 -0
- package/dist/shared/log/core/log-manager.d.ts.map +1 -0
- package/dist/shared/log/core/log-min-level.d.ts.map +1 -0
- package/dist/shared/log/core/log-scope.d.ts.map +1 -0
- package/dist/shared/log/index.d.ts +13 -13
- package/dist/shared/log/index.d.ts.map +1 -1
- package/dist/shared/log/spool/collector.d.ts +21 -0
- package/dist/shared/log/spool/collector.d.ts.map +1 -0
- package/dist/shared/log/spool/config.d.ts +7 -0
- package/dist/shared/log/spool/config.d.ts.map +1 -0
- package/dist/shared/log/spool/coordinator.d.ts +18 -0
- package/dist/shared/log/spool/coordinator.d.ts.map +1 -0
- package/dist/shared/log/spool/enable.d.ts +23 -0
- package/dist/shared/log/spool/enable.d.ts.map +1 -0
- package/dist/shared/log/spool/file.d.ts +21 -0
- package/dist/shared/log/spool/file.d.ts.map +1 -0
- package/dist/shared/log/spool/interface.d.ts +55 -0
- package/dist/shared/log/spool/interface.d.ts.map +1 -0
- package/dist/shared/log/spool/paths.d.ts +6 -0
- package/dist/shared/log/spool/paths.d.ts.map +1 -0
- package/dist/shared/log/spool/process-roles.d.ts +17 -0
- package/dist/shared/log/spool/process-roles.d.ts.map +1 -0
- package/dist/shared/log/spool/register-collect-log-runner.d.ts +15 -0
- package/dist/shared/log/spool/register-collect-log-runner.d.ts.map +1 -0
- package/dist/shared/log/spool/service-lifecycle.d.ts +13 -0
- package/dist/shared/log/spool/service-lifecycle.d.ts.map +1 -0
- package/dist/shared/log/spool/writer.d.ts +32 -0
- package/dist/shared/log/spool/writer.d.ts.map +1 -0
- package/dist/shared/log/{log-persist-batch-id.d.ts → upload/batch-id.d.ts} +3 -3
- package/dist/shared/log/upload/batch-id.d.ts.map +1 -0
- package/dist/shared/log/upload/env.d.ts +7 -0
- package/dist/shared/log/upload/env.d.ts.map +1 -0
- package/dist/shared/log/{log-persist-flush.d.ts → upload/flush.d.ts} +3 -3
- package/dist/shared/log/upload/flush.d.ts.map +1 -0
- package/dist/shared/log/{log-persist-hook.d.ts → upload/hook.d.ts} +2 -2
- package/dist/shared/log/upload/hook.d.ts.map +1 -0
- package/dist/shared/log/upload/interface.d.ts +33 -0
- package/dist/shared/log/upload/interface.d.ts.map +1 -0
- package/dist/shared/log/upload/logger.d.ts +22 -0
- package/dist/shared/log/upload/logger.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/browser.ts +1 -0
- package/src/node/env/index.ts +0 -4
- package/src/node/node-runtime.base.ts +33 -5
- package/src/node/node.api.ts +1 -1
- package/src/node/node.constant.ts +2 -0
- package/src/node/node.utils.ts +2 -2
- package/src/node/preload/run-service-preload.ts +11 -6
- package/src/node/preload/service-preload.interface.ts +3 -1
- package/src/node/process/create-managed-child-processes.ts +1 -1
- package/src/node/process/managed-child-processes.ts +8 -1
- package/src/node/readiness/index.ts +2 -0
- package/src/node/{service-guard → readiness}/run-readiness-gate.ts +2 -2
- package/src/node/readiness/service-ready-gate.ts +27 -23
- package/src/node/runtime-state/create-service-runtime-state-store.ts +11 -0
- package/src/node/runtime-state/service-runtime-state.types.ts +12 -0
- package/src/node/service-guard/index.ts +7 -8
- package/src/node/service-guard/register-service-guard-runner.ts +0 -2
- package/src/node/service-guard/run-service-guard-tick.ts +5 -2
- package/src/node/service-guard/service-guard-node.ts +234 -6
- package/src/node/service-guard/service-guard.interface.ts +35 -10
- package/src/node/service-host/claim-registry-slot.ts +33 -0
- package/src/node/service-host/create-l3-child-launcher.ts +157 -0
- package/src/node/service-host/index.ts +5 -20
- package/src/node/service-host/service-host.ts +33 -21
- package/src/node/service-host/single-process-service.ts +10 -20
- package/src/node.ts +45 -35
- package/src/service/index.ts +3 -0
- package/src/service/registry.service.ts +142 -0
- package/src/shared/log/README.md +206 -0
- package/src/shared/log/{log-batch.ts → core/log-batch.ts} +4 -4
- package/src/shared/log/{log-manager.ts → core/log-manager.ts} +3 -3
- package/src/shared/log/{log-min-level.ts → core/log-min-level.ts} +1 -1
- package/src/shared/log/{log-scope.ts → core/log-scope.ts} +1 -1
- package/src/shared/log/index.ts +13 -18
- package/src/shared/log/spool/collector.ts +330 -0
- package/src/shared/log/spool/config.ts +56 -0
- package/src/shared/log/spool/coordinator.ts +55 -0
- package/src/shared/log/spool/enable.ts +136 -0
- package/src/shared/log/spool/file.ts +75 -0
- package/src/shared/log/spool/interface.ts +62 -0
- package/src/shared/log/spool/paths.ts +30 -0
- package/src/shared/log/spool/process-roles.ts +75 -0
- package/src/shared/log/spool/register-collect-log-runner.ts +63 -0
- package/src/shared/log/spool/service-lifecycle.ts +35 -0
- package/src/shared/log/spool/writer.ts +273 -0
- package/src/shared/log/{log-persist-batch-id.ts → upload/batch-id.ts} +2 -2
- package/src/shared/log/upload/env.ts +26 -0
- package/src/shared/log/{log-persist-flush.ts → upload/flush.ts} +8 -7
- package/src/shared/log/{log-persist-hook.ts → upload/hook.ts} +1 -1
- package/src/shared/log/upload/interface.ts +38 -0
- package/src/shared/log/upload/logger.ts +37 -0
- package/dist/chunks/enable-log-persist-Dl55_mv2.js +0 -1674
- package/dist/chunks/enable-log-persist-Dl55_mv2.js.map +0 -1
- package/dist/chunks/task-queue-H4mpSW8R.js.map +0 -1
- package/dist/node/service-guard/run-readiness-gate.d.ts.map +0 -1
- package/dist/node/service-host/registry-slot-guard-step.d.ts.map +0 -1
- package/dist/shared/log/create-logger.d.ts.map +0 -1
- package/dist/shared/log/enable-log-persist.d.ts +0 -28
- package/dist/shared/log/enable-log-persist.d.ts.map +0 -1
- package/dist/shared/log/log-batch.d.ts.map +0 -1
- package/dist/shared/log/log-manager.d.ts.map +0 -1
- package/dist/shared/log/log-min-level.d.ts.map +0 -1
- package/dist/shared/log/log-persist-batch-id.d.ts.map +0 -1
- package/dist/shared/log/log-persist-coordinator.d.ts +0 -37
- package/dist/shared/log/log-persist-coordinator.d.ts.map +0 -1
- package/dist/shared/log/log-persist-errors.d.ts +0 -4
- package/dist/shared/log/log-persist-errors.d.ts.map +0 -1
- package/dist/shared/log/log-persist-flush.d.ts.map +0 -1
- package/dist/shared/log/log-persist-hook.d.ts.map +0 -1
- package/dist/shared/log/log-persist-logger.d.ts +0 -16
- package/dist/shared/log/log-persist-logger.d.ts.map +0 -1
- package/dist/shared/log/log-persist-registry.d.ts +0 -18
- package/dist/shared/log/log-persist-registry.d.ts.map +0 -1
- package/dist/shared/log/log-persist.config.d.ts +0 -9
- package/dist/shared/log/log-persist.config.d.ts.map +0 -1
- package/dist/shared/log/log-persist.d.ts +0 -62
- package/dist/shared/log/log-persist.d.ts.map +0 -1
- package/dist/shared/log/log-persist.interface.d.ts +0 -132
- package/dist/shared/log/log-persist.interface.d.ts.map +0 -1
- package/dist/shared/log/log-scope.d.ts.map +0 -1
- package/src/shared/log/enable-log-persist.ts +0 -225
- package/src/shared/log/log-persist-coordinator.ts +0 -107
- package/src/shared/log/log-persist-errors.ts +0 -16
- package/src/shared/log/log-persist-logger.ts +0 -61
- package/src/shared/log/log-persist-registry.ts +0 -252
- package/src/shared/log/log-persist.config.ts +0 -115
- package/src/shared/log/log-persist.interface.ts +0 -150
- package/src/shared/log/log-persist.ts +0 -822
- /package/dist/node/{service-host → service-guard}/registry-slot-guard-step.d.ts +0 -0
- /package/dist/shared/log/{create-logger.d.ts → core/create-logger.d.ts} +0 -0
- /package/dist/shared/log/{log-manager.d.ts → core/log-manager.d.ts} +0 -0
- /package/dist/shared/log/{log-min-level.d.ts → core/log-min-level.d.ts} +0 -0
- /package/dist/shared/log/{log-scope.d.ts → core/log-scope.d.ts} +0 -0
- /package/src/node/{service-host → service-guard}/registry-slot-guard-step.ts +0 -0
- /package/src/shared/log/{create-logger.ts → core/create-logger.ts} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { access, constants, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isLogEntry } from "../core/log-batch";
|
|
4
|
+
import { computeLogBatchIdempotencyKey } from "../upload/batch-id";
|
|
5
|
+
import { buildLogListDraft, postLogBatch } from "../upload/flush";
|
|
6
|
+
import { spoolLogger } from "../upload/logger";
|
|
7
|
+
import { type DEFAULT_LOG_SPOOL_CONFIG, resolveLogSpoolConfig } from "./config";
|
|
8
|
+
import { parseLogSpoolBatchFilename } from "./file";
|
|
9
|
+
import type { LogSpoolBatchFile, LogSpoolProcessRole } from "./interface";
|
|
10
|
+
import { logSpoolProcessDir, resolveLogSpoolRoot } from "./paths";
|
|
11
|
+
import { resolveAllLogSpoolProcessRoles } from "./process-roles";
|
|
12
|
+
|
|
13
|
+
const UPLOAD_LANE = "slow";
|
|
14
|
+
|
|
15
|
+
/** Per serviceId — guard collect-log tick runs GC at most once per `gc.intervalMs`. */
|
|
16
|
+
const lastGcAtMsByServiceId = new Map<string, number>();
|
|
17
|
+
|
|
18
|
+
export interface RunCollectLogTickInput {
|
|
19
|
+
serviceId: string;
|
|
20
|
+
serviceValue: string;
|
|
21
|
+
spoolRoot?: string;
|
|
22
|
+
config?: Partial<typeof DEFAULT_LOG_SPOOL_CONFIG>;
|
|
23
|
+
nowMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RunCollectLogTickResult {
|
|
27
|
+
uploaded: number;
|
|
28
|
+
skipped: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
gcTmpRemoved: number;
|
|
31
|
+
gcSyncedRemoved: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
await access(filePath, constants.F_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readBatchTmpFile(tmpPath: string): Promise<LogSpoolBatchFile | null> {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(tmpPath, "utf8");
|
|
46
|
+
const parsed = JSON.parse(raw) as LogSpoolBatchFile;
|
|
47
|
+
if (!Array.isArray(parsed.entries) || parsed.entries.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const entries = parsed.entries.filter(isLogEntry);
|
|
51
|
+
if (entries.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { entries, meta: parsed.meta };
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function processMetaKey(serviceId: string, processRole: LogSpoolProcessRole): string {
|
|
61
|
+
return `${serviceId}:${processRole}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function uploadTmpBatch(input: {
|
|
65
|
+
serviceId: string;
|
|
66
|
+
serviceValue: string;
|
|
67
|
+
processRole: LogSpoolProcessRole;
|
|
68
|
+
tmpPath: string;
|
|
69
|
+
fileName: string;
|
|
70
|
+
spoolRoot: string;
|
|
71
|
+
postTimeoutMs: number;
|
|
72
|
+
}): Promise<"uploaded" | "skipped" | "failed"> {
|
|
73
|
+
const parsed = parseLogSpoolBatchFilename(input.fileName);
|
|
74
|
+
if (parsed == null) {
|
|
75
|
+
spoolLogger.warn("collect-log skipped — invalid tmp filename", { fileName: input.fileName });
|
|
76
|
+
return "failed";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const jsonFileName = input.fileName.replace(/\.tmp$/, ".json");
|
|
80
|
+
const jsonPath = join(logSpoolProcessDir(input.spoolRoot, input.serviceId, input.processRole), jsonFileName);
|
|
81
|
+
|
|
82
|
+
if (await pathExists(jsonPath)) {
|
|
83
|
+
try {
|
|
84
|
+
await unlink(input.tmpPath);
|
|
85
|
+
spoolLogger.info("collect-log removed orphan tmp (json exists)", {
|
|
86
|
+
tmpPath: input.tmpPath,
|
|
87
|
+
jsonPath,
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// best-effort
|
|
91
|
+
}
|
|
92
|
+
return "skipped";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const batch = await readBatchTmpFile(input.tmpPath);
|
|
96
|
+
if (batch == null) {
|
|
97
|
+
spoolLogger.warn("collect-log skipped — tmp failed validation", {
|
|
98
|
+
tmpPath: input.tmpPath,
|
|
99
|
+
fileName: input.fileName,
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
await unlink(input.tmpPath);
|
|
103
|
+
spoolLogger.info("collect-log removed invalid tmp", { tmpPath: input.tmpPath });
|
|
104
|
+
} catch {
|
|
105
|
+
// best-effort
|
|
106
|
+
}
|
|
107
|
+
return "failed";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const processKey = processMetaKey(input.serviceId, input.processRole);
|
|
111
|
+
// TODO: `lane` is legacy transport metadata — scheduled for removal; fixed to "slow" for downstream decode compatibility.
|
|
112
|
+
const idempotencyKey = computeLogBatchIdempotencyKey({
|
|
113
|
+
service: input.serviceValue,
|
|
114
|
+
process: processKey,
|
|
115
|
+
lane: UPLOAD_LANE,
|
|
116
|
+
entries: batch.entries,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const draft = buildLogListDraft({
|
|
120
|
+
service: input.serviceValue,
|
|
121
|
+
process: processKey,
|
|
122
|
+
lane: UPLOAD_LANE,
|
|
123
|
+
idempotencyKey,
|
|
124
|
+
entries: batch.entries,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await postLogBatch(draft, input.postTimeoutMs);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
spoolLogger.error("collect-log upload failed", {
|
|
132
|
+
fileName: input.fileName,
|
|
133
|
+
processRole: input.processRole,
|
|
134
|
+
error: message,
|
|
135
|
+
});
|
|
136
|
+
return "failed";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
await rename(input.tmpPath, jsonPath);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
+
spoolLogger.error("collect-log rename tmp→json failed after upload", {
|
|
144
|
+
tmpPath: input.tmpPath,
|
|
145
|
+
jsonPath,
|
|
146
|
+
error: message,
|
|
147
|
+
});
|
|
148
|
+
return "failed";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
spoolLogger.info("collect-log uploaded batch", {
|
|
152
|
+
processRole: input.processRole,
|
|
153
|
+
fileName: jsonFileName,
|
|
154
|
+
entryCount: batch.entries.length,
|
|
155
|
+
idempotencyKey,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return "uploaded";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function gcTmpFiles(input: {
|
|
162
|
+
spoolRoot: string;
|
|
163
|
+
serviceId: string;
|
|
164
|
+
processRole: LogSpoolProcessRole;
|
|
165
|
+
tmpRetentionMs: number;
|
|
166
|
+
nowMs: number;
|
|
167
|
+
}): Promise<number> {
|
|
168
|
+
const processDir = logSpoolProcessDir(input.spoolRoot, input.serviceId, input.processRole);
|
|
169
|
+
let removed = 0;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const names = await readdir(processDir);
|
|
173
|
+
for (const name of names) {
|
|
174
|
+
if (!name.endsWith(".tmp")) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const parsed = parseLogSpoolBatchFilename(name);
|
|
178
|
+
if (parsed == null) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (input.nowMs - parsed.timestampMs <= input.tmpRetentionMs) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await unlink(join(processDir, name));
|
|
186
|
+
removed += 1;
|
|
187
|
+
spoolLogger.info("collect-log gc removed stale tmp", {
|
|
188
|
+
fileName: name,
|
|
189
|
+
processRole: input.processRole,
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
// best-effort
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// directory may not exist yet
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return removed;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function gcSyncedJsonFiles(input: {
|
|
203
|
+
spoolRoot: string;
|
|
204
|
+
serviceId: string;
|
|
205
|
+
syncedRetentionMs: number;
|
|
206
|
+
nowMs: number;
|
|
207
|
+
}): Promise<number> {
|
|
208
|
+
let removed = 0;
|
|
209
|
+
|
|
210
|
+
for (const processRole of resolveAllLogSpoolProcessRoles()) {
|
|
211
|
+
const processDir = logSpoolProcessDir(input.spoolRoot, input.serviceId, processRole);
|
|
212
|
+
let names: string[] = [];
|
|
213
|
+
try {
|
|
214
|
+
names = await readdir(processDir);
|
|
215
|
+
} catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const name of names) {
|
|
220
|
+
if (!name.endsWith(".json")) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (parseLogSpoolBatchFilename(name) == null) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const jsonPath = join(processDir, name);
|
|
228
|
+
try {
|
|
229
|
+
const fileStat = await stat(jsonPath);
|
|
230
|
+
if (input.nowMs - fileStat.mtimeMs <= input.syncedRetentionMs) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
await unlink(jsonPath);
|
|
234
|
+
removed += 1;
|
|
235
|
+
spoolLogger.info("collect-log gc removed synced json", {
|
|
236
|
+
fileName: name,
|
|
237
|
+
processRole,
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
// best-effort
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return removed;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function shouldRunGc(serviceId: string, nowMs: number, intervalMs: number): boolean {
|
|
249
|
+
const lastAt = lastGcAtMsByServiceId.get(serviceId) ?? 0;
|
|
250
|
+
if (nowMs - lastAt < intervalMs) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
lastGcAtMsByServiceId.set(serviceId, nowMs);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Guard-only: upload pending `.tmp` batches, rename to `.json`, then periodic GC.
|
|
259
|
+
* Upload completion is implied by `.json` presence; no sync-state file.
|
|
260
|
+
*/
|
|
261
|
+
export async function runCollectLogTick(input: RunCollectLogTickInput): Promise<RunCollectLogTickResult> {
|
|
262
|
+
const config = resolveLogSpoolConfig(input.config);
|
|
263
|
+
const spoolRoot = input.spoolRoot ?? resolveLogSpoolRoot();
|
|
264
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
265
|
+
|
|
266
|
+
const result: RunCollectLogTickResult = {
|
|
267
|
+
uploaded: 0,
|
|
268
|
+
skipped: 0,
|
|
269
|
+
failed: 0,
|
|
270
|
+
gcTmpRemoved: 0,
|
|
271
|
+
gcSyncedRemoved: 0,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
for (const processRole of resolveAllLogSpoolProcessRoles()) {
|
|
275
|
+
const processDir = logSpoolProcessDir(spoolRoot, input.serviceId, processRole);
|
|
276
|
+
let names: string[] = [];
|
|
277
|
+
try {
|
|
278
|
+
names = await readdir(processDir);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const name of names) {
|
|
284
|
+
if (!name.endsWith(".tmp")) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const status = await uploadTmpBatch({
|
|
288
|
+
serviceId: input.serviceId,
|
|
289
|
+
serviceValue: input.serviceValue,
|
|
290
|
+
processRole,
|
|
291
|
+
tmpPath: join(processDir, name),
|
|
292
|
+
fileName: name,
|
|
293
|
+
spoolRoot,
|
|
294
|
+
postTimeoutMs: config.writer.postTimeoutMs,
|
|
295
|
+
});
|
|
296
|
+
if (status === "uploaded") {
|
|
297
|
+
result.uploaded += 1;
|
|
298
|
+
} else if (status === "skipped") {
|
|
299
|
+
result.skipped += 1;
|
|
300
|
+
} else {
|
|
301
|
+
result.failed += 1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (shouldRunGc(input.serviceId, nowMs, config.gc.intervalMs)) {
|
|
307
|
+
for (const processRole of resolveAllLogSpoolProcessRoles()) {
|
|
308
|
+
result.gcTmpRemoved += await gcTmpFiles({
|
|
309
|
+
spoolRoot,
|
|
310
|
+
serviceId: input.serviceId,
|
|
311
|
+
processRole,
|
|
312
|
+
tmpRetentionMs: config.gc.tmpRetentionMs,
|
|
313
|
+
nowMs,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
result.gcSyncedRemoved = await gcSyncedJsonFiles({
|
|
318
|
+
spoolRoot,
|
|
319
|
+
serviceId: input.serviceId,
|
|
320
|
+
syncedRetentionMs: config.gc.syncedRetentionMs,
|
|
321
|
+
nowMs,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (result.uploaded > 0 || result.failed > 0) {
|
|
326
|
+
spoolLogger.info("collect-log tick complete", { ...result, serviceId: input.serviceId });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { envInt, envMs } from "../../../node/env/env-parsers";
|
|
2
|
+
import type { LogSpoolConfig } from "./interface";
|
|
3
|
+
|
|
4
|
+
export const LOG_SPOOL_SERVICE_ID_ENV = "LOG_SPOOL_SERVICE_ID";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_LOG_SPOOL_CONFIG: LogSpoolConfig = {
|
|
7
|
+
writer: {
|
|
8
|
+
maxEntries: 2000,
|
|
9
|
+
maxBytes: 2 * 1024 * 1024,
|
|
10
|
+
flushDebounceMs: 3_000,
|
|
11
|
+
maxAgeMs: 60_000,
|
|
12
|
+
postTimeoutMs: 30_000,
|
|
13
|
+
},
|
|
14
|
+
gc: {
|
|
15
|
+
intervalMs: 24 * 60 * 60 * 1000,
|
|
16
|
+
syncedRetentionMs: 7 * 24 * 60 * 60 * 1000,
|
|
17
|
+
tmpRetentionMs: 7 * 24 * 60 * 60 * 1000,
|
|
18
|
+
},
|
|
19
|
+
collectIntervalMs: 60_000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function resolveLogSpoolConfig(partial?: Partial<LogSpoolConfig>): LogSpoolConfig {
|
|
23
|
+
const defaults = DEFAULT_LOG_SPOOL_CONFIG;
|
|
24
|
+
return {
|
|
25
|
+
writer: { ...defaults.writer, ...partial?.writer },
|
|
26
|
+
gc: { ...defaults.gc, ...partial?.gc },
|
|
27
|
+
collectIntervalMs: partial?.collectIntervalMs ?? defaults.collectIntervalMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function logSpoolConfigFromEnv(env: NodeJS.ProcessEnv = process.env): LogSpoolConfig {
|
|
32
|
+
return resolveLogSpoolConfig({
|
|
33
|
+
writer: {
|
|
34
|
+
maxEntries: envInt("LOG_SPOOL_MAX_ENTRIES", DEFAULT_LOG_SPOOL_CONFIG.writer.maxEntries, { env, min: 1 }),
|
|
35
|
+
maxBytes: envInt("LOG_SPOOL_MAX_BYTES", DEFAULT_LOG_SPOOL_CONFIG.writer.maxBytes, { env, min: 1 }),
|
|
36
|
+
flushDebounceMs: envMs("LOG_SPOOL_FLUSH_DEBOUNCE_MS", DEFAULT_LOG_SPOOL_CONFIG.writer.flushDebounceMs, {
|
|
37
|
+
env,
|
|
38
|
+
}),
|
|
39
|
+
maxAgeMs: envMs("LOG_SPOOL_MAX_AGE_MS", DEFAULT_LOG_SPOOL_CONFIG.writer.maxAgeMs, { env }),
|
|
40
|
+
postTimeoutMs: envMs("LOG_PERSIST_POST_TIMEOUT_MS", DEFAULT_LOG_SPOOL_CONFIG.writer.postTimeoutMs, { env }),
|
|
41
|
+
},
|
|
42
|
+
gc: {
|
|
43
|
+
intervalMs: envMs("LOG_SPOOL_GC_INTERVAL_MS", DEFAULT_LOG_SPOOL_CONFIG.gc.intervalMs, { env }),
|
|
44
|
+
syncedRetentionMs: envMs("LOG_SPOOL_SYNCED_RETENTION_MS", DEFAULT_LOG_SPOOL_CONFIG.gc.syncedRetentionMs, {
|
|
45
|
+
env,
|
|
46
|
+
}),
|
|
47
|
+
tmpRetentionMs: envMs("LOG_SPOOL_TMP_RETENTION_MS", DEFAULT_LOG_SPOOL_CONFIG.gc.tmpRetentionMs, { env }),
|
|
48
|
+
},
|
|
49
|
+
collectIntervalMs: envMs("LOG_SPOOL_COLLECT_INTERVAL_MS", DEFAULT_LOG_SPOOL_CONFIG.collectIntervalMs, { env }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logSpoolServiceIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
54
|
+
const raw = env[LOG_SPOOL_SERVICE_ID_ENV]?.trim();
|
|
55
|
+
return raw != null && raw !== "" ? raw : undefined;
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ensureLogSpoolFromEnv, getLogSpoolStats, logSpoolEnabledFromEnv, shutdownLogSpoolFromEnv } from "./enable";
|
|
2
|
+
import type { LogSpoolWriterStats } from "./interface";
|
|
3
|
+
|
|
4
|
+
export interface LogSpoolCoordinatorOptions {
|
|
5
|
+
/** Logical service value (e.g. `watch-service`). */
|
|
6
|
+
serviceValue: string;
|
|
7
|
+
/** Instance id after registry claim — passed to child spawn env. */
|
|
8
|
+
serviceId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Service-bound facade for file log spool writer lifecycle. */
|
|
12
|
+
export interface LogSpoolCoordinator {
|
|
13
|
+
isEnabled(): boolean;
|
|
14
|
+
getServiceId(): string;
|
|
15
|
+
/** Enable spool writer in this process. */
|
|
16
|
+
registerProcess(process?: string): Promise<boolean>;
|
|
17
|
+
getWriterStats(): LogSpoolWriterStats | null;
|
|
18
|
+
shutdownLogSpool(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createLogSpoolCoordinator(options: LogSpoolCoordinatorOptions): LogSpoolCoordinator {
|
|
22
|
+
const { serviceValue, serviceId } = options;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
isEnabled() {
|
|
26
|
+
return logSpoolEnabledFromEnv();
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
getServiceId() {
|
|
30
|
+
return serviceId;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
registerProcess(process?: string) {
|
|
34
|
+
return ensureLogSpoolFromEnv({
|
|
35
|
+
serviceValue,
|
|
36
|
+
serviceId,
|
|
37
|
+
processRole: process,
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
getWriterStats() {
|
|
42
|
+
if (!logSpoolEnabledFromEnv()) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return getLogSpoolStats();
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async shutdownLogSpool() {
|
|
49
|
+
if (!logSpoolEnabledFromEnv()) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await shutdownLogSpoolFromEnv();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createLogger } from "../core/create-logger";
|
|
2
|
+
import { logPersistProcessFromEnv, logPersistServiceFromEnv, logSpoolEnabledFromEnv } from "../upload/env";
|
|
3
|
+
import { registerLogPersistOffer } from "../upload/hook";
|
|
4
|
+
import { LOG_PERSIST_TOPIC } from "../upload/logger";
|
|
5
|
+
import { LOG_SPOOL_SERVICE_ID_ENV, logSpoolConfigFromEnv, logSpoolServiceIdFromEnv } from "./config";
|
|
6
|
+
import type { EnsureLogSpoolFromEnvOptions, LogSpoolProcessRole } from "./interface";
|
|
7
|
+
import { resolveLogSpoolRoot } from "./paths";
|
|
8
|
+
import {
|
|
9
|
+
assertValidLogSpoolExtraProcessRole,
|
|
10
|
+
isKnownLogSpoolProcessRole,
|
|
11
|
+
LOG_SPOOL_EXTRA_PROCESS_ROLES_ENV,
|
|
12
|
+
} from "./process-roles";
|
|
13
|
+
import { enableLogSpoolWriter, getLogSpoolWriterStats, shutdownLogSpoolWriter } from "./writer";
|
|
14
|
+
|
|
15
|
+
export { logSpoolEnabledFromEnv };
|
|
16
|
+
|
|
17
|
+
export function resolveLogSpoolProcessRole(explicit?: string): LogSpoolProcessRole {
|
|
18
|
+
const raw = explicit ?? logPersistProcessFromEnv();
|
|
19
|
+
if (raw == null || raw === "") {
|
|
20
|
+
throw new Error("[LogSpool] LOG_PERSIST_PROCESS is required when LOG_PERSIST_ENABLED=true");
|
|
21
|
+
}
|
|
22
|
+
if (!isKnownLogSpoolProcessRole(raw)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`[LogSpool] invalid LOG_PERSIST_PROCESS: ${raw} (not a core role and not listed in ${LOG_SPOOL_EXTRA_PROCESS_ROLES_ENV})`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Env vars for child spawn after main claims registry slot.
|
|
32
|
+
* Optional when using {@link ManagedChildProcesses.spawn} — auto-injected from parent env.
|
|
33
|
+
*/
|
|
34
|
+
export function buildLogSpoolSpawnEnv(input: {
|
|
35
|
+
serviceId: string;
|
|
36
|
+
processRole: LogSpoolProcessRole;
|
|
37
|
+
}): Record<string, string> {
|
|
38
|
+
return {
|
|
39
|
+
[LOG_SPOOL_SERVICE_ID_ENV]: input.serviceId,
|
|
40
|
+
LOG_PERSIST_PROCESS: input.processRole,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* When persistence is enabled and parent has `LOG_SPOOL_SERVICE_ID`, map spawn `label`
|
|
46
|
+
* to spool env when `label` is a known core or extra process role.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveLogSpoolSpawnEnvForLabel(
|
|
49
|
+
label: string,
|
|
50
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
51
|
+
): Record<string, string> {
|
|
52
|
+
if (!logSpoolEnabledFromEnv(env)) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
const serviceId = env[LOG_SPOOL_SERVICE_ID_ENV]?.trim();
|
|
56
|
+
if (serviceId == null || serviceId === "") {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
if (!isKnownLogSpoolProcessRole(label, env)) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
return buildLogSpoolSpawnEnv({ serviceId, processRole: label });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function ensureLogSpoolFromEnv(options: EnsureLogSpoolFromEnvOptions = {}): Promise<boolean> {
|
|
66
|
+
if (!logSpoolEnabledFromEnv()) {
|
|
67
|
+
registerLogPersistOffer(null);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const serviceId = options.serviceId ?? logSpoolServiceIdFromEnv();
|
|
72
|
+
const serviceValue = options.serviceValue ?? logPersistServiceFromEnv();
|
|
73
|
+
const processRole = resolveLogSpoolProcessRole(options.processRole);
|
|
74
|
+
|
|
75
|
+
if (serviceId == null || serviceId === "") {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"[LogSpool] LOG_SPOOL_SERVICE_ID is required when LOG_PERSIST_ENABLED=true (set after registry claim or pass serviceId)",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (serviceValue == null || serviceValue === "") {
|
|
81
|
+
throw new Error("[LogSpool] LOG_PERSIST_SERVICE is required when LOG_PERSIST_ENABLED=true");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const spoolRoot = options.spoolRoot ?? resolveLogSpoolRoot();
|
|
85
|
+
|
|
86
|
+
await enableLogSpoolWriter({
|
|
87
|
+
serviceId,
|
|
88
|
+
processRole,
|
|
89
|
+
serviceValue,
|
|
90
|
+
spoolRoot,
|
|
91
|
+
config: options.config ?? logSpoolConfigFromEnv(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const lifecycleLogger = createLogger({ module: "log-spool" });
|
|
95
|
+
lifecycleLogger.info("log spool enabled from env", {
|
|
96
|
+
topic: LOG_PERSIST_TOPIC,
|
|
97
|
+
data: { serviceId, processRole, serviceValue, spoolRoot },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function shutdownLogSpoolFromEnv(): Promise<void> {
|
|
104
|
+
await shutdownLogSpoolWriter();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLogSpoolStats(): ReturnType<typeof getLogSpoolWriterStats> {
|
|
108
|
+
return getLogSpoolWriterStats();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateLogSpoolPreloadEnv(env: NodeJS.ProcessEnv = process.env): void {
|
|
112
|
+
if (!logSpoolEnabledFromEnv(env)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const missing: string[] = [];
|
|
117
|
+
if (env.LOG_PERSIST_SERVICE?.trim() == null || env.LOG_PERSIST_SERVICE.trim() === "") {
|
|
118
|
+
missing.push("LOG_PERSIST_SERVICE");
|
|
119
|
+
}
|
|
120
|
+
const runtimeDir = env.LOG_PERSIST_REGISTRY_DIR?.trim() || env.RUNTIME_DATA_DIR?.trim();
|
|
121
|
+
if (runtimeDir == null || runtimeDir === "") {
|
|
122
|
+
missing.push("LOG_PERSIST_REGISTRY_DIR or RUNTIME_DATA_DIR");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
throw new Error(`[LogSpool] preload env incomplete: ${missing.join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const role of env[LOG_SPOOL_EXTRA_PROCESS_ROLES_ENV]?.split(",") ?? []) {
|
|
130
|
+
const trimmed = role.trim();
|
|
131
|
+
if (trimmed === "") {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
assertValidLogSpoolExtraProcessRole(trimmed);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { LogEntry } from "../core/log-manager";
|
|
2
|
+
import { computeLogBatchIdempotencyKey } from "../upload/batch-id";
|
|
3
|
+
import type { LogSpoolProcessRole } from "./interface";
|
|
4
|
+
|
|
5
|
+
/** `role.timestamp.hash.tmp|json` — role is lowercase alphanumeric + hyphen. */
|
|
6
|
+
const BATCH_FILE_PATTERN = /^([a-z][a-z0-9-]*)\.(\d{14})\.([a-f0-9]{8})\.(tmp|json)$/;
|
|
7
|
+
|
|
8
|
+
export function formatLogSpoolBatchTimestamp(ms: number): string {
|
|
9
|
+
const d = new Date(ms);
|
|
10
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
11
|
+
return (
|
|
12
|
+
`${d.getFullYear()}` +
|
|
13
|
+
`${pad(d.getMonth() + 1)}` +
|
|
14
|
+
`${pad(d.getDate())}` +
|
|
15
|
+
`${pad(d.getHours())}` +
|
|
16
|
+
`${pad(d.getMinutes())}` +
|
|
17
|
+
`${pad(d.getSeconds())}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function computeLogSpoolShortHash(input: {
|
|
22
|
+
serviceValue: string;
|
|
23
|
+
serviceId: string;
|
|
24
|
+
processRole: LogSpoolProcessRole;
|
|
25
|
+
entries: LogEntry[];
|
|
26
|
+
}): string {
|
|
27
|
+
const processKey = `${input.serviceId}:${input.processRole}`;
|
|
28
|
+
const idempotencyKey = computeLogBatchIdempotencyKey({
|
|
29
|
+
service: input.serviceValue,
|
|
30
|
+
process: processKey,
|
|
31
|
+
lane: "slow",
|
|
32
|
+
entries: input.entries,
|
|
33
|
+
});
|
|
34
|
+
return idempotencyKey.slice(0, 8);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildLogSpoolBatchBaseName(input: {
|
|
38
|
+
processRole: LogSpoolProcessRole;
|
|
39
|
+
firstEntryTimestamp: number;
|
|
40
|
+
shortHash: string;
|
|
41
|
+
}): string {
|
|
42
|
+
const ts = formatLogSpoolBatchTimestamp(input.firstEntryTimestamp);
|
|
43
|
+
return `${input.processRole}.${ts}.${input.shortHash}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseLogSpoolBatchFilename(filename: string): {
|
|
47
|
+
processRole: LogSpoolProcessRole;
|
|
48
|
+
timestampMs: number;
|
|
49
|
+
shortHash: string;
|
|
50
|
+
extension: "tmp" | "json";
|
|
51
|
+
} | null {
|
|
52
|
+
const match = BATCH_FILE_PATTERN.exec(filename);
|
|
53
|
+
if (match == null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const processRole = match[1] as LogSpoolProcessRole;
|
|
58
|
+
const tsRaw = match[2];
|
|
59
|
+
const shortHash = match[3];
|
|
60
|
+
const extension = match[4] as "tmp" | "json";
|
|
61
|
+
|
|
62
|
+
const year = Number(tsRaw.slice(0, 4));
|
|
63
|
+
const month = Number(tsRaw.slice(4, 6));
|
|
64
|
+
const day = Number(tsRaw.slice(6, 8));
|
|
65
|
+
const hour = Number(tsRaw.slice(8, 10));
|
|
66
|
+
const minute = Number(tsRaw.slice(10, 12));
|
|
67
|
+
const second = Number(tsRaw.slice(12, 14));
|
|
68
|
+
const timestampMs = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
69
|
+
|
|
70
|
+
if (!Number.isFinite(timestampMs)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { processRole, timestampMs, shortHash, extension };
|
|
75
|
+
}
|