@feltdb/core 0.7.2 → 0.7.4
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/application-development.d.ts +76 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +137 -0
- package/dist/application-manifest.d.ts +5 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +26 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +45 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/commands.js +485 -30
- package/dist/cli/index.js +1 -1
- package/dist/cli/proposal-client.js +54 -0
- package/dist/cli/publish-engine.js +162 -0
- package/dist/cli/source-sync.js +139 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +30 -6
- package/dist/create/managed-account.js +14 -7
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +847 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +338 -29
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +169 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +27 -6
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +3 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +3 -0
- package/dist/proposal.d.ts +85 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +105 -0
- package/dist/studio/app.d.ts +5 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +10 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +20 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +3 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/index.d.ts +2 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/{components-C5p2TfIU.js → components-Bm4sjhcX.js} +531 -192
- package/dist/studio/index.js +184 -172
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
- package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +62 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-C71X92EK.js +0 -28
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
/// - DistributedExecutor: Orchestrates consistency semantics
|
|
8
8
|
///
|
|
9
9
|
/// Invariant: All replicas converge to identical state_hash for same operations
|
|
10
|
-
|
|
11
10
|
use crate::causal_dependency_barrier::CausalDependencyBarrier;
|
|
12
11
|
use crate::convergence::VectorClock;
|
|
13
12
|
use crate::durable_operation_log::OperationLog;
|
|
@@ -136,11 +135,7 @@ impl ReplicaState {
|
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
/// Update replica state after transaction
|
|
139
|
-
pub fn apply_transaction(
|
|
140
|
-
&mut self,
|
|
141
|
-
envelope: &TransactionEnvelope,
|
|
142
|
-
new_state_hash: StateHash,
|
|
143
|
-
) {
|
|
138
|
+
pub fn apply_transaction(&mut self, envelope: &TransactionEnvelope, new_state_hash: StateHash) {
|
|
144
139
|
self.vector_clock.merge(&envelope.vector_clock);
|
|
145
140
|
self.vector_clock.increment(&self.node_id);
|
|
146
141
|
self.state_hash = new_state_hash;
|
|
@@ -276,7 +271,10 @@ impl CausalCapacity {
|
|
|
276
271
|
|
|
277
272
|
/// A bound small enough to be reached by a test through the real path.
|
|
278
273
|
pub fn new(max_pending_entries: usize, max_pending_bytes: usize) -> Self {
|
|
279
|
-
Self {
|
|
274
|
+
Self {
|
|
275
|
+
max_pending_entries,
|
|
276
|
+
max_pending_bytes,
|
|
277
|
+
}
|
|
280
278
|
}
|
|
281
279
|
}
|
|
282
280
|
|
|
@@ -393,8 +391,12 @@ impl DistributedTransactionExecutor {
|
|
|
393
391
|
/// source `next_origin_sequence` is recovered from.
|
|
394
392
|
pub fn origin_sequence_high_water(&self) -> std::collections::BTreeMap<String, u64> {
|
|
395
393
|
let mut marks = std::collections::BTreeMap::new();
|
|
396
|
-
let Some(ref log) = self.operation_log else {
|
|
397
|
-
|
|
394
|
+
let Some(ref log) = self.operation_log else {
|
|
395
|
+
return marks;
|
|
396
|
+
};
|
|
397
|
+
let Ok(envelopes) = log.load_all() else {
|
|
398
|
+
return marks;
|
|
399
|
+
};
|
|
398
400
|
for envelope in envelopes {
|
|
399
401
|
let entry = marks
|
|
400
402
|
.entry(envelope.envelope_id.originating_node.clone())
|
|
@@ -516,7 +518,9 @@ impl DistributedTransactionExecutor {
|
|
|
516
518
|
|
|
517
519
|
/// The size local work would contribute, measured before it is executed.
|
|
518
520
|
fn operations_bytes(operations: &[Operation]) -> usize {
|
|
519
|
-
serde_json::to_string(operations)
|
|
521
|
+
serde_json::to_string(operations)
|
|
522
|
+
.map(|json| json.len())
|
|
523
|
+
.unwrap_or(0)
|
|
520
524
|
}
|
|
521
525
|
|
|
522
526
|
/// Execute local work unconditionally.
|
|
@@ -582,8 +586,10 @@ impl DistributedTransactionExecutor {
|
|
|
582
586
|
// Step 6: A locally executed operation is applied, so it advances the
|
|
583
587
|
// frontier exactly as a replicated one does. Own writes are always
|
|
584
588
|
// causally eligible: they are built on the frontier they advance.
|
|
585
|
-
self.causal_barrier
|
|
586
|
-
|
|
589
|
+
self.causal_barrier.record_applied(
|
|
590
|
+
&Self::barrier_key(&envelope.envelope_id),
|
|
591
|
+
&envelope.vector_clock,
|
|
592
|
+
);
|
|
587
593
|
|
|
588
594
|
Ok(envelope)
|
|
589
595
|
}
|
|
@@ -712,7 +718,9 @@ impl DistributedTransactionExecutor {
|
|
|
712
718
|
/// per-operation constant, so a large envelope consumes its actual byte
|
|
713
719
|
/// count.
|
|
714
720
|
fn envelope_bytes(envelope: &TransactionEnvelope) -> usize {
|
|
715
|
-
serde_json::to_string(envelope)
|
|
721
|
+
serde_json::to_string(envelope)
|
|
722
|
+
.map(|json| json.len())
|
|
723
|
+
.unwrap_or(0)
|
|
716
724
|
}
|
|
717
725
|
|
|
718
726
|
/// Apply one envelope and advance the applied frontier.
|
|
@@ -741,8 +749,10 @@ impl DistributedTransactionExecutor {
|
|
|
741
749
|
|
|
742
750
|
// The frontier advances here and only here, so it describes what has
|
|
743
751
|
// been successfully applied rather than what was claimed or received.
|
|
744
|
-
self.causal_barrier
|
|
745
|
-
|
|
752
|
+
self.causal_barrier.record_applied(
|
|
753
|
+
&Self::barrier_key(&envelope.envelope_id),
|
|
754
|
+
&envelope.vector_clock,
|
|
755
|
+
);
|
|
746
756
|
|
|
747
757
|
Ok(state_hash)
|
|
748
758
|
}
|
|
@@ -816,16 +826,14 @@ impl DistributedTransactionExecutor {
|
|
|
816
826
|
return Ok(0);
|
|
817
827
|
}
|
|
818
828
|
let by_key = self.log_by_key()?;
|
|
819
|
-
let mut keys: Vec<String> = self
|
|
820
|
-
.deferred
|
|
821
|
-
.iter()
|
|
822
|
-
.map(Self::barrier_key)
|
|
823
|
-
.collect();
|
|
829
|
+
let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
|
|
824
830
|
keys.sort();
|
|
825
831
|
|
|
826
832
|
let mut admitted = 0;
|
|
827
833
|
for key in keys {
|
|
828
|
-
let Some(envelope) = by_key.get(&key).cloned() else {
|
|
834
|
+
let Some(envelope) = by_key.get(&key).cloned() else {
|
|
835
|
+
continue;
|
|
836
|
+
};
|
|
829
837
|
let dependencies = Self::dependencies_of(&envelope);
|
|
830
838
|
let payload_bytes = Self::envelope_bytes(&envelope);
|
|
831
839
|
|
|
@@ -888,9 +896,7 @@ impl DistributedTransactionExecutor {
|
|
|
888
896
|
.clocks
|
|
889
897
|
.iter()
|
|
890
898
|
.filter(|(node, required)| frontier.get(node) < **required)
|
|
891
|
-
.map(|(node, required)| {
|
|
892
|
-
format!("{}@{} (have {})", node, required, frontier.get(node))
|
|
893
|
-
})
|
|
899
|
+
.map(|(node, required)| format!("{}@{} (have {})", node, required, frontier.get(node)))
|
|
894
900
|
.collect();
|
|
895
901
|
missing.sort();
|
|
896
902
|
missing
|
|
@@ -1021,11 +1027,7 @@ impl DistributedTransactionExecutor {
|
|
|
1021
1027
|
return true;
|
|
1022
1028
|
}
|
|
1023
1029
|
|
|
1024
|
-
let first_hash = self
|
|
1025
|
-
.replicas
|
|
1026
|
-
.values()
|
|
1027
|
-
.next()
|
|
1028
|
-
.map(|r| r.state_hash.clone());
|
|
1030
|
+
let first_hash = self.replicas.values().next().map(|r| r.state_hash.clone());
|
|
1029
1031
|
|
|
1030
1032
|
if let Some(first) = first_hash {
|
|
1031
1033
|
self.replicas.values().all(|r| r.state_hash == first)
|
|
@@ -1122,6 +1124,27 @@ impl DistributedTransactionExecutor {
|
|
|
1122
1124
|
);
|
|
1123
1125
|
}
|
|
1124
1126
|
|
|
1127
|
+
// Restore the observable local replica state from the same applied
|
|
1128
|
+
// subset. Recovery previously rebuilt the barrier and canonical data
|
|
1129
|
+
// but left this counter and clock at their constructor values, making
|
|
1130
|
+
// a fully recovered node report that it had applied zero operations.
|
|
1131
|
+
let applied_keys = self.causal_barrier.applied_keys();
|
|
1132
|
+
let operations_applied = envelopes
|
|
1133
|
+
.iter()
|
|
1134
|
+
.filter(|envelope| applied_keys.contains(&Self::barrier_key(&envelope.envelope_id)))
|
|
1135
|
+
.map(|envelope| envelope.operations.len() as u64)
|
|
1136
|
+
.sum();
|
|
1137
|
+
let recovered_clock = self.causal_barrier.get_frontier().clone();
|
|
1138
|
+
let recovered_hash = self
|
|
1139
|
+
.canonical_state()
|
|
1140
|
+
.map(|state| state.hash())
|
|
1141
|
+
.unwrap_or_else(|_| StateHash::from_hex("0".repeat(64)));
|
|
1142
|
+
if let Some(local) = self.replicas.get_mut(&self.local_node_id) {
|
|
1143
|
+
local.vector_clock = recovered_clock;
|
|
1144
|
+
local.operations_applied = operations_applied;
|
|
1145
|
+
local.state_hash = recovered_hash;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1125
1148
|
// Identity is recovered from the same log, and this is the half that
|
|
1126
1149
|
// was missing: the causal frontier was already rebuilt here, so a
|
|
1127
1150
|
// restarted origin came back knowing its causal position and not its
|
|
@@ -1201,7 +1224,10 @@ mod tests {
|
|
|
1201
1224
|
|
|
1202
1225
|
#[test]
|
|
1203
1226
|
fn test_replica_state_creation() {
|
|
1204
|
-
let state = ReplicaState::new(
|
|
1227
|
+
let state = ReplicaState::new(
|
|
1228
|
+
"node1".to_string(),
|
|
1229
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1230
|
+
);
|
|
1205
1231
|
|
|
1206
1232
|
assert_eq!(state.node_id, "node1");
|
|
1207
1233
|
assert_eq!(state.operations_applied, 0);
|
|
@@ -1224,7 +1250,10 @@ mod tests {
|
|
|
1224
1250
|
StateHash::from_hex("hash0".to_string()),
|
|
1225
1251
|
);
|
|
1226
1252
|
|
|
1227
|
-
executor.register_replica(
|
|
1253
|
+
executor.register_replica(
|
|
1254
|
+
"node2".to_string(),
|
|
1255
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1256
|
+
);
|
|
1228
1257
|
|
|
1229
1258
|
assert!(executor.replicas.contains_key("node2"));
|
|
1230
1259
|
}
|
|
@@ -1247,8 +1276,14 @@ mod tests {
|
|
|
1247
1276
|
StateHash::from_hex("hash0".to_string()),
|
|
1248
1277
|
);
|
|
1249
1278
|
|
|
1250
|
-
executor.register_replica(
|
|
1251
|
-
|
|
1279
|
+
executor.register_replica(
|
|
1280
|
+
"node2".to_string(),
|
|
1281
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1282
|
+
);
|
|
1283
|
+
executor.register_replica(
|
|
1284
|
+
"node3".to_string(),
|
|
1285
|
+
StateHash::from_hex("hash0".to_string()),
|
|
1286
|
+
);
|
|
1252
1287
|
|
|
1253
1288
|
assert!(executor.check_convergence());
|
|
1254
1289
|
}
|
|
@@ -1260,7 +1295,10 @@ mod tests {
|
|
|
1260
1295
|
StateHash::from_hex("hash0".to_string()),
|
|
1261
1296
|
);
|
|
1262
1297
|
|
|
1263
|
-
executor.register_replica(
|
|
1298
|
+
executor.register_replica(
|
|
1299
|
+
"node2".to_string(),
|
|
1300
|
+
StateHash::from_hex("hash1".to_string()),
|
|
1301
|
+
);
|
|
1264
1302
|
|
|
1265
1303
|
assert!(!executor.check_convergence());
|
|
1266
1304
|
}
|
|
@@ -137,6 +137,36 @@ impl IdentityStore {
|
|
|
137
137
|
.identities
|
|
138
138
|
.clone())
|
|
139
139
|
}
|
|
140
|
+
pub fn sign_up(&self, email: &str, password: &str, display_name: Option<String>) -> Result<(Identity, IdentitySession, String), String> {
|
|
141
|
+
let normalized = email.trim().to_lowercase();
|
|
142
|
+
if normalized.is_empty() || password.len() < 8 { return Err("email and a password of at least 8 characters are required".into()); }
|
|
143
|
+
if self.state.read().map_err(|_| "identity lock poisoned")?.identities.iter().any(|value| value.email.as_deref() == Some(&normalized)) {
|
|
144
|
+
return Err("identity already exists".into());
|
|
145
|
+
}
|
|
146
|
+
let mut identity = self.normalize("password", &normalized, Some(normalized.clone()), display_name, serde_json::json!({}))?;
|
|
147
|
+
identity.status = IdentityStatus::Active;
|
|
148
|
+
let mut state = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
149
|
+
if let Some(stored) = state.identities.iter_mut().find(|value| value.identity_id == identity.identity_id) { stored.status = IdentityStatus::Active; }
|
|
150
|
+
state.password_hashes.insert(identity.subject_id.clone(), Self::hash_password(password)?);
|
|
151
|
+
self.persist(&state)?;
|
|
152
|
+
drop(state);
|
|
153
|
+
let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
|
|
154
|
+
Ok((identity, session, token))
|
|
155
|
+
}
|
|
156
|
+
pub fn sign_in(&self, email: &str, password: &str) -> Result<(Identity, IdentitySession, String), String> {
|
|
157
|
+
let normalized = email.trim().to_lowercase();
|
|
158
|
+
let state = self.state.read().map_err(|_| "identity lock poisoned")?;
|
|
159
|
+
let identity = state.identities.iter().find(|value| value.email.as_deref() == Some(&normalized) && value.status == IdentityStatus::Active).cloned().ok_or("invalid credentials")?;
|
|
160
|
+
let valid = state.password_hashes.get(&identity.subject_id).is_some_and(|hash| Self::verify_password(hash, password));
|
|
161
|
+
drop(state);
|
|
162
|
+
if !valid { return Err("invalid credentials".into()); }
|
|
163
|
+
let (session, token) = self.issue_session(&identity, "password", None, 15 * 60)?;
|
|
164
|
+
Ok((identity, session, token))
|
|
165
|
+
}
|
|
166
|
+
pub fn authenticate_session(&self, token: &str) -> Option<IdentitySession> {
|
|
167
|
+
let current = now();
|
|
168
|
+
self.state.read().ok()?.sessions.iter().find(|value| value.refresh_hash.as_deref().is_some_and(|hash| Self::verify_password(hash, token)) && value.status == "active" && value.expires_at > current).cloned()
|
|
169
|
+
}
|
|
140
170
|
pub fn sessions(&self, subject: &str) -> Result<Vec<IdentitySession>, String> {
|
|
141
171
|
Ok(self
|
|
142
172
|
.state
|
|
@@ -154,9 +184,10 @@ impl IdentityStore {
|
|
|
154
184
|
provider: &str,
|
|
155
185
|
device: Option<String>,
|
|
156
186
|
ttl: u64,
|
|
157
|
-
) -> Result<IdentitySession, String> {
|
|
187
|
+
) -> Result<(IdentitySession, String), String> {
|
|
158
188
|
let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
159
189
|
let t = now();
|
|
190
|
+
let token = id("session_token");
|
|
160
191
|
let x = IdentitySession {
|
|
161
192
|
session_id: id("session"),
|
|
162
193
|
identity_id: identity.identity_id.clone(),
|
|
@@ -168,11 +199,11 @@ impl IdentityStore {
|
|
|
168
199
|
provider: provider.into(),
|
|
169
200
|
status: "active".into(),
|
|
170
201
|
revocation_reason: None,
|
|
171
|
-
refresh_hash: Some(
|
|
202
|
+
refresh_hash: Some(Self::hash_password(&token)?),
|
|
172
203
|
};
|
|
173
204
|
s.sessions.push(x.clone());
|
|
174
205
|
self.persist(&s)?;
|
|
175
|
-
Ok(x)
|
|
206
|
+
Ok((x, token))
|
|
176
207
|
}
|
|
177
208
|
pub fn revoke_session(&self, subject: &str, session: &str, reason: &str) -> Result<(), String> {
|
|
178
209
|
let mut s = self.state.write().map_err(|_| "identity lock poisoned")?;
|
|
@@ -247,4 +278,16 @@ mod tests {
|
|
|
247
278
|
assert!(IdentityStore::verify_password(&h, "correct horse"));
|
|
248
279
|
assert!(!IdentityStore::verify_password(&h, "wrong"));
|
|
249
280
|
}
|
|
281
|
+
#[test]
|
|
282
|
+
fn actor_tokens_are_short_lived_hashed_and_revocable() {
|
|
283
|
+
let p = std::env::temp_dir().join(format!("identity-session-{}.json", id("test")));
|
|
284
|
+
let store = IdentityStore::load(&p).unwrap();
|
|
285
|
+
let (identity, session, token) = store.sign_up("actor@example.com", "correct horse", Some("Actor".into())).unwrap();
|
|
286
|
+
assert!(session.expires_at - session.issued_at <= 15 * 60);
|
|
287
|
+
assert_ne!(session.refresh_hash.as_deref(), Some(token.as_str()));
|
|
288
|
+
assert_eq!(store.authenticate_session(&token).unwrap().subject_id, identity.subject_id);
|
|
289
|
+
store.revoke_session(&identity.subject_id, &session.session_id, "test").unwrap();
|
|
290
|
+
assert!(store.authenticate_session(&token).is_none());
|
|
291
|
+
let _ = std::fs::remove_file(p);
|
|
292
|
+
}
|
|
250
293
|
}
|