Skip to content

Commit cba5cff

Browse files
query and insert loro storage
1 parent 105689b commit cba5cff

File tree

7 files changed

+149
-75
lines changed

7 files changed

+149
-75
lines changed

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import { SnapshotSchema } from "@local/schema";
33
import { RuntimeClient } from "../runtime-client";
44

55
export const useFood = ({ workspaceId }: { workspaceId: string }) => {
6-
return useDexieQuery(
7-
() =>
8-
RuntimeClient.runPromise(
9-
Service.LoroStorage.use(({ query }) =>
10-
query((doc) => doc.getList("food"), {
11-
workspaceId,
12-
})
13-
)
14-
),
15-
SnapshotSchema.fields.food.value
6+
return useDexieQuery(() =>
7+
RuntimeClient.runPromise(
8+
Service.LoroStorage.use(({ query }) =>
9+
query((doc) => doc.getList("food"), SnapshotSchema.fields.food.value, {
10+
workspaceId,
11+
})
12+
)
13+
)
1614
);
1715
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Service, useDexieQuery } from "@local/client-lib";
2+
import { SnapshotSchema } from "@local/schema";
3+
import { Effect } from "effect";
4+
import { RuntimeClient } from "../runtime-client";
5+
6+
export const useMeal = ({ workspaceId }: { workspaceId: string }) => {
7+
return useDexieQuery(() =>
8+
RuntimeClient.runPromise(
9+
Effect.gen(function* () {
10+
const { query } = yield* Service.LoroStorage;
11+
12+
const meals = yield* query(
13+
(doc) => doc.getList("meal"),
14+
SnapshotSchema.fields.meal.value,
15+
{ workspaceId }
16+
);
17+
18+
const foods = yield* query(
19+
(doc) => doc.getList("food"),
20+
SnapshotSchema.fields.food.value,
21+
{ workspaceId }
22+
);
23+
24+
return meals.map(({ foodId, id, quantity }) => {
25+
const food = foods.find((food) => food.id === foodId);
26+
return { id, quantity, food };
27+
});
28+
})
29+
)
30+
);
31+
};

apps/client/src/lib/storage.ts

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Service } from "@local/client-lib";
2-
import { SnapshotSchema } from "@local/schema";
2+
import { CurrentSchema, SnapshotSchema } from "@local/schema";
33
import { Effect, Schema } from "effect";
44
import { LoroMap, VersionVector } from "loro-crdt";
55

@@ -9,46 +9,48 @@ export class Storage extends Effect.Service<Storage>()("Storage", {
99
const temp = yield* Service.TempWorkspace;
1010
const { load } = yield* Service.LoroStorage;
1111

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-
);
12+
const insert =
13+
<T extends typeof SnapshotSchema.Table.Type>(table: T) =>
14+
({
15+
workspaceId,
16+
value,
17+
}: {
18+
workspaceId: string;
19+
value: Schema.Schema.Encoded<(typeof CurrentSchema.fields)[T]>[number];
20+
}) =>
21+
Effect.gen(function* () {
22+
const { doc, workspace } = yield* load({ workspaceId });
23+
24+
const list = doc.getList(table);
25+
26+
const container = list.insertContainer(list.length, new LoroMap());
27+
28+
const data = yield* Schema.encode(
29+
CurrentSchema.fields[table].value as Schema.Schema<typeof value>
30+
)(value);
31+
32+
Object.entries(data).forEach(([key, val]) => {
33+
container.set(key, val);
34+
});
35+
36+
const snapshotExport =
37+
workspace === undefined
38+
? doc.export({ mode: "snapshot" })
39+
: doc.export({
40+
mode: "update",
41+
from: new VersionVector(workspace.version),
42+
});
43+
44+
return yield* temp.put({
45+
workspaceId,
46+
snapshot: snapshotExport,
47+
snapshotId: crypto.randomUUID(),
48+
});
3549
});
3650

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 };
51+
return {
52+
insertFood: insert("food"),
53+
insertMeal: insert("meal"),
54+
} as const;
5355
}),
5456
}) {}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
55
import { Effect } from "effect";
66
import { startTransition, useEffect } from "react";
77
import { useFood } from "../../lib/hooks/use-food";
8+
import { useMeal } from "../../lib/hooks/use-meal";
89
import { RuntimeClient } from "../../lib/runtime-client";
910
import { Storage } from "../../lib/storage";
1011

@@ -44,11 +45,16 @@ function RouteComponent() {
4445
workspaceId: workspace.workspaceId,
4546
});
4647

48+
const { data: meals } = useMeal({
49+
workspaceId: workspace.workspaceId,
50+
});
51+
4752
const [, onBootstrap, bootstrapping] = useActionEffect(
4853
RuntimeClient,
4954
bootstrap
5055
);
51-
const [, onAdd] = useActionEffect(RuntimeClient, (formData: FormData) =>
56+
57+
const [, onAddFood] = useActionEffect(RuntimeClient, (formData: FormData) =>
5258
Effect.gen(function* () {
5359
const loroStorage = yield* Storage;
5460

@@ -66,6 +72,24 @@ function RouteComponent() {
6672
})
6773
);
6874

75+
const [, onAddMeal] = useActionEffect(RuntimeClient, (formData: FormData) =>
76+
Effect.gen(function* () {
77+
const loroStorage = yield* Storage;
78+
79+
const foodId = formData.get("foodId") as string;
80+
const quantity = formData.get("quantity") as string;
81+
82+
yield* loroStorage.insertMeal({
83+
workspaceId: workspace.workspaceId,
84+
value: {
85+
id: crypto.randomUUID(),
86+
foodId,
87+
quantity: parseInt(quantity, 10),
88+
},
89+
});
90+
})
91+
);
92+
6993
useEffect(() => {
7094
const url = new URL("./src/workers/live.ts", globalThis.origin);
7195
const newWorker = new globalThis.Worker(url, { type: "module" });
@@ -112,7 +136,7 @@ function RouteComponent() {
112136
{bootstrapping ? "Bootstrapping..." : "Bootstrap"}
113137
</button>
114138

115-
<form action={onAdd}>
139+
<form action={onAddFood}>
116140
<input type="text" name="name" />
117141
<input type="number" name="calories" min={1} />
118142
<button type="submit">Add food</button>
@@ -128,6 +152,26 @@ function RouteComponent() {
128152
</div>
129153
))}
130154
</div>
155+
156+
<form action={onAddMeal}>
157+
{(data ?? []).map((food) => (
158+
<div key={food.id}>
159+
<input type="radio" name="foodId" id={food.id} value={food.id} />
160+
<label htmlFor={food.id}>{food.name}</label>
161+
</div>
162+
))}
163+
<input type="number" name="quantity" min={1} />
164+
<button type="submit">Add meal</button>
165+
</form>
166+
167+
<div>
168+
{(meals ?? []).map((meal) => (
169+
<div key={meal.id}>
170+
<p>Food: {meal.food?.name}</p>
171+
<p>Quantity: {meal.quantity}</p>
172+
</div>
173+
))}
174+
</div>
131175
</div>
132176
);
133177
}

packages/client-lib/src/services/loro-storage.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SnapshotSchema, type LoroSchema } from "@local/schema";
2-
import { Effect } from "effect";
3-
import { LoroDoc } from "loro-crdt";
2+
import { Effect, Schema } from "effect";
3+
import { LoroDoc, type LoroList, type LoroMap } from "loro-crdt";
44
import { TempWorkspace } from "./temp-workspace";
55
import { WorkspaceManager } from "./workspace-manager";
66

@@ -34,10 +34,20 @@ export class LoroStorage extends Effect.Service<LoroStorage>()("LoroStorage", {
3434
})
3535
);
3636

37-
const query = <A>(
38-
extract: (doc: LoroDoc<LoroSchema>) => A,
37+
const query = <A extends Record<string, unknown>>(
38+
extract: (doc: LoroDoc<LoroSchema>) => LoroList<LoroMap<A>>,
39+
schema: Schema.Schema<A>,
3940
{ workspaceId }: { workspaceId: string }
40-
) => load({ workspaceId }).pipe(Effect.map(({ doc }) => extract(doc)));
41+
) =>
42+
Effect.gen(function* () {
43+
const { doc } = yield* load({ workspaceId });
44+
const data = extract(doc);
45+
const list = data.toArray();
46+
return yield* Effect.all(
47+
list.map((item) => Schema.decode(schema)(item.toJSON() as A)),
48+
{ concurrency: 10 }
49+
);
50+
});
4151

4252
return { load, query } as const;
4353
}),

packages/client-lib/src/use-dexie-query.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useLiveQuery } from "dexie-react-hooks";
2-
import { Data, Effect, Either, Match, pipe, Schema } from "effect";
3-
import { type LoroList, type LoroMap } from "loro-crdt";
2+
import { Data, Effect, Either, Function, Match, pipe } from "effect";
43
import { Dexie } from "./services/dexie";
54

65
class MissingData extends Data.TaggedError("MissingData")<{}> {}
@@ -9,9 +8,8 @@ class DexieError extends Data.TaggedError("DexieError")<{
98
cause: unknown;
109
}> {}
1110

12-
export const useDexieQuery = <A, I extends Record<string, unknown>>(
13-
query: (db: (typeof Dexie.Service)["db"]) => Promise<LoroList<LoroMap<I>>>,
14-
schema: Schema.Schema<A, I>,
11+
export const useDexieQuery = <A, I>(
12+
query: (db: (typeof Dexie.Service)["db"]) => Promise<I>,
1513
deps: unknown[] = []
1614
) => {
1715
const results = useLiveQuery(
@@ -31,18 +29,7 @@ export const useDexieQuery = <A, I extends Record<string, unknown>>(
3129
return pipe(
3230
results,
3331
Either.fromNullable(() => new MissingData()),
34-
Either.flatMap(
35-
Either.match({
36-
onLeft: Either.left,
37-
onRight: (container) =>
38-
pipe(
39-
Schema.decodeEither(Schema.Array(schema))(container.toJSON()),
40-
Either.mapLeft(
41-
(cause) => new DexieError({ reason: "invalid-data", cause })
42-
)
43-
),
44-
})
45-
),
32+
Either.flatMap(Function.identity),
4633
Either.match({
4734
onLeft: (_) =>
4835
Match.value(_).pipe(

packages/schema/src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AnyLoroDocSchema, Table, VersioningSchema } from "./schema";
44
import type { Version } from "./versioning";
55

66
export const VERSION = 1 satisfies Version;
7-
const CurrentSchema = VersioningSchema[VERSION];
7+
export const CurrentSchema = VersioningSchema[VERSION];
88

99
const Metadata = Schema.Struct({ version: Schema.Number });
1010

@@ -20,6 +20,8 @@ export class SnapshotSchema extends Schema.Class<SnapshotSchema>(
2020
metadata: Metadata,
2121
...CurrentSchema.fields,
2222
}) {
23+
static readonly Table = Table;
24+
2325
static readonly EmptyDoc = () => {
2426
const doc = new LoroDoc<LoroSchema>();
2527
doc.getMap("metadata").set("version", VERSION);

0 commit comments

Comments
 (0)