about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-02-19 20:00:38 +0100
committerMel <einebeere@gmail.com>2022-02-19 20:00:38 +0100
commitbf1450799df0deb424a9675be89e13c29e3620d7 (patch)
tree6a5f2f7559c5946058deadf8375f6609485a3d3f
parent5384c34952b031995ecb8aa58d72954b0c685e18 (diff)
downloadrook-bf1450799df0deb424a9675be89e13c29e3620d7.tar.zst
rook-bf1450799df0deb424a9675be89e13c29e3620d7.zip
Split state into stages to handle messages
-rw-r--r--assets/src/components/SharePage.svelte6
-rw-r--r--assets/src/components/request/RequestStatus.svelte21
-rw-r--r--assets/src/components/share/DataSelector.svelte8
-rw-r--r--assets/src/components/share/Request.svelte13
-rw-r--r--assets/src/components/share/RequestList.svelte16
-rw-r--r--assets/src/components/share/ShareStatus.svelte26
-rw-r--r--assets/src/entries/request.ts2
-rw-r--r--assets/src/entries/share.ts2
-rw-r--r--assets/src/models/incoming_request.ts16
-rw-r--r--assets/src/models/own_request.ts40
-rw-r--r--assets/src/network/transfer/request_transfer.ts35
-rw-r--r--assets/src/network/transfer/share_transfer.ts52
-rw-r--r--assets/src/network/transfer/transfer.ts46
-rw-r--r--assets/src/state/received_requests.ts17
-rw-r--r--assets/src/state/request.ts153
-rw-r--r--assets/src/state/share.ts172
-rw-r--r--assets/src/utils/bind.ts3
17 files changed, 411 insertions, 217 deletions
diff --git a/assets/src/components/SharePage.svelte b/assets/src/components/SharePage.svelte
index dff6ba0..c0c6f18 100644
--- a/assets/src/components/SharePage.svelte
+++ b/assets/src/components/SharePage.svelte
@@ -1,8 +1,10 @@
 <script lang="ts">
-    import data from "../stores/data";
     import Header from "./Header.svelte";
     import ShareStatus from "./share/ShareStatus.svelte";
     import RequestList from "./share/RequestList.svelte";
+    import { getShareState, ShareStateType } from "../state/share";
+
+    const state = getShareState().type;
 </script>
 
 <Header color="black" />
@@ -12,7 +14,7 @@
         <ShareStatus />
     </div>
     <div class="right-segment">
-        {#if $data.locked}
+        {#if $state === ShareStateType.SHARING}
             <h1>Requests</h1>
             <RequestList />
         {/if}
diff --git a/assets/src/components/request/RequestStatus.svelte b/assets/src/components/request/RequestStatus.svelte
index 49a0c31..7ba93d9 100644
--- a/assets/src/components/request/RequestStatus.svelte
+++ b/assets/src/components/request/RequestStatus.svelte
@@ -1,37 +1,30 @@
 <script lang="ts">
-    import {
-        initializeRequest,
-        OwnRequestState,
-    } from "../../models/own_request";
-    import { startRequestConnection } from "../../network/channel/request_connection";
+    import { getRequestState, RequestStateType } from "../../state/request";
     import DataView from "../DataView.svelte";
 
-    const request = initializeRequest();
-    const state = request.state;
-
-    startRequestConnection(request);
+    const state = getRequestState().type;
 </script>
 
 <!-- TODO: Bind states of same path together -->
-{#if $state === OwnRequestState.PENDING || $state === OwnRequestState.ACKNOWLEDGED}
+{#if $state === RequestStateType.CONNECTING || $state === RequestStateType.WAITING_FOR_RESPONSE}
     <h1>Waiting for a response...</h1>
     <p>
-        {#if $state === OwnRequestState.ACKNOWLEDGED}
+        {#if $state === RequestStateType.CONNECTING}
             Connecting to signaling server...
         {:else}
             The share's content will become available to you once the sharer
             decides to accept your request.
         {/if}
     </p>
-{:else if $state === OwnRequestState.IN_FLIGHT || $state === OwnRequestState.DONE}
+{:else if $state === RequestStateType.IN_FLIGHT || $state === RequestStateType.DONE}
     <h1>Your request was <b>accepted!</b></h1>
-    {#if $state === OwnRequestState.IN_FLIGHT}
+    {#if $state === RequestStateType.IN_FLIGHT}
         Transferring...
     {:else}
         <p>Congratulations! You can access the received data below:</p>
         <DataView />
     {/if}
-{:else if $state === OwnRequestState.DECLINED}
+{:else if $state === RequestStateType.DECLINED}
     <h1>Your request was <b>declined!</b></h1>
     <p>Sorry! I hope we can still be friends?</p>
 {:else}
diff --git a/assets/src/components/share/DataSelector.svelte b/assets/src/components/share/DataSelector.svelte
index 6838a05..965d0a1 100644
--- a/assets/src/components/share/DataSelector.svelte
+++ b/assets/src/components/share/DataSelector.svelte
@@ -1,13 +1,11 @@
 <script lang="ts">
-    import { startShareConnection } from "../../network/channel/share_connection";
-
-    import data from "../../stores/data";
+    import { ChoosingData, getShareState } from "../../state/share";
 
     let value = "";
 
     const submit = () => {
-        data.set(value);
-        startShareConnection();
+        const share = getShareState().state as ChoosingData;
+        share.submitData(value);
     };
 
     // TODO: Accept data other than text.
diff --git a/assets/src/components/share/Request.svelte b/assets/src/components/share/Request.svelte
index e554bfe..40a62fc 100644
--- a/assets/src/components/share/Request.svelte
+++ b/assets/src/components/share/Request.svelte
@@ -1,12 +1,9 @@
 <script lang="ts">
-    import {
-        acceptIncomingRequest,
-        declineIncomingRequest,
-        IncomingRequestState,
-    } from "../../models/incoming_request";
+    import { IncomingRequestState } from "../../models/incoming_request";
     import type { IncomingRequest } from "../../models/incoming_request";
     import CheckIcon from "../icons/CheckIcon.svelte";
     import CloseIcon from "../icons/CloseIcon.svelte";
+    import { getShareState, Sharing } from "../../state/share";
 
     export let request: IncomingRequest;
     const state = request.state;
@@ -14,11 +11,13 @@
     const time = `${request.info.receivedAt.getHours()}:${request.info.receivedAt.getMinutes()}`;
 
     async function accept() {
-        acceptIncomingRequest(request);
+        const sharing = getShareState().state as Sharing;
+        sharing.acceptRequest(request);
     }
 
     function decline() {
-        declineIncomingRequest(request);
+        const sharing = getShareState().state as Sharing;
+        sharing.declineRequest(request);
     }
 </script>
 
diff --git a/assets/src/components/share/RequestList.svelte b/assets/src/components/share/RequestList.svelte
index 510e80c..1a3b715 100644
--- a/assets/src/components/share/RequestList.svelte
+++ b/assets/src/components/share/RequestList.svelte
@@ -1,6 +1,20 @@
 <script lang="ts">
-    import requests from "../../stores/received_requests";
+    import { derived } from "svelte/store";
+    import type { Readable } from "svelte/store";
+    import type { IncomingRequest } from "../../models/incoming_request";
+    import { getShareState, Sharing } from "../../state/share";
     import Request from "./Request.svelte";
+
+    const sharing = getShareState().state as Sharing;
+    const requestMap = sharing.getRequests();
+
+    function requestSorter(a: IncomingRequest, b: IncomingRequest): number {
+        return a.info.receivedAt.getTime() - b.info.receivedAt.getTime();
+    }
+
+    const requests: Readable<IncomingRequest[]> = derived(requestMap, $map => {
+        return Object.values($map).sort(requestSorter);
+    });
 </script>
 
 {#each $requests as request (request.info.token)}
diff --git a/assets/src/components/share/ShareStatus.svelte b/assets/src/components/share/ShareStatus.svelte
index 7c22a04..ba0fea0 100644
--- a/assets/src/components/share/ShareStatus.svelte
+++ b/assets/src/components/share/ShareStatus.svelte
@@ -1,17 +1,17 @@
 <script lang="ts">
-    import {
-        ConnectionState,
-        getOwnToken,
-        getStateStore,
-    } from "../../network/channel/connection";
-    import data from "../../stores/data";
-    import DataView from "../DataView.svelte";
+    import { getShareState, ShareStateType, Sharing } from "../../state/share";
     import DataSelector from "./DataSelector.svelte";
+    import DataView from "../DataView.svelte";
 
-    let connection = getStateStore();
+    const state = getShareState().type;
+
+    function token() {
+        const sharing = getShareState().state as Sharing;
+        return sharing.getToken();
+    }
 </script>
 
-{#if !$data.locked}
+{#if $state == ShareStateType.CHOOSING_DATA}
     <h1>What do you want to share?</h1>
     <DataSelector />
 {:else}
@@ -19,14 +19,14 @@
         You are <br />
         sharing <b>a text.</b>
     </h1>
-    {#if $connection === ConnectionState.CONNECTED}
+    {#if $state === ShareStateType.CONNECTING}
+        <p>Connecting to signaling server...</p>
+    {:else}
         <p>
             Your share is available under: <br />
-            rook.rnrd.eu/<span>{getOwnToken()}</span>
+            rook.rnrd.eu/<span>{token()}</span>
         </p>
         <DataView />
-    {:else}
-        <p>Connecting to signaling server...</p>
     {/if}
 {/if}
 
diff --git a/assets/src/entries/request.ts b/assets/src/entries/request.ts
index 4b13ca7..d8b9b93 100644
--- a/assets/src/entries/request.ts
+++ b/assets/src/entries/request.ts
@@ -1,8 +1,10 @@
 import RequestPage from "../components/RequestPage.svelte";
 import { RookType } from "../models/rook_type";
 import { setClientType } from "../state/constant_state";
+import { initializeRequest } from "../state/request";
 
 setClientType(RookType.REQUEST);
+initializeRequest();
 
 const app = new RequestPage({
     target: document.getElementById("app"),
diff --git a/assets/src/entries/share.ts b/assets/src/entries/share.ts
index f38ce98..f7890b0 100644
--- a/assets/src/entries/share.ts
+++ b/assets/src/entries/share.ts
@@ -1,8 +1,10 @@
 import SharePage from "../components/SharePage.svelte";
 import { RookType } from "../models/rook_type";
 import { setClientType } from "../state/constant_state";
+import { initializeShare } from "../state/share";
 
 setClientType(RookType.SHARE);
+initializeShare();
 
 const app = new SharePage({
     target: document.getElementById("app"),
diff --git a/assets/src/models/incoming_request.ts b/assets/src/models/incoming_request.ts
index 4af1e02..f784540 100644
--- a/assets/src/models/incoming_request.ts
+++ b/assets/src/models/incoming_request.ts
@@ -1,6 +1,5 @@
-import { bindTransfer, Transfer } from "../network/transfer/transfer";
+import type { Transfer } from "../network/transfer/transfer";
 import { Writable, writable } from "svelte/store";
-import { createOfferTransfer } from "../network/transfer/share_transfer";
 
 // Represents the current progress of every request
 export enum IncomingRequestState {
@@ -58,16 +57,3 @@ export function newIncomingRequest(
         state: writable(IncomingRequestState.WAITING),
     };
 }
-
-// Starts the transfer of data from the sharer to the requestor
-export function acceptIncomingRequest(request: IncomingRequest) {
-    request.state.set(IncomingRequestState.IN_FLIGHT);
-
-    bindTransfer(request, createOfferTransfer(request.info.token), () =>
-        request.state.set(IncomingRequestState.DONE)
-    );
-}
-
-export function declineIncomingRequest(request: IncomingRequest) {
-    // TODO
-}
diff --git a/assets/src/models/own_request.ts b/assets/src/models/own_request.ts
deleted file mode 100644
index 2ad29af..0000000
--- a/assets/src/models/own_request.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { bindTransfer, Transfer } from "../network/transfer/transfer";
-import { writable, Writable } from "svelte/store";
-import { createAnswerTransfer } from "../network/transfer/request_transfer";
-
-// Represents the current progress of the request
-export enum OwnRequestState {
-    PENDING,
-    ACKNOWLEDGED,
-
-    IN_FLIGHT,
-    DONE,
-
-    DECLINED,
-    SHARE_CANCELLED,
-    NO_SUCH_SHARE,
-}
-
-export type OwnRequest = {
-    // Transfer is null while request isn't IN_FLIGHT
-    transfer: Transfer | null;
-    state: Writable<OwnRequestState>;
-};
-
-export function initializeRequest(): OwnRequest {
-    return {
-        transfer: null,
-        state: writable(OwnRequestState.PENDING),
-    };
-}
-
-export function requestAccepted(
-    request: OwnRequest,
-    description: RTCSessionDescriptionInit
-) {
-    request.state.set(OwnRequestState.IN_FLIGHT);
-
-    bindTransfer(request, createAnswerTransfer(description), () =>
-        request.state.set(OwnRequestState.DONE)
-    );
-}
diff --git a/assets/src/network/transfer/request_transfer.ts b/assets/src/network/transfer/request_transfer.ts
index e69a9a8..6e4b322 100644
--- a/assets/src/network/transfer/request_transfer.ts
+++ b/assets/src/network/transfer/request_transfer.ts
@@ -1,17 +1,13 @@
-import data from "../../stores/data";
-import { on, send } from "../channel/connection";
-import type { ShareIceCandidateMessage } from "../channel/messages/messages";
-import {
-    createTransfer,
-    onIncomingIceCandidate,
-    Transfer,
-    unregisterIceOnComplete,
-} from "./transfer";
-
-export async function createAnswerTransfer(
-    offer: RTCSessionDescriptionInit
+import data from "../../state/data";
+import type { Connection } from "../channel/connection";
+import { createTransfer, Transfer } from "./transfer";
+
+export async function respondToOffer(
+    c: Connection,
+    offer: RTCSessionDescriptionInit,
+    onComplete: () => void
 ): Promise<Transfer> {
-    const transfer = createTransfer(onChannel);
+    const transfer = createTransfer(c => onChannel(c, onComplete));
 
     const offerDescription = new RTCSessionDescription(offer);
     transfer.pc.setRemoteDescription(offerDescription);
@@ -22,19 +18,12 @@ export async function createAnswerTransfer(
     transfer.pc.onicecandidate = event => {
         const candidate = event.candidate;
         if (event.candidate !== null) {
-            send("ice_candidate", { candidate });
+            // TODO: Check whether transfer was cancelled
+            c.send("ice_candidate", { candidate });
         }
     };
 
-    const unregisterIce = on(
-        "share_ice_candidate",
-        (message: ShareIceCandidateMessage) =>
-            onIncomingIceCandidate(transfer, message)
-    );
-
-    unregisterIceOnComplete(transfer, unregisterIce);
-
-    send("accept_share", {
+    c.send("accept_share", {
         sdp: answer.sdp,
         type: answer.type,
     });
diff --git a/assets/src/network/transfer/share_transfer.ts b/assets/src/network/transfer/share_transfer.ts
index f641a56..85e67b0 100644
--- a/assets/src/network/transfer/share_transfer.ts
+++ b/assets/src/network/transfer/share_transfer.ts
@@ -1,22 +1,18 @@
 import { get } from "svelte/store";
-import dataStore from "../../stores/data";
-import { onWithToken, send } from "../channel/connection";
-import type { UnregisterFn } from "../channel/messages/event_handler";
+import dataStore from "../../state/data";
+import type { Connection } from "../channel/connection";
 import type {
     RequestIceCandidateMessage,
     ShareAcceptedMessage,
 } from "../channel/messages/messages";
-import {
-    createTransfer,
-    onIncomingIceCandidate,
-    Transfer,
-    unregisterIceOnComplete,
-} from "./transfer";
+import { createTransfer, addRemoteIceCandidate, Transfer, TransferState } from "./transfer";
 
-export async function createOfferTransfer(
-    request_token: string
+export async function createTranferAndSendOffer(
+    c: Connection,
+    request_token: string,
+    onComplete: () => void
 ): Promise<Transfer> {
-    const transfer = createTransfer(onChannel);
+    const transfer = createTransfer(c => onChannel(c, onComplete));
 
     const offer = await transfer.pc.createOffer();
     transfer.pc.setLocalDescription(offer);
@@ -24,46 +20,26 @@ export async function createOfferTransfer(
     transfer.pc.onicecandidate = event => {
         const candidate = event.candidate;
         if (event.candidate !== null) {
-            send("ice_candidate", { candidate, token: request_token });
+            // TODO: Check whether transfer was cancelled and don't send if so.
+            c.send("ice_candidate", { candidate, token: request_token });
         }
     };
 
-    send("accept_request", {
+    c.send("accept_request", {
         token: request_token,
         sdp: offer.sdp,
         type: offer.type,
     });
 
-    const unregister: UnregisterFn = onWithToken(
-        "share_accepted",
-        request_token,
-        (message: ShareAcceptedMessage) =>
-            onShareAccepted(transfer, message, unregister)
-    );
-
     return transfer;
 }
 
-function onShareAccepted(
+export function addRemoteDescription(
     transfer: Transfer,
-    message: ShareAcceptedMessage,
-    unregister: UnregisterFn
+    session: RTCSessionDescriptionInit
 ) {
-    const token = message.token;
-
-    const answerDescription = new RTCSessionDescription(message);
+    const answerDescription = new RTCSessionDescription(session);
     transfer.pc.setRemoteDescription(answerDescription);
-
-    const unregisterIce = onWithToken(
-        "request_ice_candidate",
-        token,
-        (message: RequestIceCandidateMessage) =>
-            onIncomingIceCandidate(transfer, message)
-    );
-
-    unregisterIceOnComplete(transfer, unregisterIce);
-
-    unregister();
 }
 
 function onChannel(channel: RTCDataChannel, completeTransfer: () => void) {
diff --git a/assets/src/network/transfer/transfer.ts b/assets/src/network/transfer/transfer.ts
index e950589..399250f 100644
--- a/assets/src/network/transfer/transfer.ts
+++ b/assets/src/network/transfer/transfer.ts
@@ -1,11 +1,4 @@
 import { Writable, writable } from "svelte/store";
-import type { IncomingRequest } from "../../models/incoming_request";
-import type { OwnRequest } from "../../models/own_request";
-import type { UnregisterFn } from "../channel/messages/event_handler";
-import type {
-    RequestIceCandidateMessage,
-    ShareIceCandidateMessage,
-} from "../channel/messages/messages";
 
 export enum TransferState {
     CONNECTING,
@@ -49,6 +42,7 @@ export function createTransfer(
     };
 
     channel.onopen = () => {
+        console.log("Transfer channel open.");
         state.set(TransferState.TRANSFERRING);
         onChannel(channel, () => onTransferComplete(transfer));
     };
@@ -56,43 +50,11 @@ export function createTransfer(
     return transfer;
 }
 
-export function bindTransfer(
-    request: OwnRequest | IncomingRequest,
-    transferPromise: Promise<Transfer>,
-    completeTransfer: () => void
-) {
-    transferPromise.then(transfer => {
-        request.transfer = transfer;
-
-        const unsubsribe = transfer.state.subscribe(transferState => {
-            if (transferState === TransferState.DONE) {
-                unsubsribe();
-                // Once the data has been transferred we can remove the transfer
-                request.transfer = null;
-
-                completeTransfer();
-            }
-        });
-    });
-}
-
-export function onIncomingIceCandidate(
+export function addRemoteIceCandidate(
     transfer: Transfer,
-    message: ShareIceCandidateMessage | RequestIceCandidateMessage
+    candidate: RTCIceCandidateInit
 ) {
-    transfer.pc.addIceCandidate(message.candidate);
-}
-
-export function unregisterIceOnComplete(
-    transfer: Transfer,
-    unregister: UnregisterFn
-) {
-    transfer.pc.onicegatheringstatechange = event => {
-        const connection = event.target as any;
-        if (connection.iceGatheringState === "complete") {
-            unregister();
-        }
-    };
+    transfer.pc.addIceCandidate(candidate);
 }
 
 function onTransferComplete(transfer: Transfer) {
diff --git a/assets/src/state/received_requests.ts b/assets/src/state/received_requests.ts
deleted file mode 100644
index 48916ad..0000000
--- a/assets/src/state/received_requests.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { writable } from "svelte/store";
-import type { IncomingRequest } from "../models/incoming_request";
-
-const createRequestStore = () => {
-    const { subscribe, update } = writable<IncomingRequest[]>([]);
-
-    return {
-        subscribe,
-        addRequest: (request: IncomingRequest) => update(state => [request, ...state]),
-        removeRequest: (token: string) =>
-            update(state =>
-                state.filter(request => request.info.token !== token)
-            ),
-    };
-};
-
-export default createRequestStore();
diff --git a/assets/src/state/request.ts b/assets/src/state/request.ts
new file mode 100644
index 0000000..30e71af
--- /dev/null
+++ b/assets/src/state/request.ts
@@ -0,0 +1,153 @@
+import { writable, Writable } from "svelte/store";
+import { RookType } from "../models/rook_type";
+import { Connection } from "../network/channel/connection";
+import type {
+    RequestAcceptedMessage,
+    RequestAcknowledgedMessage,
+    ShareCancelledMessage,
+    ShareIceCandidateMessage,
+} from "../network/channel/messages/messages";
+import { respondToOffer } from "../network/transfer/request_transfer";
+import { addRemoteIceCandidate, Transfer } from "../network/transfer/transfer";
+import { isClientRequest } from "./constant_state";
+import b from "../utils/bind";
+
+export enum RequestStateType {
+    CONNECTING,
+
+    WAITING_FOR_RESPONSE,
+
+    IN_FLIGHT,
+    DONE,
+
+    DECLINED,
+    SHARE_CANCELLED,
+    NO_SUCH_SHARE,
+}
+
+type RequestState = {
+    type: Writable<RequestStateType>;
+    state:
+        | Connecting
+        | WaitingForResponse
+        | Transferring
+        | Done
+        | Declined
+        | ShareCancelled
+        | NoSuchShare;
+};
+
+let request: RequestState | null = null;
+
+export function initializeRequest() {
+    if (!isClientRequest()) {
+        throw new Error(
+            "Tried to initialize request state on non-request client."
+        );
+    }
+
+    if (request) {
+        throw new Error("Request state already initialized.");
+    }
+
+    request = {
+        type: writable(RequestStateType.CONNECTING),
+        state: new Connecting(),
+    };
+}
+
+export function getRequestState(): RequestState {
+    if (!isClientRequest()) {
+        throw new Error("Tried to access share state on non-share client.");
+    }
+
+    return request;
+}
+
+class Connecting {
+    private connection: Connection;
+
+    constructor() {
+        this.connection = new Connection();
+        this.connection.setChannelMessageHandler({
+            request_acknowledged: b(this, this.onRequestAcknowledged),
+        });
+
+        this.connection.start(RookType.REQUEST);
+    }
+
+    private onRequestAcknowledged(m: RequestAcknowledgedMessage) {
+        request.type.set(RequestStateType.WAITING_FOR_RESPONSE);
+        request.state = new WaitingForResponse(this.connection);
+    }
+}
+
+class WaitingForResponse {
+    private connection: Connection;
+
+    constructor(connection: Connection) {
+        this.connection = connection;
+
+        this.connection.setChannelMessageHandler({
+            request_accepted: b(this, this.onRequestAccepted),
+            share_cancelled: b(this, this.onShareCancelled),
+            // TODO: request_declined
+        });
+    }
+
+    private onRequestAccepted(m: RequestAcceptedMessage) {
+        request.type.set(RequestStateType.IN_FLIGHT);
+        request.state = new Transferring(this.connection, m);
+    }
+
+    private onShareCancelled(m: ShareCancelledMessage) {
+        request.type.set(RequestStateType.SHARE_CANCELLED);
+        request.state = null;
+    }
+}
+
+class Transferring {
+    private connection: Connection;
+    private transfer: Transfer | null = null;
+    private unaddedIceCandidates: RTCIceCandidateInit[] = [];
+
+    constructor(connection: Connection, offer: RTCSessionDescriptionInit) {
+        this.connection = connection;
+        this.connection.setChannelMessageHandler({
+            share_ice_candidate: b(this, this.onShareIceCandidate),
+            // TODO: share_cancelled
+        });
+
+        const offerPromise = respondToOffer(
+            this.connection,
+            offer,
+            b(this, this.onTransferComplete)
+        );
+
+        offerPromise.then(transfer => {
+            for (const candidate of this.unaddedIceCandidates) {
+                addRemoteIceCandidate(transfer, candidate);
+            }
+            this.unaddedIceCandidates = [];
+        });
+    }
+
+    private onShareIceCandidate(m: ShareIceCandidateMessage) {
+        if (!this.transfer) {
+            this.unaddedIceCandidates.push(m.candidate);
+        } else {
+            addRemoteIceCandidate(this.transfer, m.candidate);
+        }
+    }
+
+    private onTransferComplete() {
+        request.type.set(RequestStateType.DONE);
+        request.state = null;
+    }
+}
+
+// Finished states.
+type Done = null;
+type Declined = null;
+type ShareCancelled = null;
+type NoSuchShare = null;
diff --git a/assets/src/state/share.ts b/assets/src/state/share.ts
new file mode 100644
index 0000000..5a2aaae
--- /dev/null
+++ b/assets/src/state/share.ts
@@ -0,0 +1,172 @@
+import { get, Readable, writable, Writable } from "svelte/store";
+import {
+    IncomingRequest,
+    IncomingRequestState,
+    newIncomingRequest,
+} from "../models/incoming_request";
+import { RookType } from "../models/rook_type";
+import { Connection } from "../network/channel/connection";
+import type {
+    NewRequestMessage,
+    RequestCancelledMessage,
+    RequestIceCandidateMessage,
+    ShareAcceptedMessage,
+} from "../network/channel/messages/messages";
+import {
+    addRemoteDescription,
+    createTranferAndSendOffer,
+} from "../network/transfer/share_transfer";
+import { addRemoteIceCandidate } from "../network/transfer/transfer";
+import { isClientShare } from "./constant_state";
+import data from "./data";
+import b from "../utils/bind";
+
+export enum ShareStateType {
+    CHOOSING_DATA,
+    CONNECTING,
+    SHARING,
+}
+
+type ShareState = {
+    type: Writable<ShareStateType>;
+    state: ChoosingData | Connecting | Sharing;
+};
+
+let share: ShareState | null = null;
+
+export function initializeShare() {
+    if (!isClientShare()) {
+        throw new Error("Tried to initialize share state on non-share client.");
+    }
+
+    if (share) {
+        throw new Error("Share state already initialized.");
+    }
+
+    share = {
+        type: writable(ShareStateType.CHOOSING_DATA),
+        state: new ChoosingData(),
+    };
+}
+
+export function getShareState(): ShareState {
+    if (!isClientShare()) {
+        throw new Error("Tried to access share state on non-share client.");
+    }
+
+    return share;
+}
+
+export class ChoosingData {
+    public submitData(d: string): void {
+        data.set(d);
+
+        share.type.set(ShareStateType.CONNECTING);
+        share.state = new Connecting();
+    }
+}
+
+export class Connecting {
+    private connection: Connection;
+
+    constructor() {
+        this.connection = new Connection();
+        this.connection.setChannelMessageHandler({});
+
+        this.connection.start(RookType.SHARE).then(b(this, this.onConnect));
+    }
+
+    private onConnect() {
+        share.type.set(ShareStateType.SHARING);
+        share.state = new Sharing(this.connection);
+    }
+}
+
+export class Sharing {
+    private incomingRequests: Writable<{ [key: string]: IncomingRequest }> =
+        writable({});
+
+    private connection: Connection;
+
+    constructor(connection: Connection) {
+        this.connection = connection;
+        this.connection.setChannelMessageHandler({
+            new_request: b(this, this.onNewRequest),
+            request_cancelled: b(this, this.onRequestCancelled),
+
+            share_accepted: b(this, this.onShareAccepted),
+            request_ice_candidate: b(this, this.onRequestIceCandidate),
+        });
+    }
+
+    public getToken(): string {
+        return this.connection.token;
+    }
+
+    public getRequests(): Readable<{ [key: string]: IncomingRequest }> {
+        return this.incomingRequests;
+    }
+
+    public async acceptRequest(request: IncomingRequest) {
+        request.state.set(IncomingRequestState.IN_FLIGHT);
+
+        const transfer = await createTranferAndSendOffer(
+            this.connection,
+            request.info.token,
+            () => {
+                this.onRequestTransferComplete(request);
+            }
+        );
+        request.transfer = transfer;
+    }
+
+    public async declineRequest(request: IncomingRequest) {
+        // TODO: Implement.
+        throw new Error("Declining requests is not implemented yet.");
+    }
+
+    private onNewRequest(m: NewRequestMessage) {
+        const request = newIncomingRequest(m.token, m.ip, m.location, m.client);
+
+        const mapping = {
+            [m.token]: request,
+        };
+
+        // TODO: Check if the request is already in the list.
+
+        this.incomingRequests.update(requests => {
+            return {
+                ...requests,
+                ...mapping,
+            };
+        });
+    }
+
+    private onRequestCancelled(m: RequestCancelledMessage) {
+        // TODO: Cancel ongoing share.
+
+        this.incomingRequests.update(requests => {
+            const newRequests = {
+                ...requests,
+            };
+            // TODO: Check if the request is in the list.
+            delete newRequests[m.token];
+            return newRequests;
+        });
+    }
+
+    private onShareAccepted(m: ShareAcceptedMessage) {
+        // TODO: Check if the request is in the list.
+        const request = get(this.incomingRequests)[m.token];
+        addRemoteDescription(request.transfer, m);
+    }
+
+    private onRequestIceCandidate(m: RequestIceCandidateMessage) {
+        const request = get(this.incomingRequests)[m.token];
+        addRemoteIceCandidate(request.transfer, m.candidate);
+    }
+
+    private onRequestTransferComplete(request: IncomingRequest) {
+        request.state.set(IncomingRequestState.DONE);
+    }
+}
diff --git a/assets/src/utils/bind.ts b/assets/src/utils/bind.ts
new file mode 100644
index 0000000..fe2d8fd
--- /dev/null
+++ b/assets/src/utils/bind.ts
@@ -0,0 +1,3 @@
+export default function <F extends Function>(to: object, f: F): F {
+    return f.bind(to);
+}