target-supabase-sdk 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/browser.d.ts +1 -1
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/chunks/enable-DqnfLPsQ.js +874 -0
  5. package/dist/chunks/enable-DqnfLPsQ.js.map +1 -0
  6. package/dist/chunks/list.api-BzbTJBsM.js.map +1 -1
  7. package/dist/chunks/{task-queue-H4mpSW8R.js → task-queue-6X3aglJh.js} +108 -3
  8. package/dist/chunks/task-queue-6X3aglJh.js.map +1 -0
  9. package/dist/node/preload/run-service-preload.d.ts.map +1 -1
  10. package/dist/node/preload/service-preload.interface.d.ts +3 -1
  11. package/dist/node/preload/service-preload.interface.d.ts.map +1 -1
  12. package/dist/node/process/managed-child-processes.d.ts.map +1 -1
  13. package/dist/node/service-guard/service-guard-node.d.ts.map +1 -1
  14. package/dist/node/service-host/index.d.ts +1 -0
  15. package/dist/node/service-host/index.d.ts.map +1 -1
  16. package/dist/node/service-host/service-host.d.ts +5 -1
  17. package/dist/node/service-host/service-host.d.ts.map +1 -1
  18. package/dist/node.d.ts +10 -5
  19. package/dist/node.d.ts.map +1 -1
  20. package/dist/node.js +492 -47
  21. package/dist/node.js.map +1 -1
  22. package/dist/preload.js +9 -6
  23. package/dist/preload.js.map +1 -1
  24. package/dist/service/index.d.ts +2 -2
  25. package/dist/service/index.d.ts.map +1 -1
  26. package/dist/service/registry.service.d.ts +19 -0
  27. package/dist/service/registry.service.d.ts.map +1 -1
  28. package/dist/shared/log/core/create-logger.d.ts.map +1 -0
  29. package/dist/shared/log/{log-batch.d.ts → core/log-batch.d.ts} +4 -4
  30. package/dist/shared/log/core/log-batch.d.ts.map +1 -0
  31. package/dist/shared/log/core/log-manager.d.ts.map +1 -0
  32. package/dist/shared/log/core/log-min-level.d.ts.map +1 -0
  33. package/dist/shared/log/core/log-scope.d.ts.map +1 -0
  34. package/dist/shared/log/index.d.ts +13 -13
  35. package/dist/shared/log/index.d.ts.map +1 -1
  36. package/dist/shared/log/spool/collector.d.ts +21 -0
  37. package/dist/shared/log/spool/collector.d.ts.map +1 -0
  38. package/dist/shared/log/spool/config.d.ts +7 -0
  39. package/dist/shared/log/spool/config.d.ts.map +1 -0
  40. package/dist/shared/log/spool/coordinator.d.ts +18 -0
  41. package/dist/shared/log/spool/coordinator.d.ts.map +1 -0
  42. package/dist/shared/log/spool/enable.d.ts +23 -0
  43. package/dist/shared/log/spool/enable.d.ts.map +1 -0
  44. package/dist/shared/log/spool/file.d.ts +21 -0
  45. package/dist/shared/log/spool/file.d.ts.map +1 -0
  46. package/dist/shared/log/spool/interface.d.ts +55 -0
  47. package/dist/shared/log/spool/interface.d.ts.map +1 -0
  48. package/dist/shared/log/spool/paths.d.ts +6 -0
  49. package/dist/shared/log/spool/paths.d.ts.map +1 -0
  50. package/dist/shared/log/spool/process-roles.d.ts +17 -0
  51. package/dist/shared/log/spool/process-roles.d.ts.map +1 -0
  52. package/dist/shared/log/spool/register-collect-log-runner.d.ts +15 -0
  53. package/dist/shared/log/spool/register-collect-log-runner.d.ts.map +1 -0
  54. package/dist/shared/log/spool/service-lifecycle.d.ts +13 -0
  55. package/dist/shared/log/spool/service-lifecycle.d.ts.map +1 -0
  56. package/dist/shared/log/spool/writer.d.ts +32 -0
  57. package/dist/shared/log/spool/writer.d.ts.map +1 -0
  58. package/dist/shared/log/{log-persist-batch-id.d.ts → upload/batch-id.d.ts} +3 -3
  59. package/dist/shared/log/upload/batch-id.d.ts.map +1 -0
  60. package/dist/shared/log/upload/env.d.ts +7 -0
  61. package/dist/shared/log/upload/env.d.ts.map +1 -0
  62. package/dist/shared/log/{log-persist-flush.d.ts → upload/flush.d.ts} +3 -3
  63. package/dist/shared/log/upload/flush.d.ts.map +1 -0
  64. package/dist/shared/log/{log-persist-hook.d.ts → upload/hook.d.ts} +2 -2
  65. package/dist/shared/log/upload/hook.d.ts.map +1 -0
  66. package/dist/shared/log/upload/interface.d.ts +33 -0
  67. package/dist/shared/log/upload/interface.d.ts.map +1 -0
  68. package/dist/shared/log/upload/logger.d.ts +22 -0
  69. package/dist/shared/log/upload/logger.d.ts.map +1 -0
  70. package/package.json +1 -1
  71. package/src/browser.ts +1 -0
  72. package/src/node/preload/run-service-preload.ts +11 -6
  73. package/src/node/preload/service-preload.interface.ts +3 -1
  74. package/src/node/process/create-managed-child-processes.ts +1 -1
  75. package/src/node/process/managed-child-processes.ts +8 -1
  76. package/src/node/service-guard/service-guard-node.ts +5 -0
  77. package/src/node/service-host/index.ts +4 -0
  78. package/src/node/service-host/service-host.ts +22 -3
  79. package/src/node.ts +26 -19
  80. package/src/service/index.ts +3 -0
  81. package/src/service/registry.service.ts +142 -0
  82. package/src/shared/log/README.md +206 -0
  83. package/src/shared/log/{log-batch.ts → core/log-batch.ts} +4 -4
  84. package/src/shared/log/{log-manager.ts → core/log-manager.ts} +3 -3
  85. package/src/shared/log/{log-min-level.ts → core/log-min-level.ts} +1 -1
  86. package/src/shared/log/{log-scope.ts → core/log-scope.ts} +1 -1
  87. package/src/shared/log/index.ts +13 -18
  88. package/src/shared/log/spool/collector.ts +330 -0
  89. package/src/shared/log/spool/config.ts +56 -0
  90. package/src/shared/log/spool/coordinator.ts +55 -0
  91. package/src/shared/log/spool/enable.ts +136 -0
  92. package/src/shared/log/spool/file.ts +75 -0
  93. package/src/shared/log/spool/interface.ts +62 -0
  94. package/src/shared/log/spool/paths.ts +30 -0
  95. package/src/shared/log/spool/process-roles.ts +75 -0
  96. package/src/shared/log/spool/register-collect-log-runner.ts +63 -0
  97. package/src/shared/log/spool/service-lifecycle.ts +35 -0
  98. package/src/shared/log/spool/writer.ts +273 -0
  99. package/src/shared/log/{log-persist-batch-id.ts → upload/batch-id.ts} +2 -2
  100. package/src/shared/log/upload/env.ts +26 -0
  101. package/src/shared/log/{log-persist-flush.ts → upload/flush.ts} +8 -7
  102. package/src/shared/log/{log-persist-hook.ts → upload/hook.ts} +1 -1
  103. package/src/shared/log/upload/interface.ts +38 -0
  104. package/src/shared/log/upload/logger.ts +37 -0
  105. package/dist/chunks/enable-log-persist-Dl55_mv2.js +0 -1674
  106. package/dist/chunks/enable-log-persist-Dl55_mv2.js.map +0 -1
  107. package/dist/chunks/task-queue-H4mpSW8R.js.map +0 -1
  108. package/dist/shared/log/create-logger.d.ts.map +0 -1
  109. package/dist/shared/log/enable-log-persist.d.ts +0 -28
  110. package/dist/shared/log/enable-log-persist.d.ts.map +0 -1
  111. package/dist/shared/log/log-batch.d.ts.map +0 -1
  112. package/dist/shared/log/log-manager.d.ts.map +0 -1
  113. package/dist/shared/log/log-min-level.d.ts.map +0 -1
  114. package/dist/shared/log/log-persist-batch-id.d.ts.map +0 -1
  115. package/dist/shared/log/log-persist-coordinator.d.ts +0 -37
  116. package/dist/shared/log/log-persist-coordinator.d.ts.map +0 -1
  117. package/dist/shared/log/log-persist-errors.d.ts +0 -4
  118. package/dist/shared/log/log-persist-errors.d.ts.map +0 -1
  119. package/dist/shared/log/log-persist-flush.d.ts.map +0 -1
  120. package/dist/shared/log/log-persist-hook.d.ts.map +0 -1
  121. package/dist/shared/log/log-persist-logger.d.ts +0 -16
  122. package/dist/shared/log/log-persist-logger.d.ts.map +0 -1
  123. package/dist/shared/log/log-persist-registry.d.ts +0 -18
  124. package/dist/shared/log/log-persist-registry.d.ts.map +0 -1
  125. package/dist/shared/log/log-persist.config.d.ts +0 -9
  126. package/dist/shared/log/log-persist.config.d.ts.map +0 -1
  127. package/dist/shared/log/log-persist.d.ts +0 -62
  128. package/dist/shared/log/log-persist.d.ts.map +0 -1
  129. package/dist/shared/log/log-persist.interface.d.ts +0 -132
  130. package/dist/shared/log/log-persist.interface.d.ts.map +0 -1
  131. package/dist/shared/log/log-scope.d.ts.map +0 -1
  132. package/src/shared/log/enable-log-persist.ts +0 -225
  133. package/src/shared/log/log-persist-coordinator.ts +0 -107
  134. package/src/shared/log/log-persist-errors.ts +0 -16
  135. package/src/shared/log/log-persist-logger.ts +0 -61
  136. package/src/shared/log/log-persist-registry.ts +0 -252
  137. package/src/shared/log/log-persist.config.ts +0 -115
  138. package/src/shared/log/log-persist.interface.ts +0 -150
  139. package/src/shared/log/log-persist.ts +0 -822
  140. /package/dist/shared/log/{create-logger.d.ts → core/create-logger.d.ts} +0 -0
  141. /package/dist/shared/log/{log-manager.d.ts → core/log-manager.d.ts} +0 -0
  142. /package/dist/shared/log/{log-min-level.d.ts → core/log-min-level.d.ts} +0 -0
  143. /package/dist/shared/log/{log-scope.d.ts → core/log-scope.d.ts} +0 -0
  144. /package/src/shared/log/{create-logger.ts → core/create-logger.ts} +0 -0
@@ -0,0 +1,206 @@
1
+ # Log module (`src/shared/log`)
2
+
3
+ Structured logging, optional file-backed persistence, and Supabase `LogBatch` upload for L3 services.
4
+
5
+ ## Layout
6
+
7
+ ```text
8
+ src/shared/log/
9
+ README.md
10
+ index.ts # browser-safe public API (re-exports core + upload types)
11
+
12
+ core/ # in-process logging (browser + Node)
13
+ log-manager.ts # LogManager singleton, LogEntry, levels
14
+ create-logger.ts # createLogger({ module, scope, … })
15
+ log-scope.ts # traceId / labels / patchScope
16
+ log-min-level.ts # LOG_MIN_LEVEL env
17
+ log-batch.ts # decode persisted LogBatch Lists (read side)
18
+
19
+ upload/ # shared Supabase upload pipeline (Node)
20
+ env.ts # LOG_PERSIST_ENABLED, SERVICE, PROCESS, runtime dir
21
+ interface.ts # LogBatch meta, loader keys, lane type
22
+ batch-id.ts # computeLogBatchIdempotencyKey
23
+ flush.ts # buildLogListDraft, postLogBatch
24
+ hook.ts # LogManager → spool writer offer bridge
25
+ logger.ts # internal pipeline loggers (no feedback loop)
26
+
27
+ spool/ # file-backed persistence (Node only)
28
+ enable.ts # ensureLogSpoolFromEnv, spawn env helpers
29
+ service-lifecycle.ts # isLogSpoolEnabled, enableLogSpoolFromEnvInChild, shutdownLogSpool
30
+ writer.ts # per-process memory buffer → .tmp files
31
+ collector.ts # guard: upload .tmp, rename .json, periodic GC
32
+ paths.ts # {RUNTIME_DATA_DIR}/log-spool/{serviceId}/…
33
+ file.ts # batch filename parse/build
34
+ process-roles.ts # main|guard|scheduler|worker + extras
35
+ config.ts # writer / GC / collect interval env
36
+ coordinator.ts # createLogSpoolCoordinator facade
37
+ register-collect-log-runner.ts
38
+ ```
39
+
40
+ **Package exports**
41
+
42
+ | Entry | Scope |
43
+ |-------|--------|
44
+ | `target-supabase-sdk` / `index.ts` | `core/` + decode types from `upload/interface` |
45
+ | `target-supabase-sdk/node` | spool + upload symbols used by L3 services |
46
+
47
+ Node-only code must not be imported from the browser bundle. Spool files use `node:fs`.
48
+
49
+ ---
50
+
51
+ ## Data flow
52
+
53
+ ```text
54
+ App code
55
+ → createLogger / logManager
56
+ → offerToLogPersist (when spool enabled)
57
+ → LogSpoolWriter (memory buffer)
58
+ → {spoolRoot}/{serviceId}/{role}/*.tmp
59
+
60
+ Guard collect-log runner (1 min tick)
61
+ → read *.tmp → postLogBatch → rename *.json
62
+ → GC old tmp/json (once per day; 7 d retention)
63
+
64
+ log-service (downstream)
65
+ → decodeLogBatch / LogTrace merge by traceId
66
+ ```
67
+
68
+ **One-line rule:** producers flush `LogEntry[]` to `.tmp`; only the guard uploads `LogBatch` and renames to `.json`.
69
+
70
+ ---
71
+
72
+ ## Environment
73
+
74
+ | Key | Role |
75
+ |-----|------|
76
+ | `LOG_PERSIST_ENABLED` | Master switch (`true` → file spool) |
77
+ | `LOG_PERSIST_SERVICE` | Logical service value in List meta (e.g. `watch-service`) |
78
+ | `LOG_PERSIST_PROCESS` | This process role: `main` \| `guard` \| `scheduler` \| `worker` or extra |
79
+ | `LOG_SPOOL_SERVICE_ID` | Instance id — set by main after registry claim |
80
+ | `LOG_SPOOL_EXTRA_PROCESS_ROLES` | Comma-separated extras (e.g. `chrome-sidecar`) |
81
+ | `RUNTIME_DATA_DIR` | Runtime root; spool lives at `{dir}/log-spool/` |
82
+ | `LOG_SPOOL_DIR` | Optional override for spool root parent |
83
+
84
+ ### Writer tuning
85
+
86
+ | Key | Default |
87
+ |-----|---------|
88
+ | `LOG_SPOOL_MAX_ENTRIES` | 2000 |
89
+ | `LOG_SPOOL_MAX_BYTES` | 2 MB |
90
+ | `LOG_SPOOL_FLUSH_DEBOUNCE_MS` | 3 s (after count/byte threshold) |
91
+ | `LOG_SPOOL_MAX_AGE_MS` | 60 s (flush buffered logs even when below threshold) |
92
+ | `LOG_PERSIST_POST_TIMEOUT_MS` | 30 s |
93
+
94
+ ### Collector / GC
95
+
96
+ | Key | Default |
97
+ |-----|---------|
98
+ | `LOG_SPOOL_COLLECT_INTERVAL_MS` | 60 s |
99
+ | `LOG_SPOOL_GC_INTERVAL_MS` | 24 h (how often guard runs tmp/json GC) |
100
+ | `LOG_SPOOL_SYNCED_RETENTION_MS` | 7 d (delete `.json` when mtime older than this) |
101
+ | `LOG_SPOOL_TMP_RETENTION_MS` | 7 d (delete stale `.tmp` by filename timestamp) |
102
+
103
+ ---
104
+
105
+ ## On-disk layout
106
+
107
+ ```text
108
+ {RUNTIME_DATA_DIR}/log-spool/{serviceId}/
109
+ main/main.{YYYYMMDDHHmmss}.{hash8}.tmp → .json after upload
110
+ guard/…
111
+ scheduler/…
112
+ worker/…
113
+ {extra}/…
114
+ ```
115
+
116
+ Upload state: `.tmp` = pending, `.json` = uploaded (no central index file).
117
+
118
+ Batch file JSON: `{ entries: LogEntry[], meta?: { serviceId, processRole, serviceValue } }`.
119
+
120
+ Uploaded List meta:
121
+
122
+ - `meta.process` = `{serviceId}:{processRole}` (e.g. `abc123:worker`)
123
+ - `meta.service` = logical value (`watch-service`)
124
+ - `meta.lane` = `"slow"` (legacy transport field; required for decode today)
125
+
126
+ ---
127
+
128
+ ## L3 integration
129
+
130
+ ### Preload (`runServicePreload`)
131
+
132
+ Sync phases set `LOG_PERSIST_SERVICE`, `RUNTIME_DATA_DIR`, validate env, merge `logSpoolExtraProcessRoles`.
133
+ **Do not** call `ensureLogSpoolFromEnv` in preload — it is async and needs `LOG_SPOOL_SERVICE_ID`.
134
+
135
+ ### Main (`createServiceHost`)
136
+
137
+ After registry claim, main sets `LOG_SPOOL_SERVICE_ID` and calls `ensureLogSpoolFromEnv({ processRole: "main", … })`.
138
+
139
+ ### Child processes
140
+
141
+ Guard / scheduler / worker `main()`:
142
+
143
+ ```typescript
144
+ import { enableLogSpoolFromEnvInChild } from "target-supabase-sdk/node";
145
+
146
+ await enableLogSpoolFromEnvInChild();
147
+ ```
148
+
149
+ ### Shutdown
150
+
151
+ ```typescript
152
+ import { shutdownLogSpool } from "target-supabase-sdk/node";
153
+
154
+ await shutdownLogSpool();
155
+ ```
156
+
157
+ Flushes remaining memory buffer to `.tmp`.
158
+
159
+ ### Extra process roles (e.g. download-service `chrome-sidecar`)
160
+
161
+ ```javascript
162
+ // scripts/preload.mjs
163
+ runServicePreload({
164
+ // …
165
+ logSpoolExtraProcessRoles: ["chrome-sidecar"],
166
+ });
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Public Node API (curated)
172
+
173
+ | Symbol | Role |
174
+ |--------|------|
175
+ | `ensureLogSpoolFromEnv` | Enable writer in current process |
176
+ | `enableLogSpoolFromEnvInChild` | Child entry: no-op when disabled, else `ensureLogSpoolFromEnv` |
177
+ | `isLogSpoolEnabled` | Read master switch (`LOG_PERSIST_ENABLED`) |
178
+ | `getMainLogSpoolWriterStats` | Writer stats or `null` when disabled (main observability) |
179
+ | `shutdownLogSpool` | No-op when disabled, else flush + disable |
180
+ | `shutdownLogSpoolFromEnv` | Flush buffer + disable (unconditional) |
181
+ | `getLogSpoolStats` | Writer buffer stats |
182
+ | `buildLogSpoolSpawnEnv` | Manual child env (usually unnecessary) |
183
+ | `validateLogSpoolPreloadEnv` | Preload validation |
184
+ | `logSpoolEnabledFromEnv` | Read master switch |
185
+ | `createLogSpoolCoordinator` | Service-bound lifecycle facade |
186
+ | `registerCollectLogRunner` | Guard TriggerNode runner (via `ServiceGuardNode`) |
187
+ | `runCollectLogTick` | Manual / tests |
188
+ | `resolveLogSpoolRoot` | Spool directory path |
189
+
190
+ Internal helpers (`resolveLogSpoolSpawnEnvForLabel`, `resolveAllLogSpoolProcessRoles`, etc.) are not exported from `node` — use preload `logSpoolExtraProcessRoles` or env instead.
191
+
192
+ ---
193
+
194
+ ## Concurrency (Windows)
195
+
196
+ - One writer per `.tmp` file — producers only create new files.
197
+ - Guard renames after successful upload; does not RMW batch contents.
198
+ - GC scans directories; no shared JSON index file.
199
+
200
+ ---
201
+
202
+ ## Related skills
203
+
204
+ - [log-spool](../../../.cursor/skills/log-spool/SKILL.md) — agent quick reference
205
+ - [service-preload](../../../.cursor/skills/service-preload/SKILL.md) — preload protocol
206
+ - [l3-service-host](../../../.cursor/skills/l3-service-host/SKILL.md) — `createServiceHost` integration
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Read side of the log-persist pipeline: decode persisted `LogBatch` Lists into
3
3
  * structured batches and flat entries. Pure and defensive — malformed rows are
4
- * skipped rather than thrown. Mirrors the write side in `log-persist-flush.ts`.
4
+ * skipped rather than thrown. Mirrors the write side in `upload/flush.ts`.
5
5
  */
6
6
 
7
- import type { List } from "../../list/list.interface";
8
- import type { LogEntry } from "./log-manager";
9
- import { LOG_PERSIST_LOADER_KEY, type LogBatchMeta, type LogPersistLane } from "./log-persist.interface";
7
+ import type { List } from "../../../list/list.interface";
8
+ import type { LogEntry } from "../core/log-manager";
9
+ import { LOG_PERSIST_LOADER_KEY, type LogBatchMeta, type LogPersistLane } from "../upload/interface";
10
10
 
11
11
  const LANES: ReadonlySet<string> = new Set<LogPersistLane>(["fast", "medium", "slow"]);
12
12
 
@@ -1,6 +1,6 @@
1
- import { isDevEnvironment } from "../../core.utils";
2
- import { generateUniqueId } from "../utils/id.utils";
3
- import { offerToLogPersist } from "./log-persist-hook";
1
+ import { isDevEnvironment } from "../../../core.utils";
2
+ import { generateUniqueId } from "../../utils/id.utils";
3
+ import { offerToLogPersist } from "../upload/hook";
4
4
  import { formatScopeLabels, type LoggerResetScopePatch, type LogScope, patchScope, resolveLogData } from "./log-scope";
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { isDevEnvironment } from "../../core.utils";
1
+ import { isDevEnvironment } from "../../../core.utils";
2
2
  import { LogLevel } from "./log-manager";
3
3
 
4
4
  /** Env key for global {@link LogManager} min level (console + log-persist when enabled). */
@@ -1,4 +1,4 @@
1
- import { generateUniqueId } from "../utils/id.utils";
1
+ import { generateUniqueId } from "../../utils/id.utils";
2
2
 
3
3
  /** Logger-bound scope: module + trace chain + optional labels. */
4
4
  export interface LogScope {
@@ -1,16 +1,14 @@
1
1
  /**
2
- * Log domain public API — curated re-exports only.
2
+ * Log domain public API — curated re-exports only (browser-safe).
3
3
  */
4
4
 
5
5
  export type {
6
6
  CreateLoggerFromModuleInput,
7
7
  CreateLoggerFromScopeInput,
8
8
  CreateLoggerInput,
9
- } from "./create-logger";
10
- // create-logger
11
- export { createLogger } from "./create-logger";
12
- export type { DecodedLogBatch, FlatLogEntry } from "./log-batch";
13
- // log-batch (persisted LogBatch decode — read side of log-persist)
9
+ } from "./core/create-logger";
10
+ export { createLogger } from "./core/create-logger";
11
+ export type { DecodedLogBatch, FlatLogEntry } from "./core/log-batch";
14
12
  export {
15
13
  decodeLogBatch,
16
14
  decodeLogBatchList,
@@ -19,19 +17,15 @@ export {
19
17
  isLogBatchList,
20
18
  isLogEntry,
21
19
  parseLogBatchMeta,
22
- } from "./log-batch";
23
- export type { LogEntry, LoggerWithScope, LogOptions, LogRestParams, WithScopeOptions } from "./log-manager";
24
- // log-manager
25
- export { LogLevel, LogManager, logManager } from "./log-manager";
26
- export type { ResolveLogMinLevelOptions } from "./log-min-level";
27
- // log-min-level
20
+ } from "./core/log-batch";
21
+ export type { LogEntry, LoggerWithScope, LogOptions, LogRestParams, WithScopeOptions } from "./core/log-manager";
22
+ export { LogLevel, LogManager, logManager } from "./core/log-manager";
23
+ export type { ResolveLogMinLevelOptions } from "./core/log-min-level";
28
24
  export {
29
25
  LOG_MIN_LEVEL_ENV_KEY,
30
26
  logMinLevelFromEnv,
31
27
  resolveLogMinLevel,
32
- } from "./log-min-level";
33
- export type { LogBatchMeta, LogPersistLane } from "./log-persist.interface";
34
- export { LOG_BATCH_LOADER_KEY, LOG_PERSIST_LOADER_KEY } from "./log-persist.interface";
28
+ } from "./core/log-min-level";
35
29
  export type {
36
30
  CreateScopeInput,
37
31
  LoggerResetScopePatch,
@@ -39,8 +33,7 @@ export type {
39
33
  LogScopeInput,
40
34
  LogScopePatch,
41
35
  PatchScopeInput,
42
- } from "./log-scope";
43
- // log-scope
36
+ } from "./core/log-scope";
44
37
  export {
45
38
  createScope,
46
39
  formatScopeLabels,
@@ -48,4 +41,6 @@ export {
48
41
  patchScope,
49
42
  resolveLogData,
50
43
  withModule,
51
- } from "./log-scope";
44
+ } from "./core/log-scope";
45
+ export type { LogBatchMeta, LogPersistLane } from "./upload/interface";
46
+ export { LOG_BATCH_LOADER_KEY, LOG_PERSIST_LOADER_KEY } from "./upload/interface";
@@ -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
+ }