@kici-dev/shared 0.0.0 → 0.1.0

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 (105) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1 -6
  3. package/dist/chunk-gOLHoazu.js +4 -0
  4. package/dist/cold-store/bucket.d.ts +26 -0
  5. package/dist/cold-store/bucket.js +40 -0
  6. package/dist/cold-store/bucket.test.d.ts +2 -0
  7. package/dist/cold-store/chunk-encoder.d.ts +63 -0
  8. package/dist/cold-store/chunk-encoder.js +94 -0
  9. package/dist/cold-store/chunk-encoder.test.d.ts +2 -0
  10. package/dist/cold-store/chunk-id.d.ts +10 -0
  11. package/dist/cold-store/chunk-id.js +29 -0
  12. package/dist/cold-store/chunk-id.test.d.ts +2 -0
  13. package/dist/cold-store/cold-store.d.ts +345 -0
  14. package/dist/cold-store/cold-store.js +1078 -0
  15. package/dist/cold-store/cold-store.test.d.ts +2 -0
  16. package/dist/cold-store/config.d.ts +62 -0
  17. package/dist/cold-store/config.js +28 -0
  18. package/dist/cold-store/index.d.ts +20 -0
  19. package/dist/cold-store/index.js +11 -0
  20. package/dist/cold-store/key.d.ts +89 -0
  21. package/dist/cold-store/key.js +88 -0
  22. package/dist/cold-store/key.test.d.ts +2 -0
  23. package/dist/cold-store/lru.d.ts +33 -0
  24. package/dist/cold-store/lru.js +59 -0
  25. package/dist/cold-store/lru.test.d.ts +2 -0
  26. package/dist/cold-store/manifest.d.ts +11 -0
  27. package/dist/cold-store/manifest.js +57 -0
  28. package/dist/cold-store/manifest.test.d.ts +2 -0
  29. package/dist/cold-store/metrics.d.ts +103 -0
  30. package/dist/cold-store/metrics.js +179 -0
  31. package/dist/cold-store/table-adapter.d.ts +229 -0
  32. package/dist/cold-store/table-adapter.js +2 -0
  33. package/dist/cold-store/types.d.ts +111 -0
  34. package/dist/cold-store/types.js +2 -0
  35. package/dist/crypto.d.ts +33 -0
  36. package/dist/crypto.js +67 -0
  37. package/dist/db-admin.d.ts +1422 -0
  38. package/dist/db-admin.js +2525 -0
  39. package/dist/db-admin.test.d.ts +2 -0
  40. package/dist/db.d.ts +14 -0
  41. package/dist/db.js +23 -0
  42. package/dist/env/allowlist.d.ts +79 -0
  43. package/dist/env/allowlist.js +86 -0
  44. package/dist/env/define-env.d.ts +172 -0
  45. package/dist/env/define-env.js +295 -0
  46. package/dist/env/define-env.test.d.ts +2 -0
  47. package/dist/env/env-rule-allowlist.test.d.ts +2 -0
  48. package/dist/env/index.d.ts +11 -0
  49. package/dist/env/index.js +4 -0
  50. package/dist/env/logger-env.d.ts +30 -0
  51. package/dist/env/logger-env.js +95 -0
  52. package/dist/error.d.ts +16 -0
  53. package/dist/error.js +58 -0
  54. package/dist/error.test.d.ts +2 -0
  55. package/dist/format-bytes.d.ts +5 -0
  56. package/dist/format-bytes.js +15 -0
  57. package/dist/format-bytes.test.d.ts +2 -0
  58. package/dist/format-duration.d.ts +11 -0
  59. package/dist/format-duration.js +32 -0
  60. package/dist/format-duration.test.d.ts +2 -0
  61. package/dist/graceful-shutdown.d.ts +58 -0
  62. package/dist/graceful-shutdown.js +71 -0
  63. package/dist/graceful-shutdown.test.d.ts +2 -0
  64. package/dist/idempotency-files.d.ts +113 -0
  65. package/dist/idempotency-files.js +190 -0
  66. package/dist/idempotency-files.test.d.ts +2 -0
  67. package/dist/idempotency.d.ts +46 -0
  68. package/dist/idempotency.js +45 -0
  69. package/dist/idempotency.test.d.ts +2 -0
  70. package/dist/index.d.ts +19 -0
  71. package/dist/index.js +31 -0
  72. package/dist/logger.d.ts +57 -0
  73. package/dist/logger.js +175 -0
  74. package/dist/logger.test.d.ts +2 -0
  75. package/dist/reconnect-delay.d.ts +11 -0
  76. package/dist/reconnect-delay.js +22 -0
  77. package/dist/request-context.d.ts +42 -0
  78. package/dist/request-context.js +37 -0
  79. package/dist/ring-buffer.d.ts +35 -0
  80. package/dist/ring-buffer.js +62 -0
  81. package/dist/ring-buffer.test.d.ts +2 -0
  82. package/dist/routes/health.d.ts +26 -0
  83. package/dist/routes/health.js +45 -0
  84. package/dist/routes/health.test.d.ts +2 -0
  85. package/dist/routes/metrics.d.ts +20 -0
  86. package/dist/routes/metrics.js +38 -0
  87. package/dist/s3-client.d.ts +42 -0
  88. package/dist/s3-client.js +25 -0
  89. package/dist/telemetry/index.d.ts +3 -0
  90. package/dist/telemetry/index.js +4 -0
  91. package/dist/telemetry/init.d.ts +20 -0
  92. package/dist/telemetry/init.js +42 -0
  93. package/dist/telemetry/init.test.d.ts +2 -0
  94. package/dist/telemetry/metrics.d.ts +8 -0
  95. package/dist/telemetry/metrics.js +16 -0
  96. package/dist/tool-check.d.ts +33 -0
  97. package/dist/tool-check.js +60 -0
  98. package/dist/tool-check.test.d.ts +2 -0
  99. package/dist/ts-loader-hook.d.ts +26 -0
  100. package/dist/ts-loader-hook.js +48 -0
  101. package/dist/zx.d.ts +8 -0
  102. package/dist/zx.js +78 -0
  103. package/package.json +69 -5
  104. package/sbom.spdx.json +8775 -0
  105. package/index.js +0 -3
@@ -0,0 +1,345 @@
1
+ /**
2
+ * BaseColdStore — the DB-agnostic framework class.
3
+ *
4
+ * Multiple concrete subclasses extend this one (e.g., `PlatformColdStore`,
5
+ * `OrchestratorColdStore`). Each wires the adapter map with its
6
+ * package-specific adapters.
7
+ *
8
+ * Phase B: `runArchiveCycle()` now drives the per-adapter archive loop
9
+ * (design §4). `fetchRange` / `hasRange` / `countRange` are real — they
10
+ * resolve manifests via `ListObjectsV2` and GET overlapping chunks
11
+ * through an in-process LRU.
12
+ *
13
+ * ## Advisory-lock namespace
14
+ *
15
+ * All Postgres advisory locks acquired by the cold-store framework use
16
+ * keys derived from `hashtext('cold-store|...')`. The `cold-store|`
17
+ * prefix is reserved — do NOT reuse it in pipeline or engine code
18
+ * without coordinating to avoid collisions.
19
+ *
20
+ * ## S3 bucket versioning caveat
21
+ *
22
+ * When the cold-store bucket has versioning ON (recommended default),
23
+ * retried chunk PUTs produce new object versions at the same key.
24
+ * Reconciliation (the `reconcile` CLI) MUST use `ListObjectVersions`,
25
+ * not `ListObjects`, to detect orphaned objects correctly. The live
26
+ * read-through uses `ListObjectsV2` because it only cares about the
27
+ * current version of each manifest.
28
+ */
29
+ import { type S3Client } from '@aws-sdk/client-s3';
30
+ import { type DbKind } from './key.js';
31
+ import type { ChunkLru } from './lru.js';
32
+ import type { ColdStoreConfig } from './config.js';
33
+ import type { TableAdapter } from './table-adapter.js';
34
+ import type { ArchiveCycleSummary, ChunkManifest } from './types.js';
35
+ export interface ColdStoreFetchRangeArgs<TRow> {
36
+ db: DbKind;
37
+ table: string;
38
+ tenantId: string;
39
+ fromTs: Date;
40
+ toTs: Date;
41
+ decode?: (line: string) => TRow;
42
+ }
43
+ export interface ColdStoreReplayChunkArgs {
44
+ db: DbKind;
45
+ table: string;
46
+ tenantId: string;
47
+ partitionDate: string;
48
+ chunkId: string;
49
+ }
50
+ export interface ColdStoreReplayRowArgs {
51
+ db: DbKind;
52
+ table: string;
53
+ tenantId: string;
54
+ rowId: string | number;
55
+ }
56
+ export interface ColdStoreReplayResult {
57
+ /** Number of rows newly inserted into PG. */
58
+ inserted: number;
59
+ /** Rows whose unique key was already in PG (idempotent re-replay). */
60
+ skipped: number;
61
+ /** The chunk that was replayed (`null` when replayRow finds no match). */
62
+ chunkId: string | null;
63
+ }
64
+ /**
65
+ * Phase 2 — purge-sweep options.
66
+ *
67
+ * `tableFilter` restricts to a single table (used by
68
+ * `cold-store purge-now <table>`). `bucketFilter` further restricts to a
69
+ * single bucket subprefix. `limit` caps the number of chunks processed
70
+ * per call so a single sweep can't OOM the orchestrator on a tenant
71
+ * with millions of expired chunks. `dryRun=true` lists candidates
72
+ * without actually deleting anything (the CLI default — operators must
73
+ * pass `--apply`).
74
+ */
75
+ export interface PurgeExpiredChunksOpts {
76
+ tableFilter?: string;
77
+ bucketFilter?: string;
78
+ limit?: number;
79
+ dryRun?: boolean;
80
+ }
81
+ /**
82
+ * Phase 2 — one row per (chunk attempt, outcome). Returned from
83
+ * `purgeExpiredChunks` so callers (scheduled job, CLI) can log / report
84
+ * what happened without re-querying the chunk index.
85
+ */
86
+ export interface PurgeChunkResult {
87
+ table: string;
88
+ tenantId: string;
89
+ chunkId: string;
90
+ bucket: string;
91
+ gzipBytes: number;
92
+ rowCount: number;
93
+ outcome: 'purged' | 'dry_run' | 'skipped_locked' | 'failure';
94
+ /** Populated only when `outcome === 'failure'`. */
95
+ error?: string;
96
+ }
97
+ export interface PurgeExpiredChunksSummary {
98
+ results: PurgeChunkResult[];
99
+ /** Total bytes (gzipped) actually purged from S3. */
100
+ bytesPurged: number;
101
+ /** Total chunks actually purged (i.e., outcome === 'purged'). */
102
+ chunksPurged: number;
103
+ /** Wall-clock duration of the whole sweep. */
104
+ durationMs: number;
105
+ }
106
+ /**
107
+ * Phase 2 — chunk-index row shape returned by
108
+ * `BaseColdStore.listPurgeableChunks`. Mirrors `cold_store_chunks` row
109
+ * with a normalized `maxColdDays` field.
110
+ */
111
+ export interface PurgeableChunk {
112
+ table: string;
113
+ tenantId: string;
114
+ chunkId: string;
115
+ bucket: string;
116
+ archivedAt: Date;
117
+ gzipBytes: number;
118
+ rowCount: number;
119
+ maxColdDays: number;
120
+ objectKey: string;
121
+ }
122
+ /**
123
+ * Public cold-store API. Phase B implements all four core methods.
124
+ * Phase F adds `replayChunk` / `replayRow` for the rerun-from-archive
125
+ * flow.
126
+ */
127
+ export interface ColdStore {
128
+ /** Stream archived rows that overlap [fromTs, toTs). */
129
+ fetchRange<TRow>(args: ColdStoreFetchRangeArgs<TRow>): AsyncIterable<TRow>;
130
+ hasRange(args: Omit<ColdStoreFetchRangeArgs<unknown>, 'decode'>): Promise<boolean>;
131
+ countRange(args: Omit<ColdStoreFetchRangeArgs<unknown>, 'decode'>): Promise<number>;
132
+ /**
133
+ * Run one archive cycle. By default iterates every registered adapter
134
+ * in registration order; pass `tableFilter` to restrict to a single
135
+ * adapter (used by `archive-now <table>` CLI commands so an operator
136
+ * can flush one table on demand without waiting for the next cron tick).
137
+ */
138
+ runArchiveCycle(opts?: {
139
+ tableFilter?: string;
140
+ }): Promise<ArchiveCycleSummary>;
141
+ /**
142
+ * Phase F — promote every row in a chunk back into PG transactionally.
143
+ * Idempotent on re-run via the adapter's `ON CONFLICT DO NOTHING`.
144
+ * Throws if the adapter doesn't implement `replayInsert`, the chunk
145
+ * is missing, or the contentHash check fails.
146
+ */
147
+ replayChunk(args: ColdStoreReplayChunkArgs): Promise<ColdStoreReplayResult>;
148
+ /**
149
+ * Phase F — locate the chunk containing `rowId` (via manifest
150
+ * `minRowId`/`maxRowId` bounds) and replay it. Returns
151
+ * `chunkId: null` when no manifest matches — caller handles as
152
+ * "row truly does not exist anywhere".
153
+ */
154
+ replayRow(args: ColdStoreReplayRowArgs): Promise<ColdStoreReplayResult>;
155
+ /**
156
+ * Phase 2 — purge expired chunks from S3.
157
+ *
158
+ * Looks up `cold_store_chunks` rows where
159
+ * `now() > archived_at + max_cold_days * INTERVAL '1 day'` AND
160
+ * `max_cold_days != 'forever'`. For each:
161
+ * 1. Acquire a per-chunk advisory lock so concurrent sweeps can't
162
+ * double-delete.
163
+ * 2. Issue `DeleteObject` for both the data and manifest keys.
164
+ * 3. Call `adapter.purgeChunkRecord(...)` to transactionally delete
165
+ * the row from `cold_store_chunks`, decrement the rollup, and
166
+ * write a `purge_chunk` audit row.
167
+ *
168
+ * `dryRun=true` (CLI default) skips steps 2–3 and returns the
169
+ * candidate list without mutating anything. Production scheduled
170
+ * sweeps pass `dryRun=false`.
171
+ */
172
+ purgeExpiredChunks(opts?: PurgeExpiredChunksOpts): Promise<PurgeExpiredChunksSummary>;
173
+ }
174
+ export interface BaseColdStoreDeps {
175
+ db: DbKind;
176
+ config: ColdStoreConfig;
177
+ instanceId: string;
178
+ /** Caller-supplied logger-like callable; omits pull on shared logger. */
179
+ log: (level: 'info' | 'warn' | 'error', msg: string, extra?: Record<string, unknown>) => void;
180
+ /** Caller-supplied LRU; shared across consumers if desired. */
181
+ chunkCache?: ChunkLru<string, Buffer>;
182
+ /**
183
+ * Pre-built S3 client. If omitted, the BaseColdStore constructs one
184
+ * from `config.storage` via `createS3Client`. Tests pass a mock.
185
+ */
186
+ s3Client?: S3Client;
187
+ }
188
+ export declare abstract class BaseColdStore implements ColdStore {
189
+ protected readonly db: DbKind;
190
+ protected readonly config: ColdStoreConfig;
191
+ protected readonly instanceId: string;
192
+ protected readonly log: BaseColdStoreDeps['log'];
193
+ protected readonly chunkCache: ChunkLru<string, Buffer> | undefined;
194
+ protected readonly s3: S3Client;
195
+ /** Registered table adapters by `table` name. */
196
+ protected readonly adapters: Map<string, TableAdapter<unknown>>;
197
+ protected constructor(deps: BaseColdStoreDeps);
198
+ /**
199
+ * Register a concrete table adapter. Subclasses typically call this
200
+ * from their constructor.
201
+ */
202
+ protected registerAdapter(adapter: TableAdapter<unknown>): void;
203
+ /** Test / CLI helper — adapter-map read access. */
204
+ getAdapter(table: string): TableAdapter<unknown> | undefined;
205
+ listAdapters(): ReadonlyArray<TableAdapter<unknown>>;
206
+ /**
207
+ * CLI-accessible S3 primitives. Exposed so admin CLI commands can run
208
+ * inspection / reconciliation operations without a direct dependency
209
+ * on `@aws-sdk/client-s3`. Not intended for hot-path use.
210
+ */
211
+ get bucket(): string;
212
+ get storagePrefix(): string;
213
+ /** List all object keys under a prefix, paging internally. */
214
+ listObjectKeys(prefix: string): Promise<string[]>;
215
+ /** Fetch an object's raw body as a Buffer. */
216
+ getObjectBody(key: string): Promise<Buffer>;
217
+ /** Upload a manifest JSON object at `key`. */
218
+ putManifestObject(key: string, manifest: ChunkManifest): Promise<void>;
219
+ /**
220
+ * Delete a single object by key. CLI-accessible companion to
221
+ * `listObjectKeys` / `getObjectBody`, used by tenant-scoped sweep
222
+ * paths (e.g. the post-`deleteOrgCascade` S3 cleanup) that operate
223
+ * on chunks the index already forgot about. Does NOT touch the
224
+ * `cold_store_chunks` index — callers that need both must follow
225
+ * the `purgeExpiredChunks` pattern with `withPurgeLock` +
226
+ * `adapter.purgeChunkRecord`.
227
+ */
228
+ deleteObject(key: string): Promise<void>;
229
+ runArchiveCycle(opts?: {
230
+ tableFilter?: string;
231
+ }): Promise<ArchiveCycleSummary>;
232
+ private processAdapter;
233
+ /**
234
+ * Archive one (tenant, partitionDate).
235
+ *
236
+ * - Adapters WITHOUT `coldTtlDays`: legacy single-chunk path. Writes one
237
+ * chunk per partition at the day-prefix root with a v1 manifest. The
238
+ * GC sweep treats v1 chunks as `'forever'`.
239
+ * - Adapters WITH `coldTtlDays`: Phase 2 per-bucket path. Buffers all
240
+ * eligible rows, groups by `coldDaysToBucket(coldTtlDays(row))`,
241
+ * then emits one chunk per non-empty bucket under the bucket
242
+ * subprefix with a v2 manifest carrying `bucket` + `maxColdDays`.
243
+ *
244
+ * In both paths the caller holds the advisory lock for the
245
+ * partition. Returns the total row count archived across all chunks
246
+ * so the outer loop can honor `maxRowsPerCycle`.
247
+ */
248
+ private archivePartition;
249
+ /**
250
+ * Legacy single-chunk-per-partition flow. Adapters that do not
251
+ * implement `coldTtlDays` get a v1 manifest at the day-prefix root.
252
+ */
253
+ private archivePartitionLegacy;
254
+ /**
255
+ * Phase 2 per-bucket flow. Buffers all eligible rows, groups by
256
+ * `coldDaysToBucket(coldTtlDays(row))`, then emits one chunk per
257
+ * non-empty bucket.
258
+ */
259
+ private archivePartitionBucketed;
260
+ /**
261
+ * Common chunk-write flow: PUT data, verify, PUT manifest, transactional
262
+ * mark-archived-and-delete on the adapter, increment metrics.
263
+ *
264
+ * Shared by `archivePartitionLegacy` (v1 manifests) and
265
+ * `archivePartitionBucketed` (v2 manifests with bucket + maxColdDays).
266
+ */
267
+ private writeChunk;
268
+ private putChunkData;
269
+ private verifyChunkData;
270
+ private putManifest;
271
+ fetchRange<TRow>(args: ColdStoreFetchRangeArgs<TRow>): AsyncIterable<TRow>;
272
+ hasRange(args: Omit<ColdStoreFetchRangeArgs<unknown>, 'decode'>): Promise<boolean>;
273
+ countRange(args: Omit<ColdStoreFetchRangeArgs<unknown>, 'decode'>): Promise<number>;
274
+ /**
275
+ * Resolve all manifest sidecars whose time-range overlaps
276
+ * [fromTs, toTs). Uses one LIST walk under the tenant prefix, then
277
+ * GETs each `.manifest.json`.
278
+ */
279
+ private listRelevantManifests;
280
+ private getManifest;
281
+ private getChunkData;
282
+ /**
283
+ * Promote every row in a chunk back into PG. The adapter owns the
284
+ * actual INSERT + audit + chunk-counter decrement (transactional).
285
+ *
286
+ * Throws when:
287
+ * - the cold store is disabled
288
+ * - the adapter is unregistered or doesn't implement `replayInsert`
289
+ * - the manifest is missing
290
+ * - the data file's sha256 doesn't match the manifest's
291
+ * `contentHash` (refuse to replay a tampered chunk)
292
+ *
293
+ * Idempotent on re-run: the adapter's `ON CONFLICT DO NOTHING`
294
+ * collapses the second pass to `inserted=0, skipped=rowCount`.
295
+ */
296
+ replayChunk(args: ColdStoreReplayChunkArgs): Promise<ColdStoreReplayResult>;
297
+ /**
298
+ * Locate the chunk whose `[minRowId, maxRowId]` window covers
299
+ * `rowId` and replay it. Scans manifests under the tenant prefix
300
+ * (LIST is one round-trip; manifests are tiny). When several manifests
301
+ * match (overlapping ranges from re-archives), the one with the
302
+ * smallest range is preferred — it's the most recently re-archived
303
+ * shard and therefore the one whose row contents are freshest.
304
+ *
305
+ * Returns `chunkId: null` when no manifest matches — the caller
306
+ * decides whether that means "row never existed" (rerun → 404) or
307
+ * "S3 outage" (rerun → 410 replayFailed). This method only fails
308
+ * loudly on infrastructure errors (missing chunk after manifest
309
+ * found, hash mismatch).
310
+ */
311
+ replayRow(args: ColdStoreReplayRowArgs): Promise<ColdStoreReplayResult>;
312
+ private toDate;
313
+ /**
314
+ * Subclass hook — query the per-DB `cold_store_chunks` index for
315
+ * purge candidates. Concrete subclasses (PlatformColdStore /
316
+ * OrchestratorColdStore) implement this with one Kysely SQL query
317
+ * keyed off `archived_at + max_cold_days * INTERVAL '1 day' < now()`
318
+ * and `max_cold_days != 'forever'`.
319
+ *
320
+ * Returning an empty array makes `purgeExpiredChunks` a no-op.
321
+ * Default implementation returns `[]` so non-purge subclasses (or
322
+ * test fixtures) can opt out trivially.
323
+ */
324
+ protected listPurgeableChunks(_opts: {
325
+ tableFilter?: string;
326
+ bucketFilter?: string;
327
+ limit: number;
328
+ }): Promise<PurgeableChunk[]>;
329
+ /**
330
+ * Acquire a per-chunk advisory lock so concurrent sweeps can't
331
+ * race on the same `DeleteObject` + `cold_store_chunks` row.
332
+ *
333
+ * Default implementation is a no-op (always succeeds). Subclasses
334
+ * with a Kysely instance override to use `pg_try_advisory_lock` on
335
+ * `hashtext('cold-store-purge|<db>|<table>|<chunkId>')` and release
336
+ * inside `fn`'s `finally`. Returns `null` when another worker holds
337
+ * the lock; returns the `fn` result on success.
338
+ */
339
+ protected withPurgeLock<T>(_args: {
340
+ table: string;
341
+ chunkId: string;
342
+ }, fn: () => Promise<T>): Promise<T | null>;
343
+ purgeExpiredChunks(opts?: PurgeExpiredChunksOpts): Promise<PurgeExpiredChunksSummary>;
344
+ }
345
+ //# sourceMappingURL=cold-store.d.ts.map