@feltdb/core 0.7.2 → 0.7.3
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/dist/cli/commands.js +99 -5
- package/dist/cli/index.js +1 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +769 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +221 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
- package/dist/studio/app.d.ts +3 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +9 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-C5p2TfIU.js → components-Dxuhrv8_.js} +196 -187
- package/dist/studio/index.js +66 -65
- package/dist/studio-app/assets/index-B5tnmvSD.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/index-C71X92EK.js +0 -28
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
//! shutdown exit cleanly
|
|
46
46
|
//! ```
|
|
47
47
|
|
|
48
|
+
use feltdb::authority_failover::{AuthorityStore, ClaimResult, FencingToken, Heartbeat};
|
|
48
49
|
use feltdb::convergence::VectorClock;
|
|
49
50
|
use feltdb::distributed_transactions::{
|
|
50
51
|
CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
|
|
@@ -176,6 +177,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
176
177
|
let mut recv_specs: Vec<(String, String)> = Vec::new();
|
|
177
178
|
let mut max_pending_entries: Option<usize> = None;
|
|
178
179
|
let mut max_pending_bytes: Option<usize> = None;
|
|
180
|
+
let mut authority_priority: u32 = 0;
|
|
179
181
|
|
|
180
182
|
let mut index = 1;
|
|
181
183
|
while index < args.len() {
|
|
@@ -225,6 +227,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
225
227
|
.map_err(|e| format!("--max-pending-bytes: {e}"))?,
|
|
226
228
|
);
|
|
227
229
|
}
|
|
230
|
+
// Static priority for authority election. Higher priority nodes are
|
|
231
|
+
// preferred during failover when multiple candidates have equal state.
|
|
232
|
+
"--authority-priority" => {
|
|
233
|
+
index += 1;
|
|
234
|
+
authority_priority = args
|
|
235
|
+
.get(index)
|
|
236
|
+
.ok_or("--authority-priority requires a value")?
|
|
237
|
+
.parse::<u32>()
|
|
238
|
+
.map_err(|e| format!("--authority-priority: {e}"))?;
|
|
239
|
+
}
|
|
228
240
|
other => return Err(format!("unknown argument {other}").into()),
|
|
229
241
|
}
|
|
230
242
|
index += 1;
|
|
@@ -265,6 +277,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
265
277
|
.map_err(|e| e.to_string())?,
|
|
266
278
|
));
|
|
267
279
|
|
|
280
|
+
// Authority state tracks who holds write authority in the cluster. The store
|
|
281
|
+
// is durable and survives restarts, so a node that crashes can rejoin and
|
|
282
|
+
// recover its previous role. The cluster ID is set by the first `authority-claim`
|
|
283
|
+
// command, so it starts as the node's own ID.
|
|
284
|
+
let authority = Arc::new(Mutex::new(
|
|
285
|
+
AuthorityStore::open(
|
|
286
|
+
data_dir
|
|
287
|
+
.as_ref()
|
|
288
|
+
.map(|dir| dir.join("authority.json"))
|
|
289
|
+
.unwrap_or_else(|| PathBuf::from("authority.json")),
|
|
290
|
+
&node_id, // cluster_id defaults to node_id until set
|
|
291
|
+
&node_id,
|
|
292
|
+
authority_priority,
|
|
293
|
+
)
|
|
294
|
+
.map_err(|e| e.to_string())?,
|
|
295
|
+
));
|
|
296
|
+
|
|
268
297
|
let initial_state = StateHash::from_hex("0".repeat(64));
|
|
269
298
|
let mut executor =
|
|
270
299
|
DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
|
|
@@ -1147,6 +1176,198 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
1147
1176
|
io::stdout().flush().ok();
|
|
1148
1177
|
}
|
|
1149
1178
|
|
|
1179
|
+
// Authority failover commands.
|
|
1180
|
+
//
|
|
1181
|
+
// These commands implement the distributed authority protocol that prevents
|
|
1182
|
+
// split-brain and allows automatic failover when the authority node fails.
|
|
1183
|
+
|
|
1184
|
+
// Claim authority for this node. This generates a new fencing token that
|
|
1185
|
+
// supersedes any previous authority. The claim succeeds only if no other
|
|
1186
|
+
// node has a higher token and appears healthy.
|
|
1187
|
+
"authority-claim" => {
|
|
1188
|
+
let mut guard = authority.lock().await;
|
|
1189
|
+
let result = guard.claim_authority();
|
|
1190
|
+
match result {
|
|
1191
|
+
Ok(ClaimResult::Granted { token }) => {
|
|
1192
|
+
println!(
|
|
1193
|
+
"AUTHORITY_CLAIMED {}",
|
|
1194
|
+
json!({
|
|
1195
|
+
"node": node_id,
|
|
1196
|
+
"fencing_token": token.0,
|
|
1197
|
+
"role": "AUTHORITY",
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
Ok(ClaimResult::Rejected { current_authority, current_token }) => {
|
|
1202
|
+
println!(
|
|
1203
|
+
"AUTHORITY_REJECTED {}",
|
|
1204
|
+
json!({
|
|
1205
|
+
"current_authority": current_authority,
|
|
1206
|
+
"current_token": current_token.0,
|
|
1207
|
+
"reason": "another authority is still active",
|
|
1208
|
+
})
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
Ok(ClaimResult::Fenced { token }) => {
|
|
1212
|
+
println!(
|
|
1213
|
+
"AUTHORITY_FENCED {}",
|
|
1214
|
+
json!({
|
|
1215
|
+
"node": node_id,
|
|
1216
|
+
"fencing_token": token.0,
|
|
1217
|
+
"reason": "this node is fenced",
|
|
1218
|
+
})
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
Err(e) => {
|
|
1222
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
io::stdout().flush().ok();
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Accept a new authority announced by another node.
|
|
1229
|
+
// Format: authority-accept NODE_ID TOKEN
|
|
1230
|
+
"authority-accept" => {
|
|
1231
|
+
let mut parts = argument.split_whitespace();
|
|
1232
|
+
let authority_node = parts.next().unwrap_or_default().to_string();
|
|
1233
|
+
let token_value: u64 = parts
|
|
1234
|
+
.next()
|
|
1235
|
+
.unwrap_or("0")
|
|
1236
|
+
.parse()
|
|
1237
|
+
.unwrap_or(0);
|
|
1238
|
+
let token = FencingToken(token_value);
|
|
1239
|
+
|
|
1240
|
+
let mut guard = authority.lock().await;
|
|
1241
|
+
let result = guard.accept_authority(&authority_node, token);
|
|
1242
|
+
match result {
|
|
1243
|
+
Ok(()) => {
|
|
1244
|
+
let state = guard.state();
|
|
1245
|
+
println!(
|
|
1246
|
+
"AUTHORITY_ACCEPTED {}",
|
|
1247
|
+
json!({
|
|
1248
|
+
"authority_node": authority_node,
|
|
1249
|
+
"fencing_token": token.0,
|
|
1250
|
+
"local_role": format!("{:?}", state.local_role),
|
|
1251
|
+
})
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
Err(e) => {
|
|
1255
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
io::stdout().flush().ok();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Query the current authority state.
|
|
1262
|
+
"authority-state" => {
|
|
1263
|
+
let guard = authority.lock().await;
|
|
1264
|
+
let state = guard.state();
|
|
1265
|
+
let operations_applied = {
|
|
1266
|
+
let core_guard = core.lock().await;
|
|
1267
|
+
let (executor, _) = &*core_guard;
|
|
1268
|
+
executor.get_replica_state(&node_id).map(|r| r.operations_applied).unwrap_or(0)
|
|
1269
|
+
};
|
|
1270
|
+
println!(
|
|
1271
|
+
"AUTHORITY {}",
|
|
1272
|
+
json!({
|
|
1273
|
+
"cluster_id": state.cluster_id,
|
|
1274
|
+
"local_node": state.local_node_id,
|
|
1275
|
+
"local_role": format!("{:?}", state.local_role),
|
|
1276
|
+
"is_authority": state.is_authority(),
|
|
1277
|
+
"is_fenced": state.is_fenced(),
|
|
1278
|
+
"authority_node": state.authority_node(),
|
|
1279
|
+
"current_token": state.current_token().0,
|
|
1280
|
+
"highest_seen_token": state.highest_seen_token.0,
|
|
1281
|
+
"operations_applied": operations_applied,
|
|
1282
|
+
})
|
|
1283
|
+
);
|
|
1284
|
+
io::stdout().flush().ok();
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Record a heartbeat from a peer. This is used for failure detection.
|
|
1288
|
+
// Format: heartbeat NODE_ID [TOKEN] [OPS_APPLIED] [IS_AUTHORITY]
|
|
1289
|
+
"heartbeat" => {
|
|
1290
|
+
let mut parts = argument.split_whitespace();
|
|
1291
|
+
let from_node = parts.next().unwrap_or_default().to_string();
|
|
1292
|
+
|
|
1293
|
+
if from_node.is_empty() {
|
|
1294
|
+
println!("ERROR {}", json!({ "reason": "heartbeat requires NODE_ID" }));
|
|
1295
|
+
} else {
|
|
1296
|
+
let mut guard = authority.lock().await;
|
|
1297
|
+
guard.heartbeat(&from_node);
|
|
1298
|
+
println!(
|
|
1299
|
+
"HEARTBEAT_RECORDED {}",
|
|
1300
|
+
json!({
|
|
1301
|
+
"from_node": from_node,
|
|
1302
|
+
})
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
io::stdout().flush().ok();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Check if the current authority has failed based on heartbeat timeout.
|
|
1309
|
+
"authority-failed" => {
|
|
1310
|
+
let guard = authority.lock().await;
|
|
1311
|
+
let failed = guard.authority_failed();
|
|
1312
|
+
let failed_nodes = guard.failed_nodes();
|
|
1313
|
+
let state = guard.state();
|
|
1314
|
+
println!(
|
|
1315
|
+
"AUTHORITY_CHECK {}",
|
|
1316
|
+
json!({
|
|
1317
|
+
"authority_failed": failed,
|
|
1318
|
+
"current_authority": state.authority_node(),
|
|
1319
|
+
"failed_nodes": failed_nodes,
|
|
1320
|
+
})
|
|
1321
|
+
);
|
|
1322
|
+
io::stdout().flush().ok();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Step down from authority role voluntarily.
|
|
1326
|
+
"authority-stepdown" => {
|
|
1327
|
+
let mut guard = authority.lock().await;
|
|
1328
|
+
let was_authority = guard.state().is_authority();
|
|
1329
|
+
let result = guard.step_down();
|
|
1330
|
+
match result {
|
|
1331
|
+
Ok(()) => {
|
|
1332
|
+
println!(
|
|
1333
|
+
"AUTHORITY_STEPDOWN {}",
|
|
1334
|
+
json!({
|
|
1335
|
+
"node": node_id,
|
|
1336
|
+
"was_authority": was_authority,
|
|
1337
|
+
"now_role": format!("{:?}", guard.state().local_role),
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
Err(e) => {
|
|
1342
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
io::stdout().flush().ok();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Recover from fenced state after catching up.
|
|
1349
|
+
"authority-recover" => {
|
|
1350
|
+
let mut guard = authority.lock().await;
|
|
1351
|
+
let was_fenced = guard.state().is_fenced();
|
|
1352
|
+
let result = guard.recover_from_fenced();
|
|
1353
|
+
match result {
|
|
1354
|
+
Ok(()) => {
|
|
1355
|
+
println!(
|
|
1356
|
+
"AUTHORITY_RECOVERED {}",
|
|
1357
|
+
json!({
|
|
1358
|
+
"node": node_id,
|
|
1359
|
+
"was_fenced": was_fenced,
|
|
1360
|
+
"now_role": format!("{:?}", guard.state().local_role),
|
|
1361
|
+
})
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
Err(e) => {
|
|
1365
|
+
println!("ERROR {}", json!({ "reason": e.to_string() }));
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
io::stdout().flush().ok();
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1150
1371
|
"shutdown" => {
|
|
1151
1372
|
should_exit.store(true, Ordering::Relaxed);
|
|
1152
1373
|
println!("SHUTDOWN");
|
package/dist/studio/app.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
+
import { ManagedPublishConfiguration } from './components/ApplicationDesigner';
|
|
2
3
|
import { StateFirstDB } from '@feltdb/core';
|
|
3
4
|
export interface StudioAppProps {
|
|
4
5
|
db?: StateFirstDB;
|
|
@@ -8,8 +9,9 @@ export interface StudioAppProps {
|
|
|
8
9
|
deploymentRuntime?: string;
|
|
9
10
|
applicationUrl?: string;
|
|
10
11
|
developmentSession?: Record<string, string>;
|
|
12
|
+
managedPublish?: ManagedPublishConfiguration;
|
|
11
13
|
onConnect?: (url: string, token: string) => void;
|
|
12
14
|
}
|
|
13
|
-
export declare function StudioApp({ db, remoteUrl, token, namespace, deploymentRuntime, applicationUrl, developmentSession, onConnect }: StudioAppProps): React.JSX.Element;
|
|
15
|
+
export declare function StudioApp({ db, remoteUrl, token, namespace, deploymentRuntime, applicationUrl, developmentSession, managedPublish, onConnect }: StudioAppProps): React.JSX.Element;
|
|
14
16
|
export default StudioApp;
|
|
15
17
|
//# sourceMappingURL=app.d.ts.map
|
package/dist/studio/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AAgBnD,OAA4B,EAAE,KAAK,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAGzG,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,WAAW,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,YAAY,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,2BAA2B,CAAC;IAC7C,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,EAAE,SAAc,EAAE,KAAU,EAAE,SAAqB,EAAE,iBAA6B,EAAE,cAAmB,EAAE,kBAAkB,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,cAAc,qBA6IrM;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { StateFirstDB, FlowSpec } from '@feltdb/core';
|
|
3
|
+
export interface ManagedPublishConfiguration {
|
|
4
|
+
url: string;
|
|
5
|
+
namespace: string;
|
|
6
|
+
token: string;
|
|
7
|
+
environment?: string;
|
|
8
|
+
applicationId?: string;
|
|
9
|
+
}
|
|
3
10
|
export interface ApplicationDesignerProps {
|
|
4
11
|
db?: StateFirstDB;
|
|
5
12
|
namespace?: string;
|
|
6
13
|
projectSpec?: FlowSpec | null;
|
|
14
|
+
managedPublish?: ManagedPublishConfiguration;
|
|
7
15
|
onSpecChange?: (spec: FlowSpec) => void;
|
|
8
16
|
}
|
|
9
|
-
export declare function ApplicationDesigner({ db, namespace, projectSpec, onSpecChange }: ApplicationDesignerProps): React.JSX.Element;
|
|
17
|
+
export declare function ApplicationDesigner({ db, namespace, projectSpec, managedPublish, onSpecChange }: ApplicationDesignerProps): React.JSX.Element;
|
|
10
18
|
export default ApplicationDesigner;
|
|
11
19
|
//# sourceMappingURL=ApplicationDesigner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApplicationDesigner.d.ts","sourceRoot":"","sources":["../../src/components/ApplicationDesigner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAO3E,MAAM,WAAW,wBAAwB;IAAG,EAAE,CAAC,EAAE,YAAY,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE;
|
|
1
|
+
{"version":3,"file":"ApplicationDesigner.d.ts","sourceRoot":"","sources":["../../src/components/ApplicationDesigner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAO3E,MAAM,WAAW,2BAA2B;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE;AAC5I,MAAM,WAAW,wBAAwB;IAAG,EAAE,CAAC,EAAE,YAAY,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,2BAA2B,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE;AAEzM,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,EAAE,SAAqB,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,EAAE,wBAAwB,qBAoHrI;AACD,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as f, u as p, x as m } from "../components-
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as f, u as p, x as m } from "../components-Dxuhrv8_.js";
|
|
2
2
|
import { t as h } from "../KeyManagementPanel-DZuSeWBK.js";
|
|
3
3
|
import { ManagedInstancePanel as g } from "./ManagedInstancePanel.js";
|
|
4
4
|
export { c as AgentExplorer, f as ApplicationDesigner, p as CapabilityExplorer, t as ConflictExplorer, a as ExecutionViewer, s as GlobalSearch, i as HealthCenter, h as KeyManagementPanel, g as ManagedInstancePanel, r as OperationsExplorer, m as OverviewDashboard, n as PeerMap, e as ProvenanceViewer, l as ReferenceExplorer, u as SettingsPanel, o as StateExplorer, d as WorkflowVisualizer };
|