Skip to content

Commit 44efe5f

Browse files
token generation and initial bootstrap query
1 parent b1c575c commit 44efe5f

26 files changed

Lines changed: 395 additions & 386 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { type LoroSchema } from "@local/sync/loro";
2+
import { Effect } from "effect";
3+
import { LoroDoc } from "loro-crdt";
4+
import { TempWorkspace } from "../services/temp-workspace";
5+
import { WorkspaceManager } from "../services/workspace-manager";
6+
7+
export const hookQuery = ({ workspaceId }: { workspaceId: string }) =>
8+
Effect.gen(function* () {
9+
const temp = yield* TempWorkspace;
10+
const manager = yield* WorkspaceManager;
11+
12+
const workspace = yield* manager.getById({ workspaceId });
13+
14+
const tempWorkspace = yield* temp.getById({ workspaceId });
15+
16+
const doc = new LoroDoc<LoroSchema>();
17+
if (workspace) {
18+
doc.import(workspace.snapshot);
19+
}
20+
if (tempWorkspace) {
21+
doc.import(tempWorkspace.snapshot);
22+
}
23+
24+
return doc.getList("activity").toJSON();
25+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Activity } from "@local/sync/loro";
2+
import { RuntimeClient } from "../runtime-client";
3+
import { useDexieQuery } from "../use-dexie-query";
4+
import { hookQuery } from "./hook-query";
5+
6+
export const useActivity = ({ workspaceId }: { workspaceId: string }) => {
7+
return useDexieQuery(
8+
() => RuntimeClient.runPromise(hookQuery({ workspaceId })),
9+
Activity
10+
);
11+
};
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { Layer, ManagedRuntime } from "effect";
22
import { ApiClient } from "./api-client";
33
import { Dexie } from "./dexie";
4+
import { LoroStorage } from "./services/loro-storage";
45
import { TempWorkspace } from "./services/temp-workspace";
56
import { WorkspaceManager } from "./services/workspace-manager";
67

78
const MainLayer = Layer.mergeAll(
89
Dexie.Default,
910
ApiClient.Default,
1011
WorkspaceManager.Default,
11-
TempWorkspace.Default
12+
TempWorkspace.Default,
13+
LoroStorage.Default
1214
);
1315

1416
export const RuntimeClient = ManagedRuntime.make(MainLayer);

apps/client/src/lib/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class WorkspaceTable extends Schema.Class<WorkspaceTable>(
1313
snapshot: Snapshot,
1414
token: Schema.NullOr(Schema.String),
1515

16-
version: Schema.Uint8Array,
16+
version: Schema.NullOr(Schema.Uint8Array),
1717
}) {}
1818

1919
export class TempWorkspaceTable extends Schema.Class<TempWorkspaceTable>(
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Activity, type LoroSchema } from "@local/sync/loro";
2+
import { Effect, Schema } from "effect";
3+
import { LoroDoc, LoroMap, VersionVector } from "loro-crdt";
4+
import { TempWorkspace } from "./temp-workspace";
5+
import { WorkspaceManager } from "./workspace-manager";
6+
7+
export class LoroStorage extends Effect.Service<LoroStorage>()("LoroStorage", {
8+
accessors: true,
9+
dependencies: [TempWorkspace.Default, WorkspaceManager.Default],
10+
effect: Effect.gen(function* () {
11+
const manager = yield* WorkspaceManager;
12+
const temp = yield* TempWorkspace;
13+
14+
const load = ({ workspaceId }: { workspaceId: string }) =>
15+
Effect.all(
16+
{
17+
workspace: manager.getById({ workspaceId }),
18+
tempWorkspace: temp.getById({ workspaceId }),
19+
},
20+
{ concurrency: "unbounded" }
21+
).pipe(
22+
Effect.map(({ workspace, tempWorkspace }) => {
23+
const doc = new LoroDoc<LoroSchema>();
24+
25+
if (workspace !== undefined) {
26+
doc.import(workspace.snapshot);
27+
}
28+
29+
if (tempWorkspace !== undefined) {
30+
doc.import(tempWorkspace.snapshot);
31+
}
32+
33+
return { doc, workspace };
34+
})
35+
);
36+
37+
const insert = ({
38+
workspaceId,
39+
value,
40+
}: {
41+
workspaceId: string;
42+
value: typeof Activity.Type;
43+
}) =>
44+
Effect.gen(function* () {
45+
const { doc, workspace } = yield* load({ workspaceId });
46+
47+
const list = doc.getList("activity");
48+
49+
const container = doc
50+
.getList("activity")
51+
.insertContainer(list.length, new LoroMap());
52+
53+
const activity = yield* Schema.encode(Activity)(value);
54+
55+
Object.entries(activity).forEach(([key, val]) => {
56+
container.set(key as keyof typeof Activity.Type, val);
57+
});
58+
59+
const snapshotExport =
60+
workspace === undefined
61+
? doc.export({ mode: "snapshot" })
62+
: doc.export({
63+
mode: "update",
64+
from: new VersionVector(workspace.version),
65+
});
66+
67+
return yield* temp.put({ workspaceId, snapshot: snapshotExport });
68+
});
69+
70+
return {
71+
insertActivity: insert,
72+
};
73+
}),
74+
}) {}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ export class TempWorkspace extends Effect.Service<TempWorkspace>()(
1414
Effect.flatMap((data) => query((_) => _.temp_workspace.put(data)))
1515
),
1616

17-
get: ({ workspaceId }: { workspaceId: string }) =>
17+
getById: ({ workspaceId }: { workspaceId: string }) =>
1818
query((_) =>
1919
_.temp_workspace
2020
.where("workspaceId")
2121
.equals(workspaceId)
2222
.limit(1)
2323
.first()
2424
).pipe(
25-
Effect.flatMap(Effect.fromNullable),
26-
Effect.flatMap(Schema.decode(TempWorkspaceTable))
25+
Effect.flatMap((workspace) =>
26+
workspace === undefined
27+
? Effect.succeed(undefined)
28+
: Schema.decode(TempWorkspaceTable)(workspace)
29+
)
2730
),
2831

2932
clean: ({ workspaceId }: { workspaceId: string }) =>

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { type LoroSchema, Snapshot } from "@local/sync/loro";
12
import { Effect, Schema } from "effect";
3+
import { LoroDoc } from "loro-crdt";
24
import { Dexie } from "../dexie";
35
import { WorkspaceTable } from "../schema";
46

@@ -33,19 +35,32 @@ export class WorkspaceManager extends Effect.Service<WorkspaceManager>()(
3335
.equals(workspaceId)
3436
.limit(1)
3537
.first()
36-
).pipe(Effect.flatMap(Effect.fromNullable)),
38+
).pipe(
39+
Effect.flatMap((workspace) =>
40+
workspace === undefined
41+
? Effect.succeed(undefined)
42+
: Schema.decode(WorkspaceTable)(workspace)
43+
)
44+
),
3745

3846
createOrJoin: (workspaceId: string | undefined) =>
3947
query((_) =>
4048
_.workspace.toCollection().modify({ current: false })
4149
).pipe(
4250
Effect.andThen(
51+
Schema.encode(Snapshot)(
52+
new LoroDoc<LoroSchema>().export({
53+
mode: "snapshot",
54+
})
55+
)
56+
),
57+
Effect.flatMap((snapshot) =>
4358
query((_) =>
4459
_.workspace.put({
45-
workspaceId: workspaceId ?? crypto.randomUUID(),
46-
version: [],
47-
snapshot: [],
60+
snapshot,
4861
token: null,
62+
version: null,
63+
workspaceId: workspaceId ?? crypto.randomUUID(),
4964
})
5065
)
5166
),

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,30 @@ import { BrowserWorker } from "@effect/platform-browser";
33
import { createFileRoute } from "@tanstack/react-router";
44
import { Effect } from "effect";
55
import { startTransition } from "react";
6+
import { useActivity } from "../lib/hooks/use-activity";
67
import { RuntimeClient } from "../lib/runtime-client";
8+
import { LoroStorage } from "../lib/services/loro-storage";
79
import { WorkspaceManager } from "../lib/services/workspace-manager";
810
import { useActionEffect } from "../lib/use-action-effect";
911
import { Bootstrap } from "../workers/schema";
1012

1113
export const Route = createFileRoute("/$workspaceId")({
1214
component: RouteComponent,
1315
loader: ({ params: { workspaceId } }) =>
14-
RuntimeClient.runPromise(WorkspaceManager.getById({ workspaceId })),
16+
RuntimeClient.runPromise(
17+
WorkspaceManager.getById({ workspaceId }).pipe(
18+
Effect.flatMap(Effect.fromNullable)
19+
)
20+
),
1521
});
1622

1723
function RouteComponent() {
1824
const workspace = Route.useLoaderData();
1925

26+
const { data, error, loading } = useActivity({
27+
workspaceId: workspace.workspaceId,
28+
});
29+
2030
const [, bootstrap] = useActionEffect(
2131
({ workspaceId }: { workspaceId: string }) =>
2232
Effect.gen(function* () {
@@ -36,6 +46,22 @@ function RouteComponent() {
3646
)
3747
);
3848

49+
const [, onAdd] = useActionEffect((formData: FormData) =>
50+
Effect.gen(function* () {
51+
const loroStorage = yield* LoroStorage;
52+
53+
const name = formData.get("name") as string;
54+
55+
yield* loroStorage.insertActivity({
56+
workspaceId: workspace.workspaceId,
57+
value: {
58+
id: crypto.randomUUID(),
59+
name,
60+
},
61+
});
62+
})
63+
);
64+
3965
return (
4066
<div>
4167
<p>{workspace.workspaceId}</p>
@@ -48,6 +74,21 @@ function RouteComponent() {
4874
>
4975
Bootstrap
5076
</button>
77+
78+
<form action={onAdd}>
79+
<input type="text" name="name" />
80+
<button type="submit">Add activity</button>
81+
</form>
82+
83+
<div>
84+
{loading && <p>Loading...</p>}
85+
{error && <pre>{JSON.stringify(error, null, 2)}</pre>}
86+
{(data ?? []).map((activity) => (
87+
<div key={activity.id}>
88+
<label>Name {activity.name}</label>
89+
</div>
90+
))}
91+
</div>
5192
</div>
5293
);
5394
}

apps/client/src/workers/sync.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,25 @@ const WorkerLive = WorkerRunner.layerSerialized(WorkerMessage, {
2222

2323
const workspace = yield* manager
2424
.getById({ workspaceId: params.workspaceId })
25-
.pipe(Effect.mapError(() => "Get workspace error"));
25+
.pipe(
26+
Effect.flatMap(Effect.fromNullable),
27+
Effect.mapError(() => "Get workspace error")
28+
);
2629

2730
const clientId = yield* initClient.pipe(
2831
Effect.mapError(() => "Init client error")
2932
);
3033

3134
const tempUpdates = yield* temp
32-
.get({ workspaceId: workspace.workspaceId })
35+
.getById({ workspaceId: workspace.workspaceId })
3336
.pipe(Effect.mapError(() => "Get temp workspace error"));
3437

35-
if (tempUpdates !== null) {
38+
if (tempUpdates !== undefined) {
3639
const response = yield* Effect.fromNullable(workspace.token).pipe(
3740
Effect.flatMap((token) =>
3841
client.syncData
3942
.push({
40-
headers: { Authorization: `Bearer ${token}` },
43+
// headers: { Authorization: `Bearer ${token}` },
4144
path: { workspaceId: workspace.workspaceId },
4245
payload: { clientId, snapshot: tempUpdates.snapshot },
4346
})
@@ -79,6 +82,10 @@ const WorkerLive = WorkerRunner.layerSerialized(WorkerMessage, {
7982
})
8083
.pipe(Effect.mapError(() => "Put workspace error"));
8184

85+
yield* temp
86+
.clean({ workspaceId: workspace.workspaceId })
87+
.pipe(Effect.mapError(() => "Clean temp workspace error"));
88+
8289
yield* Effect.log("Sync completed");
8390
} else {
8491
yield* Effect.log("No sync updates");

apps/server/drizzle/0000_careless_doctor_faustus.sql

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)