Skip to content

Commit af3ba40

Browse files
Merge pull request #1 from typeonce-dev/app-schema
App schema
2 parents 7e3af04 + 5f438d5 commit af3ba40

22 files changed

Lines changed: 207 additions & 243 deletions

apps/client/src/lib/hooks/use-activity.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { hookQuery, useDexieQuery } from "@local/client-lib";
2+
import { SnapshotSchema } from "@local/schema";
3+
import { RuntimeClient } from "../runtime-client";
4+
5+
export const useFood = ({ workspaceId }: { workspaceId: string }) => {
6+
return useDexieQuery(
7+
() =>
8+
RuntimeClient.runPromise(
9+
hookQuery((doc) => doc.getList("food"), { workspaceId })
10+
),
11+
SnapshotSchema.fields.food.value
12+
);
13+
};

apps/client/src/lib/hooks/use-metadata.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { RuntimeLayer } from "@local/client-lib";
2+
import { Layer, ManagedRuntime } from "effect";
3+
import { Storage } from "./storage";
4+
5+
const MainLayer = Layer.mergeAll(RuntimeLayer, Storage.Default);
6+
7+
export const RuntimeClient = ManagedRuntime.make(MainLayer);

apps/client/src/lib/storage.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Service } from "@local/client-lib";
2+
import { SnapshotSchema } from "@local/schema";
3+
import { Effect, Schema } from "effect";
4+
import { LoroMap, VersionVector } from "loro-crdt";
5+
6+
export class Storage extends Effect.Service<Storage>()("Storage", {
7+
dependencies: [Service.TempWorkspace.Default, Service.LoroStorage.Default],
8+
effect: Effect.gen(function* () {
9+
const temp = yield* Service.TempWorkspace;
10+
const { load } = yield* Service.LoroStorage;
11+
12+
const insertFood = ({
13+
workspaceId,
14+
value,
15+
}: {
16+
workspaceId: string;
17+
value: typeof SnapshotSchema.fields.food.value.Type;
18+
}) =>
19+
Effect.gen(function* () {
20+
const { doc, workspace } = yield* load({ workspaceId });
21+
22+
const list = doc.getList("food");
23+
24+
const container = list.insertContainer(list.length, new LoroMap());
25+
26+
const food = yield* Schema.encode(SnapshotSchema.fields.food.value)(
27+
value
28+
);
29+
30+
Object.entries(food).forEach(([key, val]) => {
31+
container.set(
32+
key as keyof typeof SnapshotSchema.fields.food.value.Type,
33+
val
34+
);
35+
});
36+
37+
const snapshotExport =
38+
workspace === undefined
39+
? doc.export({ mode: "snapshot" })
40+
: doc.export({
41+
mode: "update",
42+
from: new VersionVector(workspace.version),
43+
});
44+
45+
return yield* temp.put({
46+
workspaceId,
47+
snapshot: snapshotExport,
48+
snapshotId: crypto.randomUUID(),
49+
});
50+
});
51+
52+
return { insertFood };
53+
}),
54+
}) {}

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

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import { Worker } from "@effect/platform";
22
import { BrowserWorker } from "@effect/platform-browser";
3-
import {
4-
RuntimeLib,
5-
Service,
6-
SyncWorker,
7-
useActionEffect,
8-
} from "@local/client-lib";
3+
import { Service, SyncWorker, useActionEffect } from "@local/client-lib";
94
import { createFileRoute, Link } from "@tanstack/react-router";
105
import { Effect } from "effect";
116
import { startTransition, useEffect } from "react";
12-
import { useActivity } from "../../lib/hooks/use-activity";
13-
import { useMetadata } from "../../lib/hooks/use-metadata";
7+
import { useFood } from "../../lib/hooks/use-food";
8+
import { RuntimeClient } from "../../lib/runtime-client";
9+
import { Storage } from "../../lib/storage";
1410

1511
const bootstrap = ({ workspaceId }: { workspaceId: string }) =>
1612
Effect.gen(function* () {
@@ -33,7 +29,7 @@ const bootstrap = ({ workspaceId }: { workspaceId: string }) =>
3329
export const Route = createFileRoute("/$workspaceId/")({
3430
component: RouteComponent,
3531
loader: ({ params: { workspaceId } }) =>
36-
RuntimeLib.runPromise(
32+
RuntimeClient.runPromise(
3733
Service.WorkspaceManager.getById({ workspaceId }).pipe(
3834
Effect.flatMap(Effect.fromNullable),
3935
Effect.tap(({ workspaceId }) => bootstrap({ workspaceId }))
@@ -44,28 +40,27 @@ export const Route = createFileRoute("/$workspaceId/")({
4440
function RouteComponent() {
4541
const workspace = Route.useLoaderData();
4642

47-
const { data: metadata } = useMetadata({
48-
workspaceId: workspace.workspaceId,
49-
});
50-
const { data, error, loading } = useActivity({
43+
const { data, error, loading } = useFood({
5144
workspaceId: workspace.workspaceId,
5245
});
5346

54-
const [, onBootstrap, bootstrapping] = useActionEffect(bootstrap);
55-
const [, onAdd] = useActionEffect((formData: FormData) =>
47+
const [, onBootstrap, bootstrapping] = useActionEffect(
48+
RuntimeClient,
49+
bootstrap
50+
);
51+
const [, onAdd] = useActionEffect(RuntimeClient, (formData: FormData) =>
5652
Effect.gen(function* () {
57-
const loroStorage = yield* Service.LoroStorage;
53+
const loroStorage = yield* Storage;
5854

59-
const firstName = formData.get("firstName") as string;
60-
const lastName = formData.get("lastName") as string;
55+
const name = formData.get("name") as string;
56+
const calories = formData.get("calories") as string;
6157

62-
yield* loroStorage.insertActivity({
58+
yield* loroStorage.insertFood({
6359
workspaceId: workspace.workspaceId,
6460
value: {
6561
id: crypto.randomUUID(),
66-
firstName,
67-
lastName,
68-
age: 10,
62+
name,
63+
calories: parseInt(calories, 10),
6964
},
7065
});
7166
})
@@ -75,7 +70,7 @@ function RouteComponent() {
7570
const url = new URL("./src/workers/live.ts", globalThis.origin);
7671
const newWorker = new globalThis.Worker(url, { type: "module" });
7772

78-
void RuntimeLib.runPromise(
73+
void RuntimeClient.runPromise(
7974
Effect.gen(function* () {
8075
const pool = yield* Worker.makePoolSerialized({ size: 1 });
8176
return yield* pool.broadcast(
@@ -98,7 +93,6 @@ function RouteComponent() {
9893

9994
return (
10095
<div>
101-
<pre>{JSON.stringify(metadata)}</pre>
10296
<Link
10397
to="/$workspaceId/token"
10498
params={{ workspaceId: workspace.workspaceId }}
@@ -119,19 +113,18 @@ function RouteComponent() {
119113
</button>
120114

121115
<form action={onAdd}>
122-
<input type="text" name="firstName" />
123-
<input type="text" name="lastName" />
124-
<button type="submit">Add activity</button>
116+
<input type="text" name="name" />
117+
<input type="number" name="calories" min={1} />
118+
<button type="submit">Add food</button>
125119
</form>
126120

127121
<div>
128122
{loading && <p>Loading...</p>}
129123
{error && <pre>{JSON.stringify(error, null, 2)}</pre>}
130-
{(data ?? []).map((activity) => (
131-
<div key={activity.id}>
132-
<p>First name: {activity.firstName}</p>
133-
<p>Last name: {activity.lastName}</p>
134-
{activity.age && <p>Age: {activity.age}</p>}
124+
{(data ?? []).map((food) => (
125+
<div key={food.id}>
126+
<p>Name: {food.name}</p>
127+
<p>Calories: {food.calories}</p>
135128
</div>
136129
))}
137130
</div>

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { RuntimeLib, Service } from "@local/client-lib";
1+
import { Service } from "@local/client-lib";
22
import { createFileRoute, redirect } from "@tanstack/react-router";
33
import { Effect } from "effect";
4+
import { RuntimeClient } from "../../lib/runtime-client";
45

56
export const Route = createFileRoute("/$workspaceId/join")({
67
component: RouteComponent,
78
loader: ({ params }) =>
8-
RuntimeLib.runPromise(
9+
RuntimeClient.runPromise(
910
Effect.gen(function* () {
1011
const { join } = yield* Service.Sync;
1112
yield* join({ workspaceId: params.workspaceId });

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

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { RuntimeLib, Service, useActionEffect } from "@local/client-lib";
1+
import { Service, useActionEffect } from "@local/client-lib";
22
import { createFileRoute, Link, useRouter } from "@tanstack/react-router";
33
import { Duration, Effect } from "effect";
44
import { WEBSITE_URL } from "../../lib/constants";
5+
import { RuntimeClient } from "../../lib/runtime-client";
56

67
export const Route = createFileRoute("/$workspaceId/token")({
78
component: RouteComponent,
89
loader: ({ params: { workspaceId } }) =>
9-
RuntimeLib.runPromise(
10+
RuntimeClient.runPromise(
1011
Effect.gen(function* () {
1112
const api = yield* Service.ApiClient;
1213
const token = yield* Service.WorkspaceManager.getById({
@@ -30,39 +31,43 @@ function RouteComponent() {
3031
const { tokens, token } = Route.useLoaderData();
3132
const router = useRouter();
3233

33-
const [, onIssueToken, issuing] = useActionEffect((formData: FormData) =>
34-
Effect.gen(function* () {
35-
const api = yield* Service.ApiClient;
34+
const [, onIssueToken, issuing] = useActionEffect(
35+
RuntimeClient,
36+
(formData: FormData) =>
37+
Effect.gen(function* () {
38+
const api = yield* Service.ApiClient;
3639

37-
const clientId = formData.get("clientId") as string;
40+
const clientId = formData.get("clientId") as string;
3841

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-
});
42+
yield* api.client.syncAuth.issueToken({
43+
path: { workspaceId },
44+
headers: { "x-api-key": token },
45+
payload: {
46+
clientId,
47+
expiresIn: Duration.days(30),
48+
scope: "read_write",
49+
},
50+
});
4851

49-
yield* Effect.promise(() => router.invalidate({ sync: true }));
50-
})
52+
yield* Effect.promise(() => router.invalidate({ sync: true }));
53+
})
5154
);
5255

53-
const [, onRevoke, revoking] = useActionEffect((formData: FormData) =>
54-
Effect.gen(function* () {
55-
const api = yield* Service.ApiClient;
56+
const [, onRevoke, revoking] = useActionEffect(
57+
RuntimeClient,
58+
(formData: FormData) =>
59+
Effect.gen(function* () {
60+
const api = yield* Service.ApiClient;
5661

57-
const clientId = formData.get("clientId") as string;
62+
const clientId = formData.get("clientId") as string;
5863

59-
yield* api.client.syncAuth.revokeToken({
60-
path: { workspaceId, clientId },
61-
headers: { "x-api-key": token },
62-
});
64+
yield* api.client.syncAuth.revokeToken({
65+
path: { workspaceId, clientId },
66+
headers: { "x-api-key": token },
67+
});
6368

64-
yield* Effect.promise(() => router.invalidate({ sync: true }));
65-
})
69+
yield* Effect.promise(() => router.invalidate({ sync: true }));
70+
})
6671
);
6772

6873
return (

apps/client/src/routes/__root.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { RuntimeLib, Service } from "@local/client-lib";
1+
import { Service } from "@local/client-lib";
22
import { Outlet, createRootRoute } from "@tanstack/react-router";
33
import { Effect } from "effect";
4+
import { RuntimeClient } from "../lib/runtime-client";
45

56
export const Route = createRootRoute({
67
component: RootComponent,
78
loader: () =>
8-
RuntimeLib.runPromise(
9+
RuntimeClient.runPromise(
910
Service.Migration.pipe(
1011
Effect.flatMap((migration) => migration.migrate),
1112
Effect.catchAll((error) => Effect.logError("Migration error", error)),

apps/client/src/routes/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import { RuntimeLib, Service, useActionEffect } from "@local/client-lib";
1+
import { Service, useActionEffect } from "@local/client-lib";
22
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
33
import { Effect } from "effect";
4+
import { RuntimeClient } from "../lib/runtime-client";
45

56
export const Route = createFileRoute("/")({
67
component: HomeComponent,
7-
loader: () => RuntimeLib.runPromise(Service.WorkspaceManager.getAll),
8+
loader: () => RuntimeClient.runPromise(Service.WorkspaceManager.getAll),
89
});
910

1011
function HomeComponent() {
1112
const allWorkspaces = Route.useLoaderData();
1213
const navigate = useNavigate();
1314

14-
const [, joinWorkspace] = useActionEffect(() =>
15+
const [, joinWorkspace] = useActionEffect(RuntimeClient, () =>
1516
Effect.gen(function* () {
1617
const workspace = yield* Service.WorkspaceManager.create;
1718
yield* Effect.sync(() =>

0 commit comments

Comments
 (0)