@voltro/plugin-storage 0.46.0 → 0.47.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.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +456 -443
- package/dist/types.d.ts +11 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,36 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.47.0] — 2026-08-22
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **@voltro/database, @voltro/data-transfer, @voltro/cli, @voltro/plugin-storage** — A `--mode replace` no longer writes per-row history, and three things that were reported alongside it.
|
|
47
|
+
|
|
48
|
+
**Write recorders are suspended for a `replace`.** A replace SETS a state; it does not change rows, so a per-row history entry describes something that did not happen. A deployment measured what that costs: they run `versioningPlugin({ timing: 'in-transaction' })` on 70 of 80 tables, so one import wrote 242 950 history rows and doubled the write load of the most expensive run they make — and those rows were the source of refused writes they spent three rounds diagnosing.
|
|
49
|
+
|
|
50
|
+
The deciding argument is not the cost. **The import path had already decided this, and the recorder was the one layer that did not hear.** These routes write through the RAW store on purpose — no tenant scoping, no row filter, no `audit()` stamping. A recorder fired anyway because it hangs one level below the wrapper. Suspending it makes the layers agree.
|
|
51
|
+
|
|
52
|
+
Suspended for `replace` only: an `upsert` or an `append` CHANGES existing state, which is exactly what a recorder is for. Scoped per execution context rather than by a switch, because an import runs while the app serves requests and a process-global flag would silently drop recording for everything concurrent with it. And every such run SAYS what it suspended — dropping history quietly would be the same defect in a nicer costume.
|
|
53
|
+
|
|
54
|
+
This also removes the reason a staged `replace` used to fall back. A recorder is keyed by table name, so a staged write found none and the recorder never ran; rather than record inconsistently, the run took the slower path. For the deployment above that meant the staged path could never activate — permanently, on every environment.
|
|
55
|
+
|
|
56
|
+
**`voltro codegen` wrote a truncated table declaration.** It discovered tables with the walk that collects the rpc GROUP's inputs — descriptors, workflows, events, and deliberately no `*.entity.ts`. So the generated `voltro-tables.generated.d.ts` listed the framework's tables plus `actors`, which the framework injects. Measured downstream: 37 names where a `voltro dev` boot writes 117, and 299 `TS2322` errors from every `source:` naming one of their own tables. The way in was our own message — `voltro test` refuses a stale rpc group and tells you to run `voltro codegen`. Table discovery has its own walk now, and a test compares the two walks' RESULTS on a real tree rather than trusting they mean the same thing.
|
|
57
|
+
|
|
58
|
+
**The eager-relation doctor rule missed the state that is most wrong.** It resolved relations only against the tables a query already DECLARED, on the reasoning that the base is virtually always in `source:`. Measured: a query reading `projects` while declaring only `projectTeams` produced no finding at all, while the same query with the base added produced two. The base is a fact about the executor, so it is read from the executor now.
|
|
59
|
+
|
|
60
|
+
**A rollback capture says when its path shares the root filesystem.** The 409 answered "is storage configured", which is not the question — a `filesystem` provider pointed at a container directory with no volume behind it passes it and dies with the pod. What a process CAN observe is that a mounted volume is a different filesystem: `pathDurability` compares device ids, and a capture landing on the same device as `/` says so. Three-valued on purpose, and explicitly not an alarm on a development machine, where everything is one device and nothing is a pod.
|
|
61
|
+
|
|
62
|
+
**And the import records ITSELF, once.** Dropping per-row history left an operator asking "was this data imported, and when" with nothing to read, and trading too much for none is not obviously the better trade. `_voltro_data_imports` carries one row per RUN: the mode, the transport, the bundle, the SOURCE deployment's schema fingerprint, the counts, and — for the run an operator is actually looking for — the failure. It is written for a failed import as well as a finished one; a trail that only records successes goes quiet exactly when it is needed.
|
|
63
|
+
|
|
64
|
+
Best effort, unlike the interrupted-replace marker, and the difference is deliberate: the marker is a safety interlock and a run that cannot write it must not proceed, while history is valuable and not load-bearing. A target that has not been migrated yet still imports, and says the trace could not be written.
|
|
65
|
+
|
|
66
|
+
Two things it learned from being wired. The table is `.nonReactive()` — nobody subscribes to "an import happened", and a reactive bookkeeping write showed up in every suite that counts the LOAD's writes. And it is written only when the target actually DECLARES the table: the in-memory store accepts a write to any name, so an absent table cannot be detected by the write failing, and inventing the row would put a framework write into every embedder's counts.
|
|
67
|
+
|
|
68
|
+
**Measured at the reported magnitude.** 114 tables, 243 048 rows, real MariaDB, staged replace: the whole run takes 56.5s and the DESTRUCTIVE TRANSACTION takes **1.13s**. The window in which a dead process can leave a half-replaced target is the second number; before this it was the first.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
42
72
|
## [0.46.0] — 2026-08-22
|
|
43
73
|
|
|
44
74
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -162,6 +162,15 @@ export declare const decodeDataUri: (dataUri: string) => {
|
|
|
162
162
|
contentType: string;
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* What to tell an operator about a capture written to `path`.
|
|
167
|
+
*
|
|
168
|
+
* Says what was OBSERVED and what follows, and stops there. "Your storage is
|
|
169
|
+
* ephemeral" is a claim about their cluster; "this path is on the same
|
|
170
|
+
* filesystem as `/`" is a fact about this process.
|
|
171
|
+
*/
|
|
172
|
+
export declare const durabilityNotice: (path: string) => string | undefined;
|
|
173
|
+
|
|
165
174
|
/** Frame bytes for clamd `INSTREAM`: `zINSTREAM\0` then `<u32 len><data>`
|
|
166
175
|
* chunks, terminated by a zero-length chunk. Exposed for testing. */
|
|
167
176
|
export declare const encodeInstream: (bytes: Uint8Array, chunkSize?: number) => Buffer;
|
|
@@ -318,6 +327,16 @@ export declare const parseClamavResponse: (raw: string) => StorageScanResult;
|
|
|
318
327
|
* are present (→ caller serves the original / 302s to the CDN). */
|
|
319
328
|
export declare const parseTransformParams: (query: string) => TransformParams | null;
|
|
320
329
|
|
|
330
|
+
export declare type PathDurability = 'separate-mount' | 'same-device-as-root' | 'unknown';
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Compare `path`'s filesystem against the root filesystem's.
|
|
334
|
+
*
|
|
335
|
+
* Never throws: a path that does not exist yet, or a platform that reports no
|
|
336
|
+
* device, answers `unknown` — a diagnostic must not be the thing that fails.
|
|
337
|
+
*/
|
|
338
|
+
export declare const pathDurability: (path: string) => PathDurability;
|
|
339
|
+
|
|
321
340
|
/** A ref with the server-only `passwordHash` removed — safe to serialize. */
|
|
322
341
|
export declare type PublicStorageRef = Omit<StorageRef, 'passwordHash'>;
|
|
323
342
|
|
|
@@ -675,6 +694,17 @@ export declare interface StoragePluginOptions {
|
|
|
675
694
|
* access policy.
|
|
676
695
|
*/
|
|
677
696
|
export declare interface StorageProvider {
|
|
697
|
+
/**
|
|
698
|
+
* Where a FILESYSTEM provider writes. Absent for every other provider.
|
|
699
|
+
*
|
|
700
|
+
* It exists so a caller can ask a question this interface otherwise cannot
|
|
701
|
+
* answer: does what I write here survive the process? A rollback capture
|
|
702
|
+
* written for a pod that then dies is worthless, and `filesystem` is the one
|
|
703
|
+
* provider that can be either — a mounted volume or an ephemeral container
|
|
704
|
+
* directory. A deployment pointed this out: their check for "storage is
|
|
705
|
+
* configured" passed on a `/tmp` path with no volume behind it.
|
|
706
|
+
*/
|
|
707
|
+
readonly root?: string;
|
|
678
708
|
readonly name: string;
|
|
679
709
|
/** True when `getUrl` returns a real presigned URL (s3/minio). When
|
|
680
710
|
* false the service serves bytes via the `/_voltro/storage/:id`
|