Add a device
By the end of this page Alice’s phone joins her identity, and shows her name, her spaces and her contacts.
Background
Section titled “Background”Each device has its own key. Adding a device is the same handshake as inviting a person, with a device invitation instead of a space invitation: the device already in the identity is the host, and the new device the guest. At the end, the host writes a delegation that makes the new device a member of the identity’s group.
The new device then syncs the identity’s personal space and every space the identity is in, from the other device or through EDGE. Nothing about the identity has to be typed in again.
-
Share your identity.
Identity.sharestarts a device invitation on the device you have. Add a device on the Devices tab calls this and shows the code.src/app/Devices.tsx import * asEffect from 'effect/Effect';import Effect2 collapsed linesimportReact, {(alias) namespace Reactimport ReactuseState } from 'react';function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
import {Identity,import IdentityInvitation } from '@dxos/halo';import Invitation9 collapsed linesimport {useDevices,const useDevices: () => readonly Identity.DeviceInfo[]Returns the devices belonging to the local identity. Reactive. Replaces
@dxos/react-client'suseDevices.useHaloServices } from '@dxos/halo-react';const useHaloServices: () => Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
import {Host } from './Invitations.tsx';const Host: ({ start, send, }: {start: () => Promise<Flow>;send: string;}) => React.JSX.ElementThe host's side of an invitation: the code to send, then the authorization code to tell.
/** Your identity's devices, and a way to add another. */export constDevices = () => {const Devices: () => React.JSX.ElementYour identity's devices, and a way to add another.
constservices =const services: Context<HaloServices>useHaloServices();function useHaloServices(): Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
const [adding,const adding: booleansetAdding] =const setAdding: React.Dispatch<React.SetStateAction<boolean>>useState(false);useState<boolean>(initialState: boolean | (() => boolean)): [boolean, React.Dispatch<React.SetStateAction<boolean>>] (+1 overload)Returns a stateful value, and a function to update it.
/** Starts an invitation for another device of yours, and returns the flow and the code to open there. */consthandleAdd = () =>const handleAdd: () => Promise<{flow: Invitation.Flow;code: string;}>Starts an invitation for another device of yours, and returns the flow and the code to open there.
Effect.import Effectgen(function* () {const gen: <Effect.Effect<Invitation.Flow, IdentityError, Identity.Service> | Effect.Effect<string, InvitationError, never>, {flow: Invitation.Flow;code: string;}>(f: () => Generator<Effect.Effect<Invitation.Flow, IdentityError, Identity.Service> | Effect.Effect<string, InvitationError, never>, {flow: Invitation.Flow;code: string;}, never>) => Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service> (+1 overload)Provides a way to write effectful code using generator functions, simplifying control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.
Generator functions work similarly to
async/awaitbut keep errors, requirements, and interruption in the Effect type. You canyield*values from effects and return the final result at the end.Example (Sequencing effects with generators)
import { Data, Effect } from "effect"class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}const addServiceCharge = (amount: number) => amount + 1const applyDiscount = (total: number,discountRate: number): Effect.Effect<number, DiscountRateError> =>discountRate === 0? Effect.fail(new DiscountRateError()): Effect.succeed(total - (total * discountRate) / 100)const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))export const program = Effect.gen(function*() {const transactionAmount = yield* fetchTransactionAmountconst discountRate = yield* fetchDiscountRateconst discountedAmount = yield* applyDiscount(transactionAmount,discountRate)const finalAmount = addServiceCharge(discountedAmount)return `Final amount to charge: ${finalAmount}`})await Effect.runPromise(program) // => "Final amount to charge: 96"constflow = yield*const flow: Invitation.FlowIdentity.import Identityshare({const share: (options?: Invitation.ShareOptions) => Effect.Effect<Invitation.Flow, IdentityError, Identity.Service>Initiate a device invitation (requires
Service
).
authMethod: 'shared-secret' });authMethod?: Invitation.AuthMethod | undefinedconstcode = yield*const code: stringInvitation.import Invitationcode(const code: (flow: Invitation.Flow) => Effect.Effect<string, InvitationError>Encoded, shareable code for a flow.
flow);const flow: Invitation.Flowreturn {flow,flow: Invitation.Flowcode };code: string}).pipe(Pipeable.pipe<Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service>, Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, never>, Promise<{flow: Invitation.Flow;code: string;}>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Promise<...>): Promise<...> (+21 overloads)Effect.import Effectprovide(const provide: <HaloServices>(context: Context<HaloServices>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, HaloServices>> (+5 overloads)Provides dependencies to an effect using layers or a context. Use
options.localto build the layer every time; by default, layers are shared between provide calls.Example (Providing dependencies with a layer)
import { Context, Effect, Layer } from "effect"interface Database {readonly query: (sql: string) => Effect.Effect<string>}const Database = Context.Service<Database>("Database")const DatabaseLayer = Layer.succeed(Database)({query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))})const program = Effect.gen(function*() {const db = yield* Databasereturn yield* db.query("SELECT * FROM users")})const provided = Effect.provide(program, DatabaseLayer)await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"services),const services: Context<HaloServices>Effect.import EffectrunPromise);const runPromise: <A, E>(effect: Effect.Effect<A, E>, options?: Effect.RunOptions | undefined) => Promise<A>Executes an effect and returns the result as a
Promise.When to use
Use when you need to execute an effect and work with the result using
Promisesyntax, typically for compatibility with other promise-based code.If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error.
Example (Running a successful effect as a Promise)
import { Effect } from "effect"await Effect.runPromise(Effect.succeed(1)) // => 1Example (Running effects as promises)
//Example: Handling a Failing Effect as a Rejected Promiseimport { Effect } from "effect"const output: Array<unknown> = []await Effect.runPromise(Effect.fail("my error")).catch(() => {void output.push("rejected")})output // => ["rejected"] -
Join from the new device. On the new device, before it has an identity,
Identity.joinredeems the code. The handshake and its authorization code are the same as for a space, so the welcome screen hands the flow to the same guest panel as Join. Join your other device calls this.src/app/Welcome.tsx import * asEffect from 'effect/Effect';import Effect2 collapsed linesimportReact, {(alias) namespace Reactimport ReactuseState } from 'react';function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
import {Identity } from '@dxos/halo';import Identity24 collapsed linesimport {useHaloServices } from '@dxos/halo-react';const useHaloServices: () => Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
import { typeStage,type Stage = "name" | "spaces" | "invite" | "contacts" | "devices" | "recovery"How far a page of the learning path has built the app, in the order the path builds it.
reached } from '../stage.ts';const reached: (stage: Stage, at: Stage) => booleanWhether
stageincludes whatatadds.import {Guest } from './Invitations.tsx';const Guest: ({ label, join, onDone, }: {label: string;join: (code: string) => Promise<Flow>;onDone?: () => void;}) => React.JSX.ElementThe guest's side of an invitation: paste the code, then type the authorization code.
import {Prompt } from './ui.tsx';const Prompt: ({ label, action, initial, multiline, compact, autoFocus, onSubmit, }: {label: string;action: string;initial?: string;multiline?: boolean;compact?: boolean;autoFocus?: boolean;onSubmit: (value: string) => Promise<unknown>;}) => React.JSX.ElementA field and a button, which shows the error if
onSubmitfails./** The device before it has an identity: start one, join one from another device, or recover one. */export constWelcome = ({const Welcome: ({ stage }: {stage: Stage;}) => React.JSX.ElementThe device before it has an identity: start one, join one from another device, or recover one.
stage }: {stage: Stagestage:stage: StageStage }) => {type Stage = "name" | "spaces" | "invite" | "contacts" | "devices" | "recovery"How far a page of the learning path has built the app, in the order the path builds it.
constservices =const services: Context<HaloServices>useHaloServices();function useHaloServices(): Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
const [mode,const mode: "new" | "device" | "recover"setMode] =const setMode: React.Dispatch<React.SetStateAction<"new" | "device" | "recover">>useState<'new' | 'device' | 'recover'>('new');useState<"new" | "device" | "recover">(initialState: "new" | "device" | "recover" | (() => "new" | "device" | "recover")): ["new" | "device" | "recover", React.Dispatch<React.SetStateAction<"new" | "device" | "recover">>] (+1 overload)Returns a stateful value, and a function to update it.
// Focusing the first field on load would scroll the page to the embed.const [picked,const picked: booleansetPicked] =const setPicked: React.Dispatch<React.SetStateAction<boolean>>useState(false);useState<boolean>(initialState: boolean | (() => boolean)): [boolean, React.Dispatch<React.SetStateAction<boolean>>] (+1 overload)Returns a stateful value, and a function to update it.
constpick = (const pick: (next: "new" | "device" | "recover") => voidnext: typeofnext: "new" | "device" | "recover"mode) => {const mode: "new" | "device" | "recover"setPicked(true);const setPicked: (value: React.SetStateAction<boolean>) => voidsetMode(const setMode: (value: React.SetStateAction<"new" | "device" | "recover">) => voidnext);next: "new" | "device" | "recover"};/** Creates the identity, with this device as its first member. */consthandleStart = (const handleStart: (name: string) => Promise<{readonly did: IdentityDid;readonly displayName?: string | undefined;readonly identityKey?: string | undefined;readonly data?: {readonly [x: string]: any;} | undefined;}>Creates the identity, with this device as its first member.
name: string) =>name: stringIdentity.import Identitycreate({const create: (options?: {displayName?: string;data?: Record<string, unknown>;deviceLabel?: string;}) => Effect.Effect<Identity.Info, IdentityError, Identity.Service>Create the local identity (requires
Service
).
displayName:displayName?: string | undefinedname }).name: stringpipe(Pipeable.pipe<Effect.Effect<{readonly did: IdentityDid;readonly displayName?: string | undefined;readonly identityKey?: string | undefined;readonly data?: {readonly [x: string]: any;} | undefined;}, IdentityError, Identity.Service>, Effect.Effect<{readonly did: IdentityDid;readonly displayName?: string | undefined;readonly identityKey?: string | undefined;readonly data?: {readonly [x: string]: any;} | undefined;}, IdentityError, never>, Promise<{readonly did: IdentityDid;readonly displayName?: string | undefined;readonly identityKey?: string | undefined;readonly data?: {readonly [x: string]: any;} | undefined;}>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Promise<...>): Promise<...> (+21 overloads)Effect.import Effectprovide(const provide: <HaloServices>(context: Context<HaloServices>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, HaloServices>> (+5 overloads)Provides dependencies to an effect using layers or a context. Use
options.localto build the layer every time; by default, layers are shared between provide calls.Example (Providing dependencies with a layer)
import { Context, Effect, Layer } from "effect"interface Database {readonly query: (sql: string) => Effect.Effect<string>}const Database = Context.Service<Database>("Database")const DatabaseLayer = Layer.succeed(Database)({query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))})const program = Effect.gen(function*() {const db = yield* Databasereturn yield* db.query("SELECT * FROM users")})const provided = Effect.provide(program, DatabaseLayer)await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"services),const services: Context<HaloServices>Effect.import EffectrunPromise,const runPromise: <A, E>(effect: Effect.Effect<A, E>, options?: Effect.RunOptions | undefined) => Promise<A>Executes an effect and returns the result as a
Promise.When to use
Use when you need to execute an effect and work with the result using
Promisesyntax, typically for compatibility with other promise-based code.If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error.
Example (Running a successful effect as a Promise)
import { Effect } from "effect"await Effect.runPromise(Effect.succeed(1)) // => 1Example (Running effects as promises)
//Example: Handling a Failing Effect as a Rejected Promiseimport { Effect } from "effect"const output: Array<unknown> = []await Effect.runPromise(Effect.fail("my error")).catch(() => {void output.push("rejected")})output // => ["rejected"]);/** Redeems the invitation `code` from your other device, and returns the flow. */consthandleJoin = (const handleJoin: (code: string) => Promise<Flow>Redeems the invitation
codefrom your other device, and returns the flow.code: string) =>code: stringIdentity.import Identityjoin(const join: (code: string) => Effect.Effect<Flow, IdentityError, Identity.Service>Redeem a device-invitation code (requires
Service
).
code).code: stringpipe(Pipeable.pipe<Effect.Effect<Flow, IdentityError, Identity.Service>, Effect.Effect<Flow, IdentityError, never>, Promise<Flow>>(this: Effect.Effect<Flow, IdentityError, Identity.Service>, ab: (_: Effect.Effect<Flow, IdentityError, Identity.Service>) => Effect.Effect<Flow, IdentityError, never>, bc: (_: Effect.Effect<Flow, IdentityError, never>) => Promise<Flow>): Promise<...> (+21 overloads)Effect.import Effectprovide(const provide: <HaloServices>(context: Context<HaloServices>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, HaloServices>> (+5 overloads)Provides dependencies to an effect using layers or a context. Use
options.localto build the layer every time; by default, layers are shared between provide calls.Example (Providing dependencies with a layer)
import { Context, Effect, Layer } from "effect"interface Database {readonly query: (sql: string) => Effect.Effect<string>}const Database = Context.Service<Database>("Database")const DatabaseLayer = Layer.succeed(Database)({query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))})const program = Effect.gen(function*() {const db = yield* Databasereturn yield* db.query("SELECT * FROM users")})const provided = Effect.provide(program, DatabaseLayer)await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"services),const services: Context<HaloServices>Effect.import EffectrunPromise);const runPromise: <A, E>(effect: Effect.Effect<A, E>, options?: Effect.RunOptions | undefined) => Promise<A>Executes an effect and returns the result as a
Promise.When to use
Use when you need to execute an effect and work with the result using
Promisesyntax, typically for compatibility with other promise-based code.If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error.
Example (Running a successful effect as a Promise)
import { Effect } from "effect"await Effect.runPromise(Effect.succeed(1)) // => 1Example (Running effects as promises)
//Example: Handling a Failing Effect as a Rejected Promiseimport { Effect } from "effect"const output: Array<unknown> = []await Effect.runPromise(Effect.fail("my error")).catch(() => {void output.push("rejected")})output // => ["rejected"] -
List your devices.
useDevicesreturns the identity’s devices and re-renders when one joins or comes online. It followsIdentity.devices. Each device has a key, a kind, whether it is online, and whether it is the device the code runs on. The Devices tab shows them.src/app/Devices.tsx import {useDevices,const useDevices: () => readonly Identity.DeviceInfo[]Returns the devices belonging to the local identity. Reactive. Replaces
@dxos/react-client'suseDevices.useHaloServices } from '@dxos/halo-react';const useHaloServices: () => Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
16 collapsed linesimport {Host } from './Invitations.tsx';const Host: ({ start, send, }: {start: () => Promise<Flow>;send: string;}) => React.JSX.ElementThe host's side of an invitation: the code to send, then the authorization code to tell.
/** Your identity's devices, and a way to add another. */export constDevices = () => {const Devices: () => React.JSX.ElementYour identity's devices, and a way to add another.
constservices =const services: Context<HaloServices>useHaloServices();function useHaloServices(): Context<HaloServices>Returns the HALO services context. Throws if no
HaloProvider
is above in the tree.
const [adding,const adding: booleansetAdding] =const setAdding: React.Dispatch<React.SetStateAction<boolean>>useState(false);useState<boolean>(initialState: boolean | (() => boolean)): [boolean, React.Dispatch<React.SetStateAction<boolean>>] (+1 overload)Returns a stateful value, and a function to update it.
/** Starts an invitation for another device of yours, and returns the flow and the code to open there. */consthandleAdd = () =>const handleAdd: () => Promise<{flow: Invitation.Flow;code: string;}>Starts an invitation for another device of yours, and returns the flow and the code to open there.
Effect.import Effectgen(function* () {const gen: <Effect.Effect<Invitation.Flow, IdentityError, Identity.Service> | Effect.Effect<string, InvitationError, never>, {flow: Invitation.Flow;code: string;}>(f: () => Generator<Effect.Effect<Invitation.Flow, IdentityError, Identity.Service> | Effect.Effect<string, InvitationError, never>, {flow: Invitation.Flow;code: string;}, never>) => Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service> (+1 overload)Provides a way to write effectful code using generator functions, simplifying control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.
Generator functions work similarly to
async/awaitbut keep errors, requirements, and interruption in the Effect type. You canyield*values from effects and return the final result at the end.Example (Sequencing effects with generators)
import { Data, Effect } from "effect"class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}const addServiceCharge = (amount: number) => amount + 1const applyDiscount = (total: number,discountRate: number): Effect.Effect<number, DiscountRateError> =>discountRate === 0? Effect.fail(new DiscountRateError()): Effect.succeed(total - (total * discountRate) / 100)const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))export const program = Effect.gen(function*() {const transactionAmount = yield* fetchTransactionAmountconst discountRate = yield* fetchDiscountRateconst discountedAmount = yield* applyDiscount(transactionAmount,discountRate)const finalAmount = addServiceCharge(discountedAmount)return `Final amount to charge: ${finalAmount}`})await Effect.runPromise(program) // => "Final amount to charge: 96"constflow = yield*const flow: Invitation.FlowIdentity.import Identityshare({const share: (options?: Invitation.ShareOptions) => Effect.Effect<Invitation.Flow, IdentityError, Identity.Service>Initiate a device invitation (requires
Service
).
authMethod: 'shared-secret' });authMethod?: Invitation.AuthMethod | undefinedconstcode = yield*const code: stringInvitation.import Invitationcode(const code: (flow: Invitation.Flow) => Effect.Effect<string, InvitationError>Encoded, shareable code for a flow.
flow);const flow: Invitation.Flowreturn {flow,flow: Invitation.Flowcode };code: string}).pipe(Pipeable.pipe<Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service>, Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, never>, Promise<{flow: Invitation.Flow;code: string;}>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<{flow: Invitation.Flow;code: string;}, IdentityError | InvitationError, Identity.Service>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Promise<...>): Promise<...> (+21 overloads)Effect.import Effectprovide(const provide: <HaloServices>(context: Context<HaloServices>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, HaloServices>> (+5 overloads)Provides dependencies to an effect using layers or a context. Use
options.localto build the layer every time; by default, layers are shared between provide calls.Example (Providing dependencies with a layer)
import { Context, Effect, Layer } from "effect"interface Database {readonly query: (sql: string) => Effect.Effect<string>}const Database = Context.Service<Database>("Database")const DatabaseLayer = Layer.succeed(Database)({query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))})const program = Effect.gen(function*() {const db = yield* Databasereturn yield* db.query("SELECT * FROM users")})const provided = Effect.provide(program, DatabaseLayer)await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"services),const services: Context<HaloServices>Effect.import EffectrunPromise);const runPromise: <A, E>(effect: Effect.Effect<A, E>, options?: Effect.RunOptions | undefined) => Promise<A>Executes an effect and returns the result as a
Promise.When to use
Use when you need to execute an effect and work with the result using
Promisesyntax, typically for compatibility with other promise-based code.If the effect succeeds, the promise will resolve with the result. If the effect fails, the promise will reject with an error.
Example (Running a successful effect as a Promise)
import { Effect } from "effect"await Effect.runPromise(Effect.succeed(1)) // => 1Example (Running effects as promises)
//Example: Handling a Failing Effect as a Rejected Promiseimport { Effect } from "effect"const output: Array<unknown> = []await Effect.runPromise(Effect.fail("my error")).catch(() => {void output.push("rejected")})output // => ["rejected"]/** Your devices, re-rendering on each change. */constdevices =const devices: readonly {readonly key: string;readonly current: boolean;readonly kind?: "unknown" | "browser" | "native" | "mobile" | "agent" | "agent-managed" | undefined;readonly label?: string | undefined;readonly os?: string | undefined;readonly platform?: string | undefined;readonly presence?: "online" | "offline" | "removed" | undefined;}[]Your devices, re-rendering on each change.
useDevices();function useDevices(): readonly Identity.DeviceInfo[]Returns the devices belonging to the local identity. Reactive. Replaces
@dxos/react-client'suseDevices.
Try it
Section titled “Try it”Alice’s laptop has her identity, her spaces and her contacts; her phone has nothing.
- On the laptop, open Devices, press Add a device and copy the code.
- On the phone, press Join your other device, paste it and press Join.
- Type the authorization code the laptop shows into the phone and press Send.
The phone now shows Alice’s name, both spaces and both contacts, and each device lists the other.
What you can do now
Section titled “What you can do now”You can add a device to your identity and list your devices. Next, Recover an identity covers losing all of them.