Skip to content

Commit

Permalink
Logged in bank (#477)
Browse files Browse the repository at this point in the history
* add current user builtin

* remove account pickers

* pass through client ids

* make it work for now

* add comment

* login / logout protocol

* implement login and logout protocol

* replace cur user function with var

* basic login ui

* fix up form

* form in flight

* factor out login wrapper

* MyBalance component

* start fixing choose function

* 'fix' tests

* make todomvc log in

* make chat log in

* break out LoggedIn component; use in TodoMVC

* fix seen by

* fix unread thingy

* unread count
  • Loading branch information
vilterp committed Jun 8, 2024
1 parent 644002f commit 5cedaf2
Show file tree
Hide file tree
Showing 10 changed files with 590 additions and 229 deletions.
185 changes: 171 additions & 14 deletions apps/actors/systems/kvSync/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ export type ClientState = {
type: "ClientState";
id: string;
data: KVData;
loginState: LoginState;
liveQueries: { [id: string]: LiveQuery };
transactions: { [id: string]: TransactionRecord };
mutationDefns: MutationDefns;
randSeed: number;
time: number;
};

export type LoginState =
| { type: "LoggedOut"; loggingInAs: string | null }
| { type: "LoggedIn"; username: string; token: string; loggingOut: boolean };

export function initialClientState(
clientID: string,
mutationDefns: MutationDefns,
Expand All @@ -49,11 +54,12 @@ export function initialClientState(
return {
type: "ClientState",
id: clientID,
loginState: { type: "LoggedOut", loggingInAs: null },
data: {},
liveQueries: {},
mutationDefns,
transactions: {},
randSeed: hashString(clientID),
randSeed,
time: 0,
};
}
Expand All @@ -77,7 +83,7 @@ export type TransactionRecord = {
function processMutationResponse(
state: ClientState,
response: MutationResponse
): [ClientState, MutationRequest | null] {
): ClientState {
const txn = state.transactions[response.txnID];
const payload = response.payload;
const newTxnState: TransactionState =
Expand Down Expand Up @@ -106,9 +112,9 @@ function processMutationResponse(
payload
);
// TODO: roll back & retry?
return [state1, null];
return state1;
case "Accept":
return [state1, null];
return state1;
}
}

Expand Down Expand Up @@ -300,42 +306,193 @@ function updateClientInner(
case "messageReceived": {
const msg = init.payload;
switch (msg.type) {
// from server
case "MutationResponse": {
const [newState, resp] = processMutationResponse(state, msg);
if (resp == null) {
return effects.updateState(newState);
// ==== from server ====

// Auth

case "SignupResponse": {
if (msg.response.type === "Failure") {
console.warn("CLIENT: signup failed");
// TODO: store error in state
return effects.updateState({
...state,
loginState: { type: "LoggedOut", loggingInAs: null },
});
}
if (state.loginState.type !== "LoggedOut") {
console.warn("CLIENT: already logged in");
return effects.updateState(state);
}
if (state.loginState.loggingInAs === null) {
console.warn("CLIENT: no user to log in as");
return effects.updateState(state);
}

return effects.updateState({
...state,
loginState: {
type: "LoggedIn",
username: state.loginState.loggingInAs,
token: msg.response.token,
loggingOut: false,
},
});
}
case "LogInResponse": {
if (msg.response.type === "Failure") {
console.warn("CLIENT: login failed");
// TODO: store error in state
return effects.updateState({
...state,
loginState: { type: "LoggedOut", loggingInAs: null },
});
}

if (state.loginState.type !== "LoggedOut") {
console.warn("CLIENT: already logged in");
return effects.updateState(state);
}
return effects.reply(init, newState, resp);
if (state.loginState.loggingInAs === null) {
console.warn("CLIENT: no user to log in as");
return effects.updateState(state);
}

return effects.updateState({
...state,
loginState: {
type: "LoggedIn",
username: state.loginState.loggingInAs,
token: msg.response.token,
loggingOut: false,
},
});
}
case "LogOutResponse": {
return effects.updateState({
...state,
loginState: { type: "LoggedOut", loggingInAs: null },
});
}

// Queries & Mutations

case "MutationResponse": {
const newState = processMutationResponse(state, msg);
return effects.updateState(newState);
}
case "LiveQueryResponse": {
return effects.updateState(processLiveQueryResponse(state, msg));
}
case "LiveQueryUpdate": {
return effects.updateState(processLiveQueryUpdate(state, msg));
}
// user input

// ==== user input ===

// Auth

case "Signup": {
const newState: ClientState = {
...state,
loginState: { type: "LoggedOut", loggingInAs: msg.username },
};
return effects.updateAndSend(newState, [
{
to: "server",
msg: {
type: "SignupRequest",
username: msg.username,
password: msg.password,
},
},
]);
}
case "Login": {
const newState: ClientState = {
...state,
loginState: { type: "LoggedOut", loggingInAs: msg.username },
};
return effects.updateAndSend(newState, [
{
to: "server",
msg: {
type: "LogInRequest",
username: msg.username,
password: msg.password,
},
},
]);
}
case "Logout": {
if (state.loginState.type === "LoggedOut") {
console.warn("CLIENT: already logged out");
return effects.updateState(state);
}

const newState: ClientState = {
...state,
loginState: { ...state.loginState, loggingOut: true },
};

return effects.updateAndSend(newState, [
{
to: "server",
msg: {
type: "AuthenticatedRequest",
token: state.loginState.token,
request: { type: "LogOutRequest" },
},
},
]);
}

// Queries & Mutations

case "RegisterQuery": {
const [newState, req] = registerLiveQuery(state, msg.id, msg.query);
return effects.updateAndSend(newState, [{ to: "server", msg: req }]);
if (state.loginState.type === "LoggedOut") {
console.warn("CLIENT: must be logged in to register query");
return effects.updateState(state);
}

return effects.updateAndSend(newState, [
{
to: "server",
msg: {
type: "AuthenticatedRequest",
token: state.loginState.token,
request: req,
},
},
]);
}
case "RunMutation": {
if (state.loginState.type === "LoggedOut") {
console.warn("CLIENT: must be logged in to run mutation");
return effects.updateState(state);
}

const [newState, req] = runMutationOnClient(
state,
{
type: "Invocation",
name: msg.invocation.name,
args: msg.invocation.args,
},
state.id
state.loginState.username
);
if (req === null) {
return effects.updateState(newState);
}

return effects.updateAndSend(newState, [
{
to: "server",
msg: req,
msg: {
type: "AuthenticatedRequest",
token: state.loginState.token,
request: req,
},
},
]);
}
Expand Down
18 changes: 5 additions & 13 deletions apps/actors/systems/kvSync/examples/bank.dd.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,10 @@ runMutation{from: "0", name: "Transfer", args: ["foo", "bar", 5]}.
hop{}?
----
application/datalog
hop{from: tick{place: "client0", time: 12}, message: MutationRequest{interpState: InterpreterState{randSeed: 5046918}, invocation: Invocation{args: ["bar",10], name: "Deposit"}, trace: [Read{key: "bar", transactionID: "-1"},Write{key: "bar", value: 10}], txnID: "4540893"}, to: tick{place: "server", time: 14}}.
hop{from: tick{place: "client0", time: 19}, message: MutationRequest{interpState: InterpreterState{randSeed: 746088}, invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}, trace: [Read{key: "foo", transactionID: "10698"},Read{key: "bar", transactionID: "4540893"},Write{key: "foo", value: 5},Write{key: "bar", value: 15}], txnID: "3045903"}, to: tick{place: "server", time: 21}}.
hop{from: tick{place: "client0", time: 26}, message: MutationRequest{interpState: InterpreterState{randSeed: 1974273}, invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}, trace: [Read{key: "foo", transactionID: "3045903"},Read{key: "bar", transactionID: "3045903"},Write{key: "foo", value: 0},Write{key: "bar", value: 20}], txnID: "1820643"}, to: tick{place: "server", time: 28}}.
hop{from: tick{place: "client0", time: 5}, message: MutationRequest{interpState: InterpreterState{randSeed: 2502798}, invocation: Invocation{args: ["foo",10], name: "Deposit"}, trace: [Read{key: "foo", transactionID: "-1"},Write{key: "foo", value: 10}], txnID: "10698"}, to: tick{place: "server", time: 7}}.
hop{from: tick{place: "server", time: 14}, message: MutationResponse{payload: Accept{timestamp: 1}, txnID: "4540893"}, to: tick{place: "client0", time: 16}}.
hop{from: tick{place: "server", time: 21}, message: MutationResponse{payload: Accept{timestamp: 2}, txnID: "3045903"}, to: tick{place: "client0", time: 23}}.
hop{from: tick{place: "server", time: 28}, message: MutationResponse{payload: Accept{timestamp: 3}, txnID: "1820643"}, to: tick{place: "client0", time: 30}}.
hop{from: tick{place: "server", time: 7}, message: MutationResponse{payload: Accept{timestamp: 0}, txnID: "10698"}, to: tick{place: "client0", time: 9}}.
hop{from: tick{place: "user0", time: 10}, message: RunMutation{invocation: Invocation{args: ["bar",10], name: "Deposit"}}, to: tick{place: "client0", time: 12}}.
hop{from: tick{place: "user0", time: 17}, message: RunMutation{invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}}, to: tick{place: "client0", time: 19}}.
hop{from: tick{place: "user0", time: 24}, message: RunMutation{invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}}, to: tick{place: "client0", time: 26}}.
hop{from: tick{place: "user0", time: 12}, message: RunMutation{invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}}, to: tick{place: "client0", time: 14}}.
hop{from: tick{place: "user0", time: 3}, message: RunMutation{invocation: Invocation{args: ["foo",10], name: "Deposit"}}, to: tick{place: "client0", time: 5}}.
hop{from: tick{place: "user0", time: 6}, message: RunMutation{invocation: Invocation{args: ["bar",10], name: "Deposit"}}, to: tick{place: "client0", time: 8}}.
hop{from: tick{place: "user0", time: 9}, message: RunMutation{invocation: Invocation{args: ["foo","bar",5], name: "Transfer"}}, to: tick{place: "client0", time: 11}}.

addClient{id: "0"}.
runMutation{from: "0", name: "Deposit", args: ["foo", 10]}.
Expand All @@ -27,6 +19,6 @@ runMutation{from: "0", name: "Transfer", args: ["foo", "bar", 5]}.
actor{}?
----
application/datalog
actor{id: "client0", initialState: ClientState{data: {}, id: "0", liveQueries: {}, mutationDefns: {"CreateAccount": Lambda{args: ["name"], body: Write{key: Var{name: "name"}, val: IntLit{val: 0}}}, "Deposit": Lambda{args: ["toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "balanceBefore"}], body: Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "+"}}}}, "Transfer": Lambda{args: ["fromAccount","toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "fromAccount"}}, "varName": "fromBalance"},{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "toBalance"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "fromBalance"}], name: ">"}, ifFalse: Do{ops: [Write{key: Var{name: "fromAccount"}, val: Apply{args: [Var{name: "fromBalance"},Var{name: "amount"}], name: "-"}},Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "toBalance"},Var{name: "amount"}], name: "+"}}]}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}, "Withdraw": Lambda{args: ["fromAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "fromAccount"}}, "varName": "balanceBefore"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "balanceBefore"}], name: ">"}, ifFalse: Write{key: Var{name: "fromAccount"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "-"}}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}}, randSeed: 48, time: 0, transactions: {}}, spawningTickID: 0}.
actor{id: "server", initialState: ServerState{data: {}, liveQueries: [], mutationDefns: {"CreateAccount": Lambda{args: ["name"], body: Write{key: Var{name: "name"}, val: IntLit{val: 0}}}, "Deposit": Lambda{args: ["toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "balanceBefore"}], body: Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "+"}}}}, "Transfer": Lambda{args: ["fromAccount","toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "fromAccount"}}, "varName": "fromBalance"},{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "toBalance"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "fromBalance"}], name: ">"}, ifFalse: Do{ops: [Write{key: Var{name: "fromAccount"}, val: Apply{args: [Var{name: "fromBalance"},Var{name: "amount"}], name: "-"}},Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "toBalance"},Var{name: "amount"}], name: "+"}}]}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}, "Withdraw": Lambda{args: ["fromAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "fromAccount"}}, "varName": "balanceBefore"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "balanceBefore"}], name: ">"}, ifFalse: Write{key: Var{name: "fromAccount"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "-"}}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}}, time: 0, transactionMetadata: {"0": {"invocation": Invocation{args: [], name: "Initial"}, "serverTimestamp": 0}}}, spawningTickID: 0}.
actor{id: "client0", initialState: ClientState{data: {}, id: "0", liveQueries: {}, loginState: LoggedOut{loggingInAs: null{}}, mutationDefns: {"CreateAccount": Lambda{args: ["name"], body: Write{key: Var{name: "name"}, val: IntLit{val: 0}}}, "Deposit": Lambda{args: ["amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "balanceBefore"}], body: Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "+"}}}}, "Transfer": Lambda{args: ["toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "fromBalance"},{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "toBalance"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "fromBalance"}], name: ">"}, ifFalse: Do{ops: [Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "fromBalance"},Var{name: "amount"}], name: "-"}},Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "toBalance"},Var{name: "amount"}], name: "+"}}]}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}, "Withdraw": Lambda{args: ["amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "balanceBefore"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "balanceBefore"}], name: ">"}, ifFalse: Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "-"}}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}}, randSeed: 48, time: 0, transactions: {}}, spawningTickID: 0}.
actor{id: "server", initialState: ServerState{data: {}, liveQueries: [], mutationDefns: {"CreateAccount": Lambda{args: ["name"], body: Write{key: Var{name: "name"}, val: IntLit{val: 0}}}, "Deposit": Lambda{args: ["amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "balanceBefore"}], body: Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "+"}}}}, "Transfer": Lambda{args: ["toAccount","amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "fromBalance"},{"val": Read{default: 0, key: Var{name: "toAccount"}}, "varName": "toBalance"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "fromBalance"}], name: ">"}, ifFalse: Do{ops: [Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "fromBalance"},Var{name: "amount"}], name: "-"}},Write{key: Var{name: "toAccount"}, val: Apply{args: [Var{name: "toBalance"},Var{name: "amount"}], name: "+"}}]}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}, "Withdraw": Lambda{args: ["amount"], body: Let{bindings: [{"val": Read{default: 0, key: Var{name: "curUser"}}, "varName": "balanceBefore"}], body: If{cond: Apply{args: [Var{name: "amount"},Var{name: "balanceBefore"}], name: ">"}, ifFalse: Write{key: Var{name: "curUser"}, val: Apply{args: [Var{name: "balanceBefore"},Var{name: "amount"}], name: "-"}}, ifTrue: Abort{reason: StringLit{val: "balance not high enough"}}}}}}, randSeed: 1234, time: 0, transactionMetadata: {"0": {"invocation": Invocation{args: [], name: "Initial"}, "serverTimestamp": 0}}, userSessions: {}, users: {}}, spawningTickID: 0}.
actor{id: "user0", initialState: UserState{}, spawningTickID: 0}.
Loading

0 comments on commit 5cedaf2

Please sign in to comment.