@voltro/protocol 0.54.0 → 0.56.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 +639 -2
- package/dist/identity.d.ts +168 -0
- package/dist/identity.js +106 -0
- package/dist/index.d.ts +196 -14
- package/dist/index.js +496 -465
- package/dist/rest.d.ts +37 -6
- package/package.json +8 -3
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an inspect readout should call this process's mode.
|
|
3
|
+
*
|
|
4
|
+
* A projection of `bootPath`, not a second decision — the two boot paths each
|
|
5
|
+
* wrote a literal here, and one of them wrote the wrong one for years.
|
|
6
|
+
* `build`, `cli` and `test` collapse to `'cli'`: none of them serves traffic,
|
|
7
|
+
* and pretending otherwise would be the same category error in reverse.
|
|
8
|
+
*/
|
|
9
|
+
export declare const appMode: () => "dev" | "serve" | "start" | "cli";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The framework version this process runs, or `undefined` when it could not be
|
|
13
|
+
* determined. The ONE answer — there were three, and one of them read
|
|
14
|
+
* `npm_package_version`, which is the APP's version, not the framework's.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT `processIdentity().version`, though it returns the same
|
|
17
|
+
* value. Asking for the version must not resolve the identity: a boot path
|
|
18
|
+
* holds the version in a module-level constant, so routing it through the
|
|
19
|
+
* identity froze `bootPath` at import time — before the command that knows it
|
|
20
|
+
* had run — and the resolver then warned about its own caller. A version is
|
|
21
|
+
* not a fact about which replica this is.
|
|
22
|
+
*/
|
|
23
|
+
export declare const frameworkVersion: () => string | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Just the instance id — this PROCESS, not the name it wears.
|
|
27
|
+
*
|
|
28
|
+
* The distinction matters wherever a monotonic counter is compared across
|
|
29
|
+
* restarts. A replica id is a NAME and names survive: a StatefulSet pod keeps
|
|
30
|
+
* `POD_NAME`, and `VOLTRO_REPLICA_ID` is stable by definition. A counter does
|
|
31
|
+
* not survive — it starts at 1 again — so anything that reads "is this newer
|
|
32
|
+
* than what I have from X" must key on the process, or it silently answers "no"
|
|
33
|
+
* for as long as the dead process's high-water mark stands.
|
|
34
|
+
*/
|
|
35
|
+
export declare const instanceId: () => string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which command this process is.
|
|
39
|
+
*
|
|
40
|
+
* `serve` and `start` are separate because they are separate commands with
|
|
41
|
+
* separate capabilities — `voltro start` is web-only and refuses an api — and
|
|
42
|
+
* collapsing them is what made an inspect readout name a command the process
|
|
43
|
+
* could not have been started by. Only ever affects the LOCAL fallback id.
|
|
44
|
+
*/
|
|
45
|
+
export declare type ProcessBootPath = 'dev' | 'serve' | 'start' | 'build' | 'cli' | 'test';
|
|
46
|
+
|
|
47
|
+
export declare interface ProcessIdentity {
|
|
48
|
+
/**
|
|
49
|
+
* The place in the fleet: `VOLTRO_REPLICA_ID` → `POD_NAME` → `HOSTNAME` →
|
|
50
|
+
* `<bootPath>-<pid>`.
|
|
51
|
+
*
|
|
52
|
+
* STABLE ACROSS RESTARTS in a real deployment — a pod that dies and comes
|
|
53
|
+
* back is the same replica. Pair it with `startedAt` to talk about one
|
|
54
|
+
* process.
|
|
55
|
+
*/
|
|
56
|
+
readonly replicaId: string;
|
|
57
|
+
/**
|
|
58
|
+
* One PROCESS: `<replicaId>@<startedAt>.<nonce>`. A restart changes it.
|
|
59
|
+
*
|
|
60
|
+
* The nonce is not decoration. `startedAt` has millisecond resolution, so
|
|
61
|
+
* two generations of one replica that start inside the same millisecond
|
|
62
|
+
* produce the same string — and an aggregation keyed on it would then merge
|
|
63
|
+
* two processes' state into one, which is precisely the arithmetic across
|
|
64
|
+
* generations the generation exists to prevent. Rare in a deployment,
|
|
65
|
+
* routine in a test worker, and a wrong merge is silent either way.
|
|
66
|
+
*/
|
|
67
|
+
readonly instanceId: string;
|
|
68
|
+
/** This process's start, on ITS OWN clock. An identity, never compared to
|
|
69
|
+
* another process's clock — see `instanceMembership`. */
|
|
70
|
+
readonly startedAt: number;
|
|
71
|
+
/** The framework version. `undefined` when it could not be determined —
|
|
72
|
+
* which must not read the same as a version, so it is not `'0.0.0'`. */
|
|
73
|
+
readonly version: string | undefined;
|
|
74
|
+
/** The app's own version, when a boot path knows it. */
|
|
75
|
+
readonly appVersion: string | undefined;
|
|
76
|
+
readonly bootPath: ProcessBootPath;
|
|
77
|
+
/**
|
|
78
|
+
* `host:port` another replica can reach this one's inspect surface on, or
|
|
79
|
+
* `undefined`.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately not derived from the bind address: a process bound to
|
|
82
|
+
* `0.0.0.0:4000` is not reachable AT `0.0.0.0`, and a loopback address is
|
|
83
|
+
* reachable only by itself. `resolveRunnerIdentity` already carries this
|
|
84
|
+
* distinction as `localhostRisk` for the cluster runner; this is the same
|
|
85
|
+
* fact for the inspect surface.
|
|
86
|
+
*/
|
|
87
|
+
readonly reachableAt: string | undefined;
|
|
88
|
+
/** True only when `reachableAt` is set AND is not loopback. */
|
|
89
|
+
readonly reachable: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This process's identity. Stable for the lifetime of the process. */
|
|
93
|
+
export declare const processIdentity: () => ProcessIdentity;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Does the ENVIRONMENT say this process is one of several?
|
|
97
|
+
*
|
|
98
|
+
* A shape question, not a count: it answers "is a process-local default going to
|
|
99
|
+
* be wrong here", which is the only thing the callers need. Several defaults in
|
|
100
|
+
* this framework are correct for one process and quietly wrong for a fleet — a
|
|
101
|
+
* process-local rate-limit store multiplies the limit by the replica count, a
|
|
102
|
+
* process-local read-your-writes position store makes a user's own write
|
|
103
|
+
* invisible on the next request, an in-process broadcast bus carries nothing
|
|
104
|
+
* across pods. Each of those warns on THIS evidence rather than on a guess.
|
|
105
|
+
*
|
|
106
|
+
* Lives here, beside the process identity, because it reads the same env and
|
|
107
|
+
* because the alternative was three copies: it began in `@voltro/cli`'s
|
|
108
|
+
* reactivity audit, and a plugin cannot import the CLI.
|
|
109
|
+
*
|
|
110
|
+
* Returns the SIGNAL as a human-readable string (so the warning can cite what it
|
|
111
|
+
* saw) or `null` for "nothing says so".
|
|
112
|
+
*/
|
|
113
|
+
export declare const replicaEvidence: (env?: Record<string, string | undefined>) => string | null;
|
|
114
|
+
|
|
115
|
+
/** Just the replica id — the common case, and the ONLY spelling of it. */
|
|
116
|
+
export declare const replicaId: () => string;
|
|
117
|
+
|
|
118
|
+
/** Reset for tests. A process has one identity; a test worker runs many. */
|
|
119
|
+
export declare const resetProcessIdentityForTest: (bootPath?: ProcessBootPath) => void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the advertised host the same way the cluster runner does, so an
|
|
123
|
+
* operator does not have to configure two things that mean one thing.
|
|
124
|
+
*/
|
|
125
|
+
export declare const resolveAdvertisedHost: (env?: NodeJS.ProcessEnv) => {
|
|
126
|
+
readonly host: string;
|
|
127
|
+
readonly declared: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Where a PEER can reach this process's inspect surface, as `host:port`.
|
|
132
|
+
*
|
|
133
|
+
* The boot path knows the port; the host comes from `POD_IP` /
|
|
134
|
+
* `VOLTRO_INSPECT_ADVERTISE_HOST` and is a loopback address when neither is
|
|
135
|
+
* set. Passing a loopback host is not an error — it is recorded and reported
|
|
136
|
+
* as `reachable: false`, which is what lets a fleet request answer "this one
|
|
137
|
+
* cannot be reached" instead of waiting for a timeout.
|
|
138
|
+
*/
|
|
139
|
+
export declare const setProcessAdvertisedAddress: (hostPort: string | undefined,
|
|
140
|
+
/**
|
|
141
|
+
* Whether the HOST was declared by the operator rather than fallen back to.
|
|
142
|
+
*
|
|
143
|
+
* This is the difference between a loopback address that is a guess and one
|
|
144
|
+
* that is a statement. `POD_IP` unset means we do not know where we are and
|
|
145
|
+
* `127.0.0.1` is our shrug — a peer must not try it. An operator who sets
|
|
146
|
+
* `VOLTRO_INSPECT_ADVERTISE_HOST` has told us, and telling us `127.0.0.1` is
|
|
147
|
+
* a legitimate thing to say when the peers really are on this machine.
|
|
148
|
+
*
|
|
149
|
+
* Same shape as every other declaration in this framework: we do not
|
|
150
|
+
* second-guess one, and we do not treat a fallback as one.
|
|
151
|
+
*/
|
|
152
|
+
declared?: boolean) => void;
|
|
153
|
+
|
|
154
|
+
/** The app's own version, when the boot path knows it. */
|
|
155
|
+
export declare const setProcessAppVersion: (version: string | undefined) => void;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Name which command this process is. Call as early as a boot path can.
|
|
159
|
+
*
|
|
160
|
+
* Only ever affects the LOCAL fallback (`dev-1234` vs `serve-1234`); in any
|
|
161
|
+
* deployment `POD_NAME` or `HOSTNAME` decides and this is cosmetic. Called
|
|
162
|
+
* after the identity has already been handed out, it does NOT rewrite it — an
|
|
163
|
+
* id that changes mid-process is worse than a slightly wrong label — and says
|
|
164
|
+
* so loudly, because silence is worse than both.
|
|
165
|
+
*/
|
|
166
|
+
export declare const setProcessBootPath: (kind: ProcessBootPath) => void;
|
|
167
|
+
|
|
168
|
+
export { }
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createLogger as e } from "@voltro/logger";
|
|
2
|
+
import { readFileSync as t } from "node:fs";
|
|
3
|
+
import { dirname as n, join as r } from "node:path";
|
|
4
|
+
import { fileURLToPath as i } from "node:url";
|
|
5
|
+
//#region src/processIdentity.ts
|
|
6
|
+
var a = e({ scope: "voltro:identity" }), o = Symbol.for("@voltro/runtime/processIdentity"), s = globalThis, c = () => Math.random().toString(36).slice(2, 8), l = s[o] ?? (s[o] = {
|
|
7
|
+
bootPath: "cli",
|
|
8
|
+
startedAt: Date.now(),
|
|
9
|
+
nonce: c(),
|
|
10
|
+
versionResolved: !1
|
|
11
|
+
}), u = /* @__PURE__ */ new Set([
|
|
12
|
+
"localhost",
|
|
13
|
+
"127.0.0.1",
|
|
14
|
+
"::1",
|
|
15
|
+
"0.0.0.0",
|
|
16
|
+
""
|
|
17
|
+
]), d = (e) => {
|
|
18
|
+
if (e.startsWith("[")) return e.slice(1, e.indexOf("]"));
|
|
19
|
+
let t = e.lastIndexOf(":");
|
|
20
|
+
return t === -1 ? e : e.slice(0, t);
|
|
21
|
+
}, f = () => {
|
|
22
|
+
let e = globalThis;
|
|
23
|
+
return typeof e.__VOLTRO_FRAMEWORK_VERSION__ == "string" && e.__VOLTRO_FRAMEWORK_VERSION__.length > 0 ? e.__VOLTRO_FRAMEWORK_VERSION__ : void 0;
|
|
24
|
+
}, p = () => {
|
|
25
|
+
if (l.versionResolved) return l.version;
|
|
26
|
+
l.versionResolved = !0;
|
|
27
|
+
let e = f();
|
|
28
|
+
if (e !== void 0) return l.version = e, l.version;
|
|
29
|
+
let a = n(i(import.meta.url));
|
|
30
|
+
for (let e of [r(a, "..", "package.json"), r(a, "..", "..", "package.json")]) try {
|
|
31
|
+
let n = JSON.parse(t(e, "utf8"));
|
|
32
|
+
if (n.name === "@voltro/protocol" && typeof n.version == "string") return l.version = n.version, l.version;
|
|
33
|
+
} catch {}
|
|
34
|
+
return l.version;
|
|
35
|
+
}, m = (e) => {
|
|
36
|
+
if (l.frozen !== void 0 && l.frozen.bootPath !== e) {
|
|
37
|
+
a.warn(`process identity was already resolved as ${l.frozen.replicaId} (bootPath=${l.frozen.bootPath}); keeping it rather than becoming ${e}. Call setProcessBootPath before anything reads the identity.`, {
|
|
38
|
+
resolved: l.frozen.replicaId,
|
|
39
|
+
was: l.frozen.bootPath,
|
|
40
|
+
requested: e
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
l.bootPath = e;
|
|
45
|
+
}, h = (e) => {
|
|
46
|
+
l.appVersion = e, l.frozen !== void 0 && (l.frozen = {
|
|
47
|
+
...l.frozen,
|
|
48
|
+
appVersion: e
|
|
49
|
+
});
|
|
50
|
+
}, g = (e, t = !1) => {
|
|
51
|
+
l.advertised = e, l.advertisedDeclared = t, l.frozen !== void 0 && (l.frozen = {
|
|
52
|
+
...l.frozen,
|
|
53
|
+
reachableAt: e,
|
|
54
|
+
reachable: _(e, t)
|
|
55
|
+
});
|
|
56
|
+
}, _ = (e, t) => e !== void 0 && (t || !u.has(d(e))), v = (e = process.env) => {
|
|
57
|
+
let t = e.VOLTRO_INSPECT_ADVERTISE_HOST;
|
|
58
|
+
if (t !== void 0 && t !== "") return {
|
|
59
|
+
host: t,
|
|
60
|
+
declared: !0
|
|
61
|
+
};
|
|
62
|
+
let n = e.POD_IP;
|
|
63
|
+
return n !== void 0 && n !== "" ? {
|
|
64
|
+
host: n,
|
|
65
|
+
declared: !0
|
|
66
|
+
} : {
|
|
67
|
+
host: "127.0.0.1",
|
|
68
|
+
declared: !1
|
|
69
|
+
};
|
|
70
|
+
}, y = () => {
|
|
71
|
+
if (l.frozen !== void 0) return l.frozen;
|
|
72
|
+
let e = process.env, t = e.VOLTRO_REPLICA_ID ?? e.POD_NAME ?? e.HOSTNAME ?? `${l.bootPath}-${process.pid}`, n = l.advertised, r = {
|
|
73
|
+
replicaId: t,
|
|
74
|
+
instanceId: `${t}@${l.startedAt}.${l.nonce}`,
|
|
75
|
+
startedAt: l.startedAt,
|
|
76
|
+
version: p(),
|
|
77
|
+
appVersion: l.appVersion,
|
|
78
|
+
bootPath: l.bootPath,
|
|
79
|
+
reachableAt: n,
|
|
80
|
+
reachable: _(n, l.advertisedDeclared === !0)
|
|
81
|
+
};
|
|
82
|
+
return l.frozen = r, r;
|
|
83
|
+
}, b = () => y().replicaId, x = () => y().instanceId, S = (e = process.env) => {
|
|
84
|
+
let t = Number(e.REPLICA_COUNT);
|
|
85
|
+
if (Number.isFinite(t) && t >= 1) return t > 1 ? `REPLICA_COUNT=${t}` : null;
|
|
86
|
+
for (let [t, n] of [
|
|
87
|
+
["KUBERNETES_SERVICE_HOST", "Kubernetes"],
|
|
88
|
+
["POD_IP", "Kubernetes (downward API)"],
|
|
89
|
+
["POD_NAME", "Kubernetes (downward API)"],
|
|
90
|
+
["FLY_ALLOC_ID", "Fly.io"],
|
|
91
|
+
["FLY_MACHINE_ID", "Fly.io"],
|
|
92
|
+
["ECS_CONTAINER_METADATA_URI_V4", "AWS ECS"],
|
|
93
|
+
["ECS_CONTAINER_METADATA_URI", "AWS ECS"],
|
|
94
|
+
["K_REVISION", "Cloud Run / Knative"],
|
|
95
|
+
["CONTAINER_APP_REPLICA_NAME", "Azure Container Apps"],
|
|
96
|
+
["RENDER_INSTANCE_ID", "Render"]
|
|
97
|
+
]) if ((e[t] ?? "") !== "") return `${t} is set (${n})`;
|
|
98
|
+
return null;
|
|
99
|
+
}, C = (e = "test") => {
|
|
100
|
+
l.frozen = void 0, l.bootPath = e, l.startedAt = Date.now(), l.nonce = c(), l.appVersion = void 0, l.advertised = void 0, l.advertisedDeclared = !1;
|
|
101
|
+
}, w = () => p(), T = () => {
|
|
102
|
+
let e = y().bootPath;
|
|
103
|
+
return e === "dev" || e === "serve" || e === "start" ? e : "cli";
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
export { T as appMode, w as frameworkVersion, x as instanceId, y as processIdentity, S as replicaEvidence, b as replicaId, C as resetProcessIdentityForTest, v as resolveAdvertisedHost, g as setProcessAdvertisedAddress, h as setProcessAppVersion, m as setProcessBootPath };
|
package/dist/index.d.ts
CHANGED
|
@@ -629,8 +629,11 @@ export declare interface CachedScopeDecision {
|
|
|
629
629
|
* - `x.y.z` — exact
|
|
630
630
|
* - `*` — any
|
|
631
631
|
*/
|
|
632
|
-
export declare const checkFrameworkCompat: (pluginName: string, range: string | undefined,
|
|
632
|
+
export declare const checkFrameworkCompat: (pluginName: string, range: string | undefined,
|
|
633
|
+
/** `undefined` when the running version could not be determined. */
|
|
634
|
+
runningVersion: string | undefined) => {
|
|
633
635
|
ok: true;
|
|
636
|
+
unverified?: string;
|
|
634
637
|
} | {
|
|
635
638
|
ok: false;
|
|
636
639
|
reason: string;
|
|
@@ -696,6 +699,10 @@ export declare interface ClientTarget {
|
|
|
696
699
|
* `path` is set. 2nd arg is the optimistic id (insert) or the current item
|
|
697
700
|
* (update) — `unknown` here since the erased view spans both; normalize casts. */
|
|
698
701
|
readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
|
|
702
|
+
/** Declared junction relations (see {@link TargetRelations}). Pure DATA —
|
|
703
|
+
* three strings per relation — so it crosses to the browser unchanged and
|
|
704
|
+
* drives junction auto-optimistic there. */
|
|
705
|
+
readonly relations?: TargetRelations | undefined;
|
|
699
706
|
}
|
|
700
707
|
|
|
701
708
|
export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrategy>, options?: {
|
|
@@ -2655,6 +2662,18 @@ export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>
|
|
|
2655
2662
|
*/
|
|
2656
2663
|
export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
|
|
2657
2664
|
|
|
2665
|
+
/**
|
|
2666
|
+
* Derive the short alias used as the rpc-tag prefix for a plugin's routes.
|
|
2667
|
+
* Pure — depends only on the plugin name.
|
|
2668
|
+
*
|
|
2669
|
+
* Examples:
|
|
2670
|
+
* '@voltro/plugin-audit' → 'audit'
|
|
2671
|
+
* '@scope/plugin-rateLimit' → 'rateLimit'
|
|
2672
|
+
* 'plain-name' → 'plainName'
|
|
2673
|
+
* '@voltro/audit' → 'audit' (no plugin- prefix)
|
|
2674
|
+
*/
|
|
2675
|
+
export declare const pluginAlias: (pluginName: string) => string;
|
|
2676
|
+
|
|
2658
2677
|
/**
|
|
2659
2678
|
* Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
|
|
2660
2679
|
* AFTER the framework has opened its store + pool. Gives a plugin the
|
|
@@ -2727,6 +2746,33 @@ export declare interface PluginBindContext {
|
|
|
2727
2746
|
* task early. The framework also stops every armed task at shutdown.
|
|
2728
2747
|
*/
|
|
2729
2748
|
readonly scheduleCoordinated: (name: string, intervalMs: number, effect: () => void | CoordinatedTickOutcome | Promise<void | CoordinatedTickOutcome>, options?: CoordinatedTaskOptions) => CoordinatedScheduleHandle;
|
|
2749
|
+
/**
|
|
2750
|
+
* Claim ONE change fleet-wide. `true` = this replica may run the effect;
|
|
2751
|
+
* `false` = another replica has it, or the claim could not be taken.
|
|
2752
|
+
*
|
|
2753
|
+
* The counterpart of `scheduleCoordinated` for the OTHER thing that fires on
|
|
2754
|
+
* every replica: a change tap. `onChangeEvent` is delivered to every replica —
|
|
2755
|
+
* that is what makes a `changeScope: 'fleet'` store cross-instance — so a tap
|
|
2756
|
+
* that COUNTS or SENDS multiplies by the replica count. `plugin-billing`
|
|
2757
|
+
* accrued a usage unit per metered row change and a tenant on two pods was
|
|
2758
|
+
* billed twice; the plugin whose entire job is counting had no way to say
|
|
2759
|
+
* "once".
|
|
2760
|
+
*
|
|
2761
|
+
* `scope` namespaces the key (use the plugin's own name); `key` must name the
|
|
2762
|
+
* CHANGE. Derive it with `changeDigest` + `OccurrenceCounter`
|
|
2763
|
+
* (`@voltro/database`) rather than inventing one — a fleet change carries no
|
|
2764
|
+
* LSN, no commit id and no `traceId`, so content plus its position among
|
|
2765
|
+
* content-identical repeats is the only thing two replicas provably agree on.
|
|
2766
|
+
*
|
|
2767
|
+
* AT MOST once: the claim is taken before the effect, so a replica that wins
|
|
2768
|
+
* and dies takes the change with it, and a claim that cannot be written is
|
|
2769
|
+
* taken by nobody. Fail-closed, like the rate slot and the budget guard.
|
|
2770
|
+
*
|
|
2771
|
+
* Required rather than optional, and deliberately: an absent gate reads
|
|
2772
|
+
* exactly like a gate that passed, and the whole point of this field is a
|
|
2773
|
+
* count.
|
|
2774
|
+
*/
|
|
2775
|
+
readonly claimChange: (scope: string, key: string) => Promise<boolean>;
|
|
2730
2776
|
}
|
|
2731
2777
|
|
|
2732
2778
|
/**
|
|
@@ -3257,11 +3303,18 @@ export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.
|
|
|
3257
3303
|
*
|
|
3258
3304
|
* **What an alias costs, stated because it is not obvious and nothing else
|
|
3259
3305
|
* says it:** the local and cloud dashboards fetch a plugin's inspect panel at
|
|
3260
|
-
* `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in
|
|
3261
|
-
*
|
|
3262
|
-
*
|
|
3263
|
-
*
|
|
3264
|
-
*
|
|
3306
|
+
* `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in, and
|
|
3307
|
+
* they are in different repositories, so they cannot follow an alias. That
|
|
3308
|
+
* used to mean an aliased plugin kept working while its panel 404'd. It no
|
|
3309
|
+
* longer does: `makePluginInspectRegistry` also mounts each plugin's inspect
|
|
3310
|
+
* endpoints under its CANONICAL slug, so the compiled-in path stays correct
|
|
3311
|
+
* however the app renames the plugin.
|
|
3312
|
+
*
|
|
3313
|
+
* The residue is `instance`, not `alias`. Two installs are two panels with one
|
|
3314
|
+
* canonical name, so they get NO shared mount — showing either under the
|
|
3315
|
+
* canonical path would hand a dashboard the other install's rows under a name
|
|
3316
|
+
* that looks right. Each is reachable at its own slug, which
|
|
3317
|
+
* `/_voltro/inspect/plugins` reports as `inspectSlug`.
|
|
3265
3318
|
*
|
|
3266
3319
|
* @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
|
|
3267
3320
|
* @param alias replaces the whole namespace — an app-chosen name
|
|
@@ -3508,6 +3561,55 @@ export declare interface PluginSchemaContribution {
|
|
|
3508
3561
|
readonly migrations?: ReadonlyArray<PluginMigration>;
|
|
3509
3562
|
}
|
|
3510
3563
|
|
|
3564
|
+
/**
|
|
3565
|
+
* Derive the URL slug for a plugin's HTTP-facing surfaces (the inspect mount
|
|
3566
|
+
* `/_voltro/inspect/plugins/<slug>/…`). Unlike `pluginAlias` (an identifier
|
|
3567
|
+
* for rpc tags / codegen, camelCase), the slug stays KEBAB-CASE — the shape
|
|
3568
|
+
* every dashboard fetches (`plugins/cdc-out/sinks`, never `plugins/cdcOut/…`)
|
|
3569
|
+
* and the shape the package name already carries. Instance-suffixed names
|
|
3570
|
+
* (`@voltro/plugin-cdc-out#analytics`) map `#` → `--` so the slug stays a
|
|
3571
|
+
* fetchable URL path segment.
|
|
3572
|
+
*
|
|
3573
|
+
* Examples:
|
|
3574
|
+
* '@voltro/plugin-audit' → 'audit'
|
|
3575
|
+
* '@voltro/plugin-cdc-out' → 'cdc-out'
|
|
3576
|
+
* '@scope/plugin-rateLimit' → 'rate-limit'
|
|
3577
|
+
* '@voltro/plugin-cdc-out#analytics'→ 'cdc-out--analytics'
|
|
3578
|
+
*/
|
|
3579
|
+
export declare const pluginSlug: (pluginName: string) => string;
|
|
3580
|
+
|
|
3581
|
+
/**
|
|
3582
|
+
* The wire tag a plugin's own client code must call for one of its routes.
|
|
3583
|
+
*
|
|
3584
|
+
* `pluginTag('@voltro/plugin-notifications', 'inbox')` is `'notifications.inbox'`
|
|
3585
|
+
* in a default app and `'inbox.inbox'` in an app that installed the plugin as
|
|
3586
|
+
* `notificationsPlugin({ alias: 'inbox' })`. Every hook a plugin ships goes
|
|
3587
|
+
* through this instead of spelling its own namespace, because the namespace is
|
|
3588
|
+
* the app's to choose and the hook is the half of the move that a literal
|
|
3589
|
+
* string cannot follow.
|
|
3590
|
+
*
|
|
3591
|
+
* **Resolution, and the one case that fails closed.** A base may have several
|
|
3592
|
+
* registered aliases when an app installs the same plugin twice
|
|
3593
|
+
* (`instance: 'analytics'` → `notifications#analytics`):
|
|
3594
|
+
*
|
|
3595
|
+
* - exactly one registered alias → that one;
|
|
3596
|
+
* - several, exactly one of which carries no `#` → the un-suffixed install.
|
|
3597
|
+
* Two installs of a plugin have a primary and a secondary, and a hook with
|
|
3598
|
+
* no way to name an install means the primary;
|
|
3599
|
+
* - several, none or more than one un-suffixed → **throws**. The hook is being
|
|
3600
|
+
* asked which of two servers it addresses and has no basis to pick; a guess
|
|
3601
|
+
* here writes to the wrong install's tables, which is worse than a stack
|
|
3602
|
+
* trace naming both candidates. Call the tag explicitly (`useSubscription(api,
|
|
3603
|
+
* 'notifications#analytics.inbox')`) to say which you mean.
|
|
3604
|
+
* - none registered → the canonical `pluginAlias(base)`. This is the path a
|
|
3605
|
+
* unit test rendering a hook in isolation takes, and the path an app takes
|
|
3606
|
+
* before its generated rpcGroup has loaded. It is NOT a fail-open guess: an
|
|
3607
|
+
* app that aliased and somehow lost its registration calls a tag the client
|
|
3608
|
+
* group does not contain, and the transport refuses it by name rather than
|
|
3609
|
+
* routing it somewhere plausible.
|
|
3610
|
+
*/
|
|
3611
|
+
export declare const pluginTag: (baseName: string, route: string) => string;
|
|
3612
|
+
|
|
3511
3613
|
/**
|
|
3512
3614
|
* A scaffold template a plugin contributes to `voltro init` /
|
|
3513
3615
|
* `voltro add-app`. Templates are listed under the plugin's name in
|
|
@@ -3887,10 +3989,29 @@ export declare type ReactivitySource = TableName | ReactivityChannel;
|
|
|
3887
3989
|
*/
|
|
3888
3990
|
export declare type ReactivitySourceValue = string | ReactivityChannel;
|
|
3889
3991
|
|
|
3992
|
+
/**
|
|
3993
|
+
* Declare, for this realm, what namespace each installed plugin actually
|
|
3994
|
+
* answers to. Emitted into `rpcGroup.generated.ts` by the codegen — one call
|
|
3995
|
+
* per api, mapping each plugin's CANONICAL base name to its EFFECTIVE alias.
|
|
3996
|
+
*
|
|
3997
|
+
* Emitted unconditionally whenever an app installs a plugin that contributes
|
|
3998
|
+
* client routes, not only when something was aliased. A registration that only
|
|
3999
|
+
* appeared in the aliased case would be a second code path exercised by nobody,
|
|
4000
|
+
* and the default case is where a regression would hide.
|
|
4001
|
+
*
|
|
4002
|
+
* Accumulates rather than overwrites: two apis in one browser bundle each
|
|
4003
|
+
* register their own map, and an app may install one plugin twice (`instance`).
|
|
4004
|
+
* The disambiguation happens at READ time — see `pluginTag`.
|
|
4005
|
+
*/
|
|
4006
|
+
export declare const registerPluginAliases: (aliases: Readonly<Record<string, string>>) => void;
|
|
4007
|
+
|
|
3890
4008
|
/** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
|
|
3891
4009
|
* Checks the EFFECTIVE set so role-derived scopes count. */
|
|
3892
4010
|
export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
|
|
3893
4011
|
|
|
4012
|
+
/** Drop every registration. Tests only — production registers once at module load. */
|
|
4013
|
+
export declare const resetPluginAliases: () => void;
|
|
4014
|
+
|
|
3894
4015
|
/** The question the runtime asks a registered resource-scope resolver. */
|
|
3895
4016
|
export declare interface ResourceScopeRequest {
|
|
3896
4017
|
/** The authenticated caller. */
|
|
@@ -4704,21 +4825,52 @@ declare const TableValidationFailed_base: Schema.TaggedErrorClass<TableValidatio
|
|
|
4704
4825
|
|
|
4705
4826
|
export declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | ReadonlyArray<TargetSpec<Input, Row>>;
|
|
4706
4827
|
|
|
4828
|
+
/**
|
|
4829
|
+
* ONE declared many-to-many relation of a write target: the junction table
|
|
4830
|
+
* plus its two reference columns.
|
|
4831
|
+
*
|
|
4832
|
+
* **Why the columns are declared and not derived.** The server can derive
|
|
4833
|
+
* them (`store.relationLinks` reads the junction's `reference()` targets),
|
|
4834
|
+
* and it still does — but the same declaration drives JUNCTION AUTO-OPTIMISTIC
|
|
4835
|
+
* in the BROWSER, and the browser has no table registry to derive from
|
|
4836
|
+
* (`@voltro/database` is server-only by construction). Guessing a column name
|
|
4837
|
+
* from the table name is the failure mode this codebase refuses everywhere
|
|
4838
|
+
* else, so the pair is stated. It is not taken on trust: before it writes, the
|
|
4839
|
+
* server compares the declaration against the junction's real reference
|
|
4840
|
+
* columns and refuses — naming the right pair — if they disagree. A wrong
|
|
4841
|
+
* declaration is a boot-loud error, never a silent client/server divergence.
|
|
4842
|
+
*/
|
|
4843
|
+
export declare interface TargetRelation {
|
|
4844
|
+
/** The junction (link) table. */
|
|
4845
|
+
readonly junction: string;
|
|
4846
|
+
/** The junction's reference column pointing at the TARGET's own table. */
|
|
4847
|
+
readonly anchorColumn: string;
|
|
4848
|
+
/** The junction's other reference column — the linked row's id. */
|
|
4849
|
+
readonly targetColumn: string;
|
|
4850
|
+
}
|
|
4851
|
+
|
|
4707
4852
|
/**
|
|
4708
4853
|
* Declared many-to-many RELATIONS of a write target: `{ inputField:
|
|
4709
|
-
*
|
|
4854
|
+
* TargetRelation }`. After the executor succeeds — inside the SAME
|
|
4710
4855
|
* transaction — the framework reconciles the junction's links for the
|
|
4711
4856
|
* written row against `input[inputField]` (an array of target ids) via the
|
|
4712
4857
|
* diff-based `store.relationLinks`, so a form's multi-reference field saves
|
|
4713
|
-
* in one mutation with no hand-written junction code.
|
|
4714
|
-
*
|
|
4715
|
-
*
|
|
4858
|
+
* in one mutation with no hand-written junction code.
|
|
4859
|
+
*
|
|
4860
|
+
* The SAME declaration drives the client's junction auto-optimistic: the
|
|
4861
|
+
* cache reconciles the junction rows of every subscription sourced on
|
|
4862
|
+
* `junction` the moment the mutation is sent, so a multi-reference field
|
|
4863
|
+
* flips as instantly as a scalar one instead of waiting for the server delta.
|
|
4864
|
+
* Those patches ride the ordinary optimistic lane (staged under the mutation
|
|
4865
|
+
* id): reverted on failure, kept on success until the base actually moves.
|
|
4716
4866
|
*
|
|
4717
4867
|
* An ABSENT input field leaves the links untouched (absent ≠ empty — an
|
|
4718
4868
|
* empty array is the explicit "clear them all"). The row id is the
|
|
4719
|
-
* executor's `output.id`, falling back to `input.id
|
|
4869
|
+
* executor's `output.id`, falling back to `input.id`; the client, which
|
|
4870
|
+
* cannot see `output` yet, uses `input.id` (and, for an insert, the same
|
|
4871
|
+
* optimistic id it stamped on the new row).
|
|
4720
4872
|
*/
|
|
4721
|
-
export declare type TargetRelations = Readonly<Record<string,
|
|
4873
|
+
export declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
|
|
4722
4874
|
|
|
4723
4875
|
export declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
|
|
4724
4876
|
|
|
@@ -5068,8 +5220,10 @@ export declare interface VoltroPlugin {
|
|
|
5068
5220
|
* declarations across seven plugins), so the escape hatch fires on all of
|
|
5069
5221
|
* them and the alias moves NOTHING on the rpc surface. A user aliasing
|
|
5070
5222
|
* `notifications` to escape a collision with their own `notifications.*`
|
|
5071
|
-
* routes would still collide — and would additionally
|
|
5072
|
-
* panel, which fetches the default slug. Worse than not
|
|
5223
|
+
* routes would still collide — and, at that time, would additionally have
|
|
5224
|
+
* lost the dashboard panel, which fetches the default slug. Worse than not
|
|
5225
|
+
* shipping the field. (The panel half is fixed since: see the canonical
|
|
5226
|
+
* mount in `makePluginInspectRegistry`.)
|
|
5073
5227
|
*
|
|
5074
5228
|
* With `baseName` set, both tag derivations strip a leading
|
|
5075
5229
|
* `<default-alias>.` before applying the EFFECTIVE alias, so
|
|
@@ -5497,6 +5651,34 @@ export declare const wireErrorUnion: (descriptor: {
|
|
|
5497
5651
|
readonly requiresApproval?: AnyApprovalPolicy | undefined;
|
|
5498
5652
|
}, kind: "query" | "mutation" | "action" | "stream") => Schema.Schema.All;
|
|
5499
5653
|
|
|
5654
|
+
/**
|
|
5655
|
+
* Re-tag a procedure descriptor for the wire.
|
|
5656
|
+
*
|
|
5657
|
+
* Every lifter below is `Rpc.make(descriptor.name, …)`, so the tag a procedure
|
|
5658
|
+
* answers to on the wire is the one baked into the DESCRIPTOR — not the one the
|
|
5659
|
+
* caller thinks it is emitting. That is fine for app-authored procedures, whose
|
|
5660
|
+
* descriptor and tag have one source. It was wrong for PLUGIN routes: an app
|
|
5661
|
+
* that installs a plugin under an `alias` moves the namespace, the server
|
|
5662
|
+
* registers the moved tag, and the generated client lifted the plugin's
|
|
5663
|
+
* descriptor unchanged — so the browser sent the tag the plugin AUTHORED to a
|
|
5664
|
+
* server that had stopped serving it. The identifier moved, the descriptor map
|
|
5665
|
+
* key moved, the type key moved; the only string the transport reads did not.
|
|
5666
|
+
*
|
|
5667
|
+
* The codegen wraps every plugin-contributed descriptor with this, always —
|
|
5668
|
+
* including the un-aliased case, where it is an identity in effect. A wrap that
|
|
5669
|
+
* only appeared under an alias would be a branch nothing exercises, and the
|
|
5670
|
+
* un-aliased case is the one a regression would hide in.
|
|
5671
|
+
*
|
|
5672
|
+
* Returns a COPY. The descriptor is imported by reference from the plugin's
|
|
5673
|
+
* browser-safe module and may be lifted by several apps in one process; mutating
|
|
5674
|
+
* `name` in place would re-tag it for all of them.
|
|
5675
|
+
*/
|
|
5676
|
+
export declare const withRpcTag: <D extends {
|
|
5677
|
+
readonly name: string;
|
|
5678
|
+
}, const T extends string>(descriptor: D, tag: T) => Omit<D, "name"> & {
|
|
5679
|
+
readonly name: T;
|
|
5680
|
+
};
|
|
5681
|
+
|
|
5500
5682
|
export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{
|
|
5501
5683
|
workflowName: typeof Schema.String;
|
|
5502
5684
|
executionId: typeof Schema.String;
|