@winstonsayno/mcp-gateway 5.1.0 → 5.3.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 +22 -0
- package/README.md +15 -0
- package/dashboard/index.html +17 -3
- package/dist/config/loader.d.ts +4 -4
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +4 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/features/edge-fleet.d.ts +97 -0
- package/dist/features/edge-fleet.d.ts.map +1 -0
- package/dist/features/edge-fleet.js +121 -0
- package/dist/features/edge-fleet.js.map +1 -0
- package/dist/features/index.d.ts +2 -0
- package/dist/features/index.d.ts.map +1 -1
- package/dist/features/index.js +2 -0
- package/dist/features/index.js.map +1 -1
- package/dist/features/regions.d.ts +132 -0
- package/dist/features/regions.d.ts.map +1 -0
- package/dist/features/regions.js +268 -0
- package/dist/features/regions.js.map +1 -0
- package/dist/gateway/features.d.ts +10 -0
- package/dist/gateway/features.d.ts.map +1 -1
- package/dist/gateway/features.js +6 -0
- package/dist/gateway/features.js.map +1 -1
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +12 -3
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/types.d.ts +4 -0
- package/dist/utils/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed edge nodes (5.3): the control plane knows its edge fleet, reports config drift and pushes new config.
|
|
3
|
+
*
|
|
4
|
+
* Edges (`@winstonsayno/mcp-gateway/edge` with `sync`) pull snapshots on their own schedule. With `edgeFleet`
|
|
5
|
+
* configured the Node gateway can also *push*: it calls each edge's `POST /api/v1/edge/sync`, which makes the
|
|
6
|
+
* edge pull the current snapshot, replay queued calls and upload usage right away.
|
|
7
|
+
*
|
|
8
|
+
* ```yaml
|
|
9
|
+
* edgeFleet:
|
|
10
|
+
* pushTimeoutMs: 10000
|
|
11
|
+
* nodes:
|
|
12
|
+
* - { id: cf-hkg, url: https://edge-hkg.example.workers.dev, apiKey: ${EDGE_KEY}, labels: { ring: canary } }
|
|
13
|
+
* - { id: deno-fra, url: https://fra.example.deno.dev, apiKey: ${EDGE_KEY}, labels: { ring: stable } }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `GET /admin/edge-fleet` — configured + seen edges with drift: `in-sync`, `stale` (applied an older snapshot),
|
|
17
|
+
* `never-synced`, `unmanaged` (syncs but is not in `nodes`), `offline` (not seen for `offlineAfterMs`).
|
|
18
|
+
* - `POST /admin/edge-fleet/push` — `{ nodes?: string[], labels?: Record<string,string>, onlyDrifted?: boolean }`
|
|
19
|
+
* → per-node result. Rolling out by ring: push `labels: { ring: canary }` first, then the rest.
|
|
20
|
+
*
|
|
21
|
+
* @module features/edge-fleet
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { registerFeature, objectBody, badRequest } from '../gateway/features.js';
|
|
25
|
+
import { buildEdgeSnapshot } from '../gateway/edge-control.js';
|
|
26
|
+
export const EdgeFleetSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
pushTimeoutMs: z.number().int().min(100).default(10_000),
|
|
29
|
+
offlineAfterMs: z.number().int().min(1000).default(15 * 60_000),
|
|
30
|
+
nodes: z
|
|
31
|
+
.array(z.object({ id: z.string().regex(/^[A-Za-z0-9_.:-]{1,128}$/), url: z.string().url(), apiKey: z.string().optional(), labels: z.record(z.string()).default({}) }).strict())
|
|
32
|
+
.default([]),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
/** Merge configured nodes with nodes seen by the control plane and classify drift. */
|
|
36
|
+
export function fleetView(cfg, seen, currentEtags, now = Date.now()) {
|
|
37
|
+
const byId = new Map(seen.map((n) => [n.edgeId, n]));
|
|
38
|
+
const out = [];
|
|
39
|
+
const classify = (n, managed) => {
|
|
40
|
+
if (!n || !n.snapshotEtag)
|
|
41
|
+
return n && !managed ? 'unmanaged' : 'never-synced';
|
|
42
|
+
if (now - Date.parse(n.lastSeen) > cfg.offlineAfterMs)
|
|
43
|
+
return 'offline';
|
|
44
|
+
if (!managed)
|
|
45
|
+
return 'unmanaged';
|
|
46
|
+
return currentEtags.includes(n.snapshotEtag) ? 'in-sync' : 'stale';
|
|
47
|
+
};
|
|
48
|
+
for (const c of cfg.nodes) {
|
|
49
|
+
const n = byId.get(c.id);
|
|
50
|
+
out.push({ id: c.id, url: c.url, labels: c.labels, managed: true, drift: classify(n, true), appliedEtag: n?.snapshotEtag, lastSeen: n?.lastSeen, lastSync: n?.lastSync, queuedCalls: n?.queuedCalls ?? 0, errors: n?.errors ?? 0 });
|
|
51
|
+
byId.delete(c.id);
|
|
52
|
+
}
|
|
53
|
+
for (const n of byId.values()) {
|
|
54
|
+
out.push({ id: n.edgeId, labels: {}, managed: false, drift: classify(n, false), appliedEtag: n.snapshotEtag, lastSeen: n.lastSeen, lastSync: n.lastSync, queuedCalls: n.queuedCalls, errors: n.errors });
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Which configured nodes a push targets. */
|
|
59
|
+
export function selectNodes(cfg, view, sel) {
|
|
60
|
+
return cfg.nodes.filter((n) => {
|
|
61
|
+
if (sel.nodes?.length && !sel.nodes.includes(n.id))
|
|
62
|
+
return false;
|
|
63
|
+
if (sel.labels && !Object.entries(sel.labels).every(([k, v]) => n.labels[k] === v))
|
|
64
|
+
return false;
|
|
65
|
+
if (sel.onlyDrifted && view.find((v) => v.id === n.id)?.drift === 'in-sync')
|
|
66
|
+
return false;
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Ask each edge to sync now. */
|
|
71
|
+
export async function pushToNodes(nodes, timeoutMs, f = fetch) {
|
|
72
|
+
return Promise.all(nodes.map(async (n) => {
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
try {
|
|
75
|
+
const res = await f(`${n.url.replace(/\/$/, '')}/api/v1/edge/sync`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'content-type': 'application/json', ...(n.apiKey ? { authorization: `Bearer ${n.apiKey}` } : {}) },
|
|
78
|
+
body: '{}',
|
|
79
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
80
|
+
});
|
|
81
|
+
const body = (await res.json().catch(() => ({})));
|
|
82
|
+
return { id: n.id, ok: res.ok, status: res.status, ...(body.config ? { config: body.config } : {}), ...(!res.ok ? { error: body.message ?? `HTTP ${res.status}` } : {}), durationMs: Date.now() - started };
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return { id: n.id, ok: false, error: e.message, durationMs: Date.now() - started };
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
registerFeature({
|
|
90
|
+
id: 'edge-fleet',
|
|
91
|
+
since: '5.3.0',
|
|
92
|
+
summary: 'Managed edge nodes: fleet view with config drift, push config to edges',
|
|
93
|
+
mount: (router, ctx) => {
|
|
94
|
+
const resolve = () => EdgeFleetSchema.parse(ctx.config().edgeFleet ?? {});
|
|
95
|
+
const etags = () => [buildEdgeSnapshot(ctx.config(), ctx.tools(), false).etag, buildEdgeSnapshot(ctx.config(), ctx.tools(), true).etag];
|
|
96
|
+
router.get('/', (_req, res) => {
|
|
97
|
+
const cfg = resolve();
|
|
98
|
+
const [etag] = etags();
|
|
99
|
+
const nodes = fleetView(cfg, ctx.edgeNodes?.() ?? [], etags());
|
|
100
|
+
const counts = nodes.reduce((m, n) => ((m[n.drift] = (m[n.drift] ?? 0) + 1), m), {});
|
|
101
|
+
res.json({ etag, counts, nodes });
|
|
102
|
+
});
|
|
103
|
+
router.post('/push', async (req, res) => {
|
|
104
|
+
const b = objectBody(req, res);
|
|
105
|
+
if (!b)
|
|
106
|
+
return;
|
|
107
|
+
if (b.nodes !== undefined && !(Array.isArray(b.nodes) && b.nodes.every((x) => typeof x === 'string')))
|
|
108
|
+
return badRequest(res, '"nodes" must be an array of ids');
|
|
109
|
+
if (b.labels !== undefined && (typeof b.labels !== 'object' || b.labels === null || Array.isArray(b.labels)))
|
|
110
|
+
return badRequest(res, '"labels" must be an object');
|
|
111
|
+
const cfg = resolve();
|
|
112
|
+
if (!cfg.nodes.length)
|
|
113
|
+
return void res.status(404).json({ error: 'Not Found', message: 'no managed edge nodes (`edgeFleet.nodes`)' });
|
|
114
|
+
const view = fleetView(cfg, ctx.edgeNodes?.() ?? [], etags());
|
|
115
|
+
const targets = selectNodes(cfg, view, { nodes: b.nodes, labels: b.labels, onlyDrifted: b.onlyDrifted === true });
|
|
116
|
+
const results = await pushToNodes(targets, cfg.pushTimeoutMs);
|
|
117
|
+
res.json({ pushed: results.filter((r) => r.ok).length, failed: results.filter((r) => !r.ok).length, results });
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=edge-fleet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-fleet.js","sourceRoot":"","sources":["../../src/features/edge-fleet.ts"],"names":[],"mappings":"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"}
|
package/dist/features/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/features/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/features/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,cAAc,CAAC;AACtB,OAAO,iBAAiB,CAAC;AAEzB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/features/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/features/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/features/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,kBAAkB,CAAC;AAC1B,OAAO,cAAc,CAAC;AACtB,OAAO,iBAAiB,CAAC;AAEzB,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-region active-active (5.2).
|
|
3
|
+
*
|
|
4
|
+
* Every region runs a full gateway. Regions replicate a small shared key-value state (last-writer-wins per key,
|
|
5
|
+
* ties broken by region id) and gossip which upstream servers are online, so a call whose upstream is down locally
|
|
6
|
+
* can be failed over to a region that has it.
|
|
7
|
+
*
|
|
8
|
+
* ```yaml
|
|
9
|
+
* regions:
|
|
10
|
+
* self: eu-west
|
|
11
|
+
* syncIntervalMs: 5000
|
|
12
|
+
* peers:
|
|
13
|
+
* - { id: us-east, url: https://us.gw.example.com, apiKey: ${US_ADMIN_KEY}, priority: 1 }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `GET /admin/regions` — this region, peers (health, last sync, servers online), replicated key count.
|
|
17
|
+
* - `POST /admin/regions/sync` — peer exchange `{ region, since, entries, servers }` → `{ region, entries, servers }`.
|
|
18
|
+
* - `GET|PUT|DELETE /admin/regions/kv/:key` — replicated state (`PUT` body `{ value }`).
|
|
19
|
+
* - `GET /admin/regions/route/:serverId` — where a call to that server should run: `local`, a peer, or none.
|
|
20
|
+
*
|
|
21
|
+
* @module features/regions
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
export declare const RegionsSchema: z.ZodObject<{
|
|
25
|
+
self: z.ZodString;
|
|
26
|
+
syncIntervalMs: z.ZodDefault<z.ZodNumber>;
|
|
27
|
+
/** Consecutive failed syncs before a peer is considered down. */
|
|
28
|
+
downAfter: z.ZodDefault<z.ZodNumber>;
|
|
29
|
+
peers: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
url: z.ZodString;
|
|
32
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
33
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
}, "strict", z.ZodTypeAny, {
|
|
35
|
+
id: string;
|
|
36
|
+
url: string;
|
|
37
|
+
priority: number;
|
|
38
|
+
apiKey?: string | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
id: string;
|
|
41
|
+
url: string;
|
|
42
|
+
apiKey?: string | undefined;
|
|
43
|
+
priority?: number | undefined;
|
|
44
|
+
}>, "many">>;
|
|
45
|
+
}, "strict", z.ZodTypeAny, {
|
|
46
|
+
self: string;
|
|
47
|
+
syncIntervalMs: number;
|
|
48
|
+
downAfter: number;
|
|
49
|
+
peers: {
|
|
50
|
+
id: string;
|
|
51
|
+
url: string;
|
|
52
|
+
priority: number;
|
|
53
|
+
apiKey?: string | undefined;
|
|
54
|
+
}[];
|
|
55
|
+
}, {
|
|
56
|
+
self: string;
|
|
57
|
+
syncIntervalMs?: number | undefined;
|
|
58
|
+
downAfter?: number | undefined;
|
|
59
|
+
peers?: {
|
|
60
|
+
id: string;
|
|
61
|
+
url: string;
|
|
62
|
+
apiKey?: string | undefined;
|
|
63
|
+
priority?: number | undefined;
|
|
64
|
+
}[] | undefined;
|
|
65
|
+
}>;
|
|
66
|
+
export type RegionsConfig = z.input<typeof RegionsSchema>;
|
|
67
|
+
type Resolved = z.output<typeof RegionsSchema>;
|
|
68
|
+
export interface ReplicatedEntry {
|
|
69
|
+
key: string;
|
|
70
|
+
/** `null` = tombstone (deleted). */
|
|
71
|
+
value: unknown;
|
|
72
|
+
/** Hybrid timestamp: wall-clock ms, bumped to stay monotonic per region. */
|
|
73
|
+
ts: number;
|
|
74
|
+
region: string;
|
|
75
|
+
}
|
|
76
|
+
export interface PeerState {
|
|
77
|
+
id: string;
|
|
78
|
+
url: string;
|
|
79
|
+
priority: number;
|
|
80
|
+
status: 'unknown' | 'up' | 'down';
|
|
81
|
+
failures: number;
|
|
82
|
+
lastSync?: string;
|
|
83
|
+
lastError?: string;
|
|
84
|
+
/** Highest `ts` received from this peer (sync cursor). */
|
|
85
|
+
cursor: number;
|
|
86
|
+
servers: string[];
|
|
87
|
+
}
|
|
88
|
+
/** Last-writer-wins: newer ts wins; equal ts → lexicographically larger region wins. */
|
|
89
|
+
export declare const newer: (a: ReplicatedEntry, b: ReplicatedEntry | undefined) => boolean;
|
|
90
|
+
export declare class RegionMesh {
|
|
91
|
+
private cfg;
|
|
92
|
+
private readonly now;
|
|
93
|
+
private readonly entries;
|
|
94
|
+
private readonly peers;
|
|
95
|
+
private clock;
|
|
96
|
+
private timer?;
|
|
97
|
+
constructor(cfg: Resolved, now?: () => number);
|
|
98
|
+
get self(): string;
|
|
99
|
+
configure(cfg: Resolved): void;
|
|
100
|
+
private tick;
|
|
101
|
+
get(key: string): unknown;
|
|
102
|
+
put(key: string, value: unknown): ReplicatedEntry;
|
|
103
|
+
delete(key: string): boolean;
|
|
104
|
+
size(): number;
|
|
105
|
+
/** Merge remote entries; returns how many changed local state. */
|
|
106
|
+
merge(entries: ReplicatedEntry[]): number;
|
|
107
|
+
/** Entries changed after `since` (any origin, so state relays through intermediate regions). */
|
|
108
|
+
delta(since?: number): ReplicatedEntry[];
|
|
109
|
+
peerList(): PeerState[];
|
|
110
|
+
/** Record a peer's gossip (from an inbound or outbound sync). */
|
|
111
|
+
sawPeer(id: string, servers: unknown, cursor?: number): void;
|
|
112
|
+
peerFailed(id: string, err: string): void;
|
|
113
|
+
/** Where a call to `serverId` should run. */
|
|
114
|
+
route(serverId: string, localOnline: string[]): {
|
|
115
|
+
target: 'local';
|
|
116
|
+
} | {
|
|
117
|
+
target: 'peer';
|
|
118
|
+
peer: string;
|
|
119
|
+
url: string;
|
|
120
|
+
} | {
|
|
121
|
+
target: 'none';
|
|
122
|
+
};
|
|
123
|
+
/** One outbound sync round with every peer. */
|
|
124
|
+
syncOnce(localOnline: string[], f?: typeof fetch): Promise<void>;
|
|
125
|
+
start(localOnline: () => string[], f?: typeof fetch): void;
|
|
126
|
+
stop(): void;
|
|
127
|
+
status(): Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
/** Resolve (validate + defaults) a `regions` section. */
|
|
130
|
+
export declare const resolveRegions: (raw: unknown) => Resolved;
|
|
131
|
+
export {};
|
|
132
|
+
//# sourceMappingURL=regions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.d.ts","sourceRoot":"","sources":["../../src/features/regions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-region active-active (5.2).
|
|
3
|
+
*
|
|
4
|
+
* Every region runs a full gateway. Regions replicate a small shared key-value state (last-writer-wins per key,
|
|
5
|
+
* ties broken by region id) and gossip which upstream servers are online, so a call whose upstream is down locally
|
|
6
|
+
* can be failed over to a region that has it.
|
|
7
|
+
*
|
|
8
|
+
* ```yaml
|
|
9
|
+
* regions:
|
|
10
|
+
* self: eu-west
|
|
11
|
+
* syncIntervalMs: 5000
|
|
12
|
+
* peers:
|
|
13
|
+
* - { id: us-east, url: https://us.gw.example.com, apiKey: ${US_ADMIN_KEY}, priority: 1 }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* - `GET /admin/regions` — this region, peers (health, last sync, servers online), replicated key count.
|
|
17
|
+
* - `POST /admin/regions/sync` — peer exchange `{ region, since, entries, servers }` → `{ region, entries, servers }`.
|
|
18
|
+
* - `GET|PUT|DELETE /admin/regions/kv/:key` — replicated state (`PUT` body `{ value }`).
|
|
19
|
+
* - `GET /admin/regions/route/:serverId` — where a call to that server should run: `local`, a peer, or none.
|
|
20
|
+
*
|
|
21
|
+
* @module features/regions
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { registerFeature, badRequest, objectBody } from '../gateway/features.js';
|
|
25
|
+
export const RegionsSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
self: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, 'lowercase letters, digits and dashes'),
|
|
28
|
+
syncIntervalMs: z.number().int().min(250).default(5000),
|
|
29
|
+
/** Consecutive failed syncs before a peer is considered down. */
|
|
30
|
+
downAfter: z.number().int().min(1).default(3),
|
|
31
|
+
peers: z
|
|
32
|
+
.array(z.object({ id: z.string().min(1), url: z.string().url(), apiKey: z.string().optional(), priority: z.number().int().min(0).default(100) }).strict())
|
|
33
|
+
.default([]),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
/** Last-writer-wins: newer ts wins; equal ts → lexicographically larger region wins. */
|
|
37
|
+
export const newer = (a, b) => !b || a.ts > b.ts || (a.ts === b.ts && a.region > b.region);
|
|
38
|
+
export class RegionMesh {
|
|
39
|
+
cfg;
|
|
40
|
+
now;
|
|
41
|
+
entries = new Map();
|
|
42
|
+
peers = new Map();
|
|
43
|
+
clock = 0;
|
|
44
|
+
timer;
|
|
45
|
+
constructor(cfg, now = Date.now) {
|
|
46
|
+
this.cfg = cfg;
|
|
47
|
+
this.now = now;
|
|
48
|
+
this.configure(cfg);
|
|
49
|
+
}
|
|
50
|
+
get self() {
|
|
51
|
+
return this.cfg.self;
|
|
52
|
+
}
|
|
53
|
+
configure(cfg) {
|
|
54
|
+
this.cfg = cfg;
|
|
55
|
+
const keep = new Set(cfg.peers.map((p) => p.id));
|
|
56
|
+
for (const id of [...this.peers.keys()])
|
|
57
|
+
if (!keep.has(id))
|
|
58
|
+
this.peers.delete(id);
|
|
59
|
+
for (const p of cfg.peers) {
|
|
60
|
+
const cur = this.peers.get(p.id);
|
|
61
|
+
this.peers.set(p.id, cur ? { ...cur, url: p.url, priority: p.priority } : { id: p.id, url: p.url, priority: p.priority, status: 'unknown', failures: 0, cursor: 0, servers: [] });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
tick() {
|
|
65
|
+
this.clock = Math.max(this.clock + 1, this.now());
|
|
66
|
+
return this.clock;
|
|
67
|
+
}
|
|
68
|
+
get(key) {
|
|
69
|
+
const e = this.entries.get(key);
|
|
70
|
+
return e && e.value !== null ? e.value : undefined;
|
|
71
|
+
}
|
|
72
|
+
put(key, value) {
|
|
73
|
+
const e = { key, value: value === undefined ? null : value, ts: this.tick(), region: this.cfg.self };
|
|
74
|
+
this.entries.set(key, e);
|
|
75
|
+
return e;
|
|
76
|
+
}
|
|
77
|
+
delete(key) {
|
|
78
|
+
const had = this.get(key) !== undefined;
|
|
79
|
+
this.put(key, null);
|
|
80
|
+
return had;
|
|
81
|
+
}
|
|
82
|
+
size() {
|
|
83
|
+
return [...this.entries.values()].filter((e) => e.value !== null).length;
|
|
84
|
+
}
|
|
85
|
+
/** Merge remote entries; returns how many changed local state. */
|
|
86
|
+
merge(entries) {
|
|
87
|
+
let n = 0;
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
if (!e || typeof e.key !== 'string' || typeof e.ts !== 'number' || typeof e.region !== 'string')
|
|
90
|
+
continue;
|
|
91
|
+
if (newer(e, this.entries.get(e.key))) {
|
|
92
|
+
this.entries.set(e.key, { key: e.key, value: e.value ?? null, ts: e.ts, region: e.region });
|
|
93
|
+
n++;
|
|
94
|
+
}
|
|
95
|
+
this.clock = Math.max(this.clock, e.ts);
|
|
96
|
+
}
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
/** Entries changed after `since` (any origin, so state relays through intermediate regions). */
|
|
100
|
+
delta(since = 0) {
|
|
101
|
+
return [...this.entries.values()].filter((e) => e.ts > since).sort((a, b) => a.ts - b.ts);
|
|
102
|
+
}
|
|
103
|
+
peerList() {
|
|
104
|
+
return [...this.peers.values()].sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
|
|
105
|
+
}
|
|
106
|
+
/** Record a peer's gossip (from an inbound or outbound sync). */
|
|
107
|
+
sawPeer(id, servers, cursor) {
|
|
108
|
+
const p = this.peers.get(id);
|
|
109
|
+
if (!p)
|
|
110
|
+
return;
|
|
111
|
+
p.status = 'up';
|
|
112
|
+
p.failures = 0;
|
|
113
|
+
p.lastSync = new Date(this.now()).toISOString();
|
|
114
|
+
delete p.lastError;
|
|
115
|
+
if (Array.isArray(servers))
|
|
116
|
+
p.servers = servers.map(String);
|
|
117
|
+
if (typeof cursor === 'number')
|
|
118
|
+
p.cursor = Math.max(p.cursor, cursor);
|
|
119
|
+
}
|
|
120
|
+
peerFailed(id, err) {
|
|
121
|
+
const p = this.peers.get(id);
|
|
122
|
+
if (!p)
|
|
123
|
+
return;
|
|
124
|
+
p.failures++;
|
|
125
|
+
p.lastError = err;
|
|
126
|
+
if (p.failures >= this.cfg.downAfter)
|
|
127
|
+
p.status = 'down';
|
|
128
|
+
}
|
|
129
|
+
/** Where a call to `serverId` should run. */
|
|
130
|
+
route(serverId, localOnline) {
|
|
131
|
+
if (localOnline.includes(serverId))
|
|
132
|
+
return { target: 'local' };
|
|
133
|
+
const peer = this.peerList().find((p) => p.status === 'up' && p.servers.includes(serverId));
|
|
134
|
+
return peer ? { target: 'peer', peer: peer.id, url: peer.url } : { target: 'none' };
|
|
135
|
+
}
|
|
136
|
+
/** One outbound sync round with every peer. */
|
|
137
|
+
async syncOnce(localOnline, f = fetch) {
|
|
138
|
+
await Promise.all(this.peerList().map(async (p) => {
|
|
139
|
+
const conf = this.cfg.peers.find((x) => x.id === p.id);
|
|
140
|
+
try {
|
|
141
|
+
const res = await f(`${p.url.replace(/\/$/, '')}/api/v1/admin/regions/sync`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'content-type': 'application/json', ...(conf?.apiKey ? { authorization: `Bearer ${conf.apiKey}` } : {}) },
|
|
144
|
+
body: JSON.stringify({ region: this.cfg.self, since: p.cursor, entries: this.delta(0), servers: localOnline }),
|
|
145
|
+
signal: AbortSignal.timeout(Math.max(1000, this.cfg.syncIntervalMs)),
|
|
146
|
+
});
|
|
147
|
+
if (!res.ok)
|
|
148
|
+
throw new Error(`HTTP ${res.status}`);
|
|
149
|
+
const body = (await res.json());
|
|
150
|
+
const entries = Array.isArray(body.entries) ? body.entries : [];
|
|
151
|
+
this.merge(entries);
|
|
152
|
+
this.sawPeer(p.id, body.servers, entries.reduce((m, e) => Math.max(m, e.ts ?? 0), 0));
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
this.peerFailed(p.id, e.message);
|
|
156
|
+
}
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
start(localOnline, f = fetch) {
|
|
160
|
+
this.stop();
|
|
161
|
+
if (!this.cfg.peers.length)
|
|
162
|
+
return;
|
|
163
|
+
this.timer = setInterval(() => void this.syncOnce(localOnline(), f), this.cfg.syncIntervalMs);
|
|
164
|
+
this.timer.unref?.();
|
|
165
|
+
}
|
|
166
|
+
stop() {
|
|
167
|
+
if (this.timer)
|
|
168
|
+
clearInterval(this.timer);
|
|
169
|
+
this.timer = undefined;
|
|
170
|
+
}
|
|
171
|
+
status() {
|
|
172
|
+
return { self: this.cfg.self, syncIntervalMs: this.cfg.syncIntervalMs, keys: this.size(), peers: this.peerList() };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Resolve (validate + defaults) a `regions` section. */
|
|
176
|
+
export const resolveRegions = (raw) => RegionsSchema.parse(raw);
|
|
177
|
+
function meshFor(ctx, state) {
|
|
178
|
+
const raw = ctx.config().regions;
|
|
179
|
+
if (!raw) {
|
|
180
|
+
state.mesh?.stop();
|
|
181
|
+
state.mesh = undefined;
|
|
182
|
+
state.key = undefined;
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
const key = JSON.stringify(raw);
|
|
186
|
+
if (state.key !== key) {
|
|
187
|
+
const cfg = resolveRegions(raw);
|
|
188
|
+
if (state.mesh && state.mesh.self === cfg.self)
|
|
189
|
+
state.mesh.configure(cfg);
|
|
190
|
+
else
|
|
191
|
+
state.mesh = new RegionMesh(cfg);
|
|
192
|
+
state.key = key;
|
|
193
|
+
state.mesh.start(() => ctx.onlineServers?.() ?? []);
|
|
194
|
+
}
|
|
195
|
+
return state.mesh;
|
|
196
|
+
}
|
|
197
|
+
registerFeature({
|
|
198
|
+
id: 'regions',
|
|
199
|
+
since: '5.2.0',
|
|
200
|
+
summary: 'Multi-region active-active: replicated state, peer health, cross-region failover routing',
|
|
201
|
+
mount: (router, ctx) => {
|
|
202
|
+
const state = {};
|
|
203
|
+
ctx.onStop?.(() => state.mesh?.stop());
|
|
204
|
+
// Start syncing as soon as the gateway is up (config may also change later; re-checked on each request).
|
|
205
|
+
setImmediate(() => { try {
|
|
206
|
+
meshFor(ctx, state);
|
|
207
|
+
}
|
|
208
|
+
catch { /* invalid config is reported on request */ } });
|
|
209
|
+
const need = (res) => {
|
|
210
|
+
const m = meshFor(ctx, state);
|
|
211
|
+
if (!m)
|
|
212
|
+
res.status(404).json({ error: 'Not Found', message: 'multi-region is not configured (`regions:`)' });
|
|
213
|
+
return m;
|
|
214
|
+
};
|
|
215
|
+
router.get('/', (_req, res) => {
|
|
216
|
+
const m = need(res);
|
|
217
|
+
if (m)
|
|
218
|
+
res.json(m.status());
|
|
219
|
+
});
|
|
220
|
+
router.post('/sync', (req, res) => {
|
|
221
|
+
const m = need(res);
|
|
222
|
+
if (!m)
|
|
223
|
+
return;
|
|
224
|
+
const b = objectBody(req, res);
|
|
225
|
+
if (!b)
|
|
226
|
+
return;
|
|
227
|
+
if (typeof b.region !== 'string')
|
|
228
|
+
return badRequest(res, '"region" is required');
|
|
229
|
+
if (!m.peerList().some((p) => p.id === b.region))
|
|
230
|
+
return void res.status(403).json({ error: 'Forbidden', message: `unknown region "${b.region}"` });
|
|
231
|
+
const entries = Array.isArray(b.entries) ? b.entries : [];
|
|
232
|
+
m.merge(entries);
|
|
233
|
+
m.sawPeer(b.region, b.servers, entries.reduce((x, e) => Math.max(x, e?.ts ?? 0), 0));
|
|
234
|
+
res.json({ region: m.self, entries: m.delta(typeof b.since === 'number' ? b.since : 0), servers: ctx.onlineServers?.() ?? [] });
|
|
235
|
+
});
|
|
236
|
+
router.get('/kv/:key', (req, res) => {
|
|
237
|
+
const m = need(res);
|
|
238
|
+
if (!m)
|
|
239
|
+
return;
|
|
240
|
+
const v = m.get(req.params.key);
|
|
241
|
+
if (v === undefined)
|
|
242
|
+
return void res.status(404).json({ error: 'Not Found', message: 'no such key' });
|
|
243
|
+
res.json({ key: req.params.key, value: v });
|
|
244
|
+
});
|
|
245
|
+
router.put('/kv/:key', (req, res) => {
|
|
246
|
+
const m = need(res);
|
|
247
|
+
if (!m)
|
|
248
|
+
return;
|
|
249
|
+
const b = objectBody(req, res);
|
|
250
|
+
if (!b)
|
|
251
|
+
return;
|
|
252
|
+
if (!('value' in b) || b.value === null)
|
|
253
|
+
return badRequest(res, '"value" is required (use DELETE to remove)');
|
|
254
|
+
res.json(m.put(req.params.key, b.value));
|
|
255
|
+
});
|
|
256
|
+
router.delete('/kv/:key', (req, res) => {
|
|
257
|
+
const m = need(res);
|
|
258
|
+
if (m)
|
|
259
|
+
res.json({ deleted: m.delete(req.params.key) });
|
|
260
|
+
});
|
|
261
|
+
router.get('/route/:serverId', (req, res) => {
|
|
262
|
+
const m = need(res);
|
|
263
|
+
if (m)
|
|
264
|
+
res.json({ serverId: req.params.serverId, ...m.route(req.params.serverId, ctx.onlineServers?.() ?? []) });
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
//# sourceMappingURL=regions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regions.js","sourceRoot":"","sources":["../../src/features/regions.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,12 @@ export interface FeatureContext {
|
|
|
17
17
|
recent: (limit?: number) => RequestMetric[];
|
|
18
18
|
/** Base URL of this gateway when listening (for self-tests). */
|
|
19
19
|
baseUrl: () => string | undefined;
|
|
20
|
+
/** Upstream server ids currently online on this gateway (5.2). */
|
|
21
|
+
onlineServers?: () => string[];
|
|
22
|
+
/** Register a cleanup run when the gateway stops (5.2): timers, sockets. */
|
|
23
|
+
onStop?: (fn: () => void | Promise<void>) => void;
|
|
24
|
+
/** Edge gateways seen by the edge control plane (5.3). */
|
|
25
|
+
edgeNodes?: () => import('./edge-control.js').EdgeNode[];
|
|
20
26
|
}
|
|
21
27
|
export interface FeatureModule {
|
|
22
28
|
/** Mount point: `/api/v1/admin/<id>`. */
|
|
@@ -26,6 +32,10 @@ export interface FeatureModule {
|
|
|
26
32
|
summary: string;
|
|
27
33
|
mount: (router: Router, ctx: FeatureContext) => void;
|
|
28
34
|
}
|
|
35
|
+
/** Top-level config sections owned by feature modules; all hot reload (5.2+). */
|
|
36
|
+
export declare const FEATURE_CONFIG_KEYS: readonly ["regions", "edgeFleet"];
|
|
37
|
+
/** Copy the feature-owned config sections of `next` (for hot reload). */
|
|
38
|
+
export declare function featureSections(next: GatewayConfig): Partial<GatewayConfig>;
|
|
29
39
|
/** Register a feature module (idempotent by id; later registrations replace earlier ones). */
|
|
30
40
|
export declare function registerFeature(f: FeatureModule): void;
|
|
31
41
|
export declare function listFeatures(): ReadonlyArray<Pick<FeatureModule, 'id' | 'since' | 'summary'>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/gateway/features.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,OAAO,EAAE,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG/F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,aAAa,CAAC;IAC5B,KAAK,EAAE,MAAM,QAAQ,EAAE,CAAC;IACxB,mFAAmF;IACnF,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACrH,iDAAiD;IACjD,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;IAC5C,gEAAgE;IAChE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/gateway/features.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,OAAO,EAAE,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG/F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,aAAa,CAAC;IAC5B,KAAK,EAAE,MAAM,QAAQ,EAAE,CAAC;IACxB,mFAAmF;IACnF,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACrH,iDAAiD;IACjD,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;IAC5C,gEAAgE;IAChE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IAClD,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,OAAO,mBAAmB,EAAE,QAAQ,EAAE,CAAC;CAC1D;AAED,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CACtD;AAID,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,mCAAiF,CAAC;AAElH,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAE3E;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAItD;AAED,wBAAgB,YAAY,IAAI,aAAa,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,CAE7F;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,cAAc,CAAC;IAC7B,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAc3E;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAOnG;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,CAAC,QAAQ,EAAE,SAAS,MAAM,cAAiE,CAAC"}
|
package/dist/gateway/features.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import { VERSION } from '../utils/version.js';
|
|
11
11
|
const registry = [];
|
|
12
|
+
/** Top-level config sections owned by feature modules; all hot reload (5.2+). */
|
|
13
|
+
export const FEATURE_CONFIG_KEYS = ['regions', 'edgeFleet'];
|
|
14
|
+
/** Copy the feature-owned config sections of `next` (for hot reload). */
|
|
15
|
+
export function featureSections(next) {
|
|
16
|
+
return Object.fromEntries(FEATURE_CONFIG_KEYS.map((k) => [k, next[k]]));
|
|
17
|
+
}
|
|
12
18
|
/** Register a feature module (idempotent by id; later registrations replace earlier ones). */
|
|
13
19
|
export function registerFeature(f) {
|
|
14
20
|
const i = registry.findIndex((x) => x.id === f.id);
|