Skip to content

Commit 8bf23e0

Browse files
share access to workspace
1 parent 2fca40d commit 8bf23e0

File tree

13 files changed

+331
-48
lines changed

13 files changed

+331
-48
lines changed

apps/client/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const WEBSITE_URL = "http://localhost:3001";

apps/client/src/lib/services/sync.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class Sync extends Effect.Service<Sync>()("Sync", {
4848
}))
4949
)
5050
),
51-
Effect.orElse(() =>
51+
Effect.catchTag("NoSuchElementException", () =>
5252
client.syncAuth
5353
.generateToken({
5454
payload: {
@@ -83,6 +83,25 @@ export class Sync extends Effect.Service<Sync>()("Sync", {
8383
}),
8484
]);
8585
}),
86+
87+
pull: ({ workspaceId }: { workspaceId: string }) =>
88+
Effect.gen(function* () {
89+
const clientId = yield* initClient;
90+
yield* Effect.log(`Pulling from ${workspaceId}`);
91+
92+
const response = yield* client.syncData.pull({
93+
path: { workspaceId, clientId },
94+
});
95+
96+
const doc = new LoroDoc();
97+
doc.import(response.snapshot);
98+
yield* manager.put({
99+
workspaceId: response.workspaceId,
100+
snapshot: response.snapshot,
101+
token: response.token,
102+
version: doc.version().encode(),
103+
});
104+
}),
86105
};
87106
}),
88107
}) {}

apps/client/src/lib/services/workspace-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class WorkspaceManager extends Effect.Service<WorkspaceManager>()(
4343
)
4444
),
4545

46-
createOrJoin: (workspaceId: string | undefined) =>
46+
create: (workspaceId: string) =>
4747
query((_) =>
4848
_.workspace.toCollection().modify({ current: false })
4949
).pipe(
@@ -60,7 +60,7 @@ export class WorkspaceManager extends Effect.Service<WorkspaceManager>()(
6060
snapshot,
6161
token: null,
6262
version: null,
63-
workspaceId: workspaceId ?? crypto.randomUUID(),
63+
workspaceId,
6464
})
6565
)
6666
),

apps/client/src/routeTree.gen.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Route as rootRoute } from './routes/__root'
1414
import { Route as IndexImport } from './routes/index'
1515
import { Route as WorkspaceIdIndexImport } from './routes/$workspaceId/index'
1616
import { Route as WorkspaceIdTokenImport } from './routes/$workspaceId/token'
17+
import { Route as WorkspaceIdJoinImport } from './routes/$workspaceId/join'
1718

1819
// Create/Update Routes
1920

@@ -35,6 +36,12 @@ const WorkspaceIdTokenRoute = WorkspaceIdTokenImport.update({
3536
getParentRoute: () => rootRoute,
3637
} as any)
3738

39+
const WorkspaceIdJoinRoute = WorkspaceIdJoinImport.update({
40+
id: '/$workspaceId/join',
41+
path: '/$workspaceId/join',
42+
getParentRoute: () => rootRoute,
43+
} as any)
44+
3845
// Populate the FileRoutesByPath interface
3946

4047
declare module '@tanstack/react-router' {
@@ -46,6 +53,13 @@ declare module '@tanstack/react-router' {
4653
preLoaderRoute: typeof IndexImport
4754
parentRoute: typeof rootRoute
4855
}
56+
'/$workspaceId/join': {
57+
id: '/$workspaceId/join'
58+
path: '/$workspaceId/join'
59+
fullPath: '/$workspaceId/join'
60+
preLoaderRoute: typeof WorkspaceIdJoinImport
61+
parentRoute: typeof rootRoute
62+
}
4963
'/$workspaceId/token': {
5064
id: '/$workspaceId/token'
5165
path: '/$workspaceId/token'
@@ -67,40 +81,54 @@ declare module '@tanstack/react-router' {
6781

6882
export interface FileRoutesByFullPath {
6983
'/': typeof IndexRoute
84+
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
7085
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
7186
'/$workspaceId': typeof WorkspaceIdIndexRoute
7287
}
7388

7489
export interface FileRoutesByTo {
7590
'/': typeof IndexRoute
91+
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
7692
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
7793
'/$workspaceId': typeof WorkspaceIdIndexRoute
7894
}
7995

8096
export interface FileRoutesById {
8197
__root__: typeof rootRoute
8298
'/': typeof IndexRoute
99+
'/$workspaceId/join': typeof WorkspaceIdJoinRoute
83100
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
84101
'/$workspaceId/': typeof WorkspaceIdIndexRoute
85102
}
86103

87104
export interface FileRouteTypes {
88105
fileRoutesByFullPath: FileRoutesByFullPath
89-
fullPaths: '/' | '/$workspaceId/token' | '/$workspaceId'
106+
fullPaths:
107+
| '/'
108+
| '/$workspaceId/join'
109+
| '/$workspaceId/token'
110+
| '/$workspaceId'
90111
fileRoutesByTo: FileRoutesByTo
91-
to: '/' | '/$workspaceId/token' | '/$workspaceId'
92-
id: '__root__' | '/' | '/$workspaceId/token' | '/$workspaceId/'
112+
to: '/' | '/$workspaceId/join' | '/$workspaceId/token' | '/$workspaceId'
113+
id:
114+
| '__root__'
115+
| '/'
116+
| '/$workspaceId/join'
117+
| '/$workspaceId/token'
118+
| '/$workspaceId/'
93119
fileRoutesById: FileRoutesById
94120
}
95121

96122
export interface RootRouteChildren {
97123
IndexRoute: typeof IndexRoute
124+
WorkspaceIdJoinRoute: typeof WorkspaceIdJoinRoute
98125
WorkspaceIdTokenRoute: typeof WorkspaceIdTokenRoute
99126
WorkspaceIdIndexRoute: typeof WorkspaceIdIndexRoute
100127
}
101128

102129
const rootRouteChildren: RootRouteChildren = {
103130
IndexRoute: IndexRoute,
131+
WorkspaceIdJoinRoute: WorkspaceIdJoinRoute,
104132
WorkspaceIdTokenRoute: WorkspaceIdTokenRoute,
105133
WorkspaceIdIndexRoute: WorkspaceIdIndexRoute,
106134
}
@@ -116,13 +144,17 @@ export const routeTree = rootRoute
116144
"filePath": "__root.tsx",
117145
"children": [
118146
"/",
147+
"/$workspaceId/join",
119148
"/$workspaceId/token",
120149
"/$workspaceId/"
121150
]
122151
},
123152
"/": {
124153
"filePath": "index.tsx"
125154
},
155+
"/$workspaceId/join": {
156+
"filePath": "$workspaceId/join.tsx"
157+
},
126158
"/$workspaceId/token": {
127159
"filePath": "$workspaceId/token.tsx"
128160
},
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { createFileRoute, redirect } from "@tanstack/react-router";
2+
import { Effect } from "effect";
3+
import { LoroDoc } from "loro-crdt";
4+
import { ApiClient } from "../../lib/api-client";
5+
import { Dexie } from "../../lib/dexie";
6+
import { RuntimeClient } from "../../lib/runtime-client";
7+
import { WorkspaceManager } from "../../lib/services/workspace-manager";
8+
9+
export const Route = createFileRoute("/$workspaceId/join")({
10+
component: RouteComponent,
11+
loader: ({ params }) =>
12+
RuntimeClient.runPromise(
13+
Effect.gen(function* () {
14+
const manager = yield* WorkspaceManager;
15+
const api = yield* ApiClient;
16+
const { initClient } = yield* Dexie;
17+
18+
const clientId = yield* initClient;
19+
const workspace = yield* api.client.syncData.pull({
20+
path: { workspaceId: params.workspaceId, clientId },
21+
});
22+
23+
const doc = new LoroDoc();
24+
doc.import(workspace.snapshot);
25+
const workspaceId = yield* manager.put({
26+
token: workspace.token,
27+
snapshot: workspace.snapshot,
28+
workspaceId: workspace.workspaceId,
29+
version: doc.version().encode(),
30+
});
31+
32+
return redirect({
33+
to: `/$workspaceId`,
34+
params: { workspaceId },
35+
});
36+
})
37+
),
38+
});
39+
40+
function RouteComponent() {
41+
return null;
42+
}

apps/client/src/routes/$workspaceId/token.tsx

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { createFileRoute } from "@tanstack/react-router";
2-
import { Effect } from "effect";
2+
import { Duration, Effect } from "effect";
33
import { ApiClient } from "../../lib/api-client";
4+
import { WEBSITE_URL } from "../../lib/constants";
45
import { RuntimeClient } from "../../lib/runtime-client";
56
import { WorkspaceManager } from "../../lib/services/workspace-manager";
7+
import { useActionEffect } from "../../lib/use-action-effect";
68

79
export const Route = createFileRoute("/$workspaceId/token")({
810
component: RouteComponent,
@@ -14,16 +16,38 @@ export const Route = createFileRoute("/$workspaceId/token")({
1416
Effect.flatMap((workspace) => Effect.fromNullable(workspace?.token))
1517
);
1618

17-
return yield* api.client.syncAuth.listTokens({
19+
const tokens = yield* api.client.syncAuth.listTokens({
1820
path: { workspaceId },
1921
headers: { "x-api-key": token },
2022
});
23+
24+
return { tokens, token };
2125
})
2226
),
2327
});
2428

2529
function RouteComponent() {
26-
const tokens = Route.useLoaderData();
30+
const { workspaceId } = Route.useParams();
31+
const { tokens, token } = Route.useLoaderData();
32+
33+
const [, onIssueToken, issuing] = useActionEffect((formData: FormData) =>
34+
Effect.gen(function* () {
35+
const api = yield* ApiClient;
36+
37+
const clientId = formData.get("clientId") as string;
38+
39+
yield* api.client.syncAuth.issueToken({
40+
path: { workspaceId },
41+
headers: { "x-api-key": token },
42+
payload: {
43+
clientId,
44+
expiresIn: Duration.days(30),
45+
scope: "read_write",
46+
},
47+
});
48+
})
49+
);
50+
2751
return (
2852
<div>
2953
<h1>Tokens</h1>
@@ -35,8 +59,9 @@ function RouteComponent() {
3559
<th>isMaster</th>
3660
<th>scope</th>
3761
<th>issuedAt</th>
38-
<th>expired</th>
39-
<th>revoked</th>
62+
<th>expiresAt</th>
63+
<th>revokedAt</th>
64+
<th>Share</th>
4065
</tr>
4166
</thead>
4267
<tbody>
@@ -71,10 +96,29 @@ function RouteComponent() {
7196
})
7297
: "N/A"}
7398
</td>
99+
<td>
100+
<button
101+
type="button"
102+
onClick={() =>
103+
navigator.clipboard.writeText(
104+
`${WEBSITE_URL}/${workspaceId}/join`
105+
)
106+
}
107+
>
108+
Share
109+
</button>
110+
</td>
74111
</tr>
75112
))}
76113
</tbody>
77114
</table>
115+
116+
<form action={onIssueToken}>
117+
<input type="text" name="clientId" />
118+
<button type="submit" disabled={issuing}>
119+
Issue token
120+
</button>
121+
</form>
78122
</div>
79123
);
80124
}

apps/client/src/routes/__root.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
import { Outlet, createRootRoute } from "@tanstack/react-router";
2+
import { Effect } from "effect";
3+
import { Dexie } from "../lib/dexie";
4+
import { RuntimeClient } from "../lib/runtime-client";
25

3-
export const Route = createRootRoute({ component: RootComponent });
6+
export const Route = createRootRoute({
7+
component: RootComponent,
8+
loader: () =>
9+
RuntimeClient.runPromise(
10+
Effect.gen(function* () {
11+
const { initClient } = yield* Dexie;
12+
return yield* initClient;
13+
})
14+
),
15+
});
416

517
function RootComponent() {
18+
const clientId = Route.useLoaderData();
619
return (
720
<>
21+
<nav>
22+
<span>{clientId}</span>
23+
</nav>
824
<Outlet />
925
</>
1026
);

apps/client/src/routes/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ function HomeComponent() {
1515

1616
const [, joinWorkspace] = useActionEffect((formData: FormData | undefined) =>
1717
Effect.gen(function* () {
18-
const workspaceId = formData?.get("workspaceId") as string | null;
19-
const workspace = yield* WorkspaceManager.createOrJoin(
20-
workspaceId ?? undefined
21-
);
18+
const workspaceId = formData?.get("workspaceId") as string;
19+
const workspace = yield* WorkspaceManager.create(workspaceId);
2220
yield* Effect.sync(() =>
2321
navigate({
2422
to: `/$workspaceId`,

apps/client/src/workers/sync.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { WorkerMessage } from "./schema";
1010
const WorkerLive = WorkerRunner.layerSerialized(WorkerMessage, {
1111
Bootstrap: (params) =>
1212
Effect.gen(function* () {
13-
const { push } = yield* Sync;
13+
const { push, pull } = yield* Sync;
1414

1515
const manager = yield* WorkspaceManager;
1616
const temp = yield* TempWorkspace;
@@ -31,9 +31,10 @@ const WorkerLive = WorkerRunner.layerSerialized(WorkerMessage, {
3131
snapshot: tempUpdates.snapshot,
3232
snapshotId: tempUpdates.snapshotId,
3333
});
34-
yield* Effect.log("Sync completed");
34+
yield* Effect.log("Push sync completed");
3535
} else {
36-
yield* Effect.log("No sync updates");
36+
yield* pull({ workspaceId: workspace.workspaceId });
37+
yield* Effect.log("Pull sync completed");
3738
}
3839

3940
return true;

0 commit comments

Comments
 (0)