Skip to content

Commit b1c575c

Browse files
push and generate token server
1 parent 0741ebc commit b1c575c

8 files changed

Lines changed: 203 additions & 36 deletions

File tree

apps/client/src/workers/sync.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { WorkerRunner } from "@effect/platform";
22
import { BrowserWorkerRunner } from "@effect/platform-browser";
33
import { Effect, Layer } from "effect";
4+
import { LoroDoc } from "loro-crdt";
45
import { ApiClient } from "../lib/api-client";
56
import { Dexie } from "../lib/dexie";
67
import { RuntimeClient } from "../lib/runtime-client";
@@ -66,12 +67,15 @@ const WorkerLive = WorkerRunner.layerSerialized(WorkerMessage, {
6667
)
6768
);
6869

70+
const doc = new LoroDoc();
71+
doc.import(response.snapshot);
72+
6973
yield* manager
7074
.put({
7175
workspaceId: response.workspaceId,
7276
snapshot: response.snapshot,
7377
token: response.token,
74-
version: new Uint8Array(), // TODO
78+
version: doc.version().encode(),
7579
})
7680
.pipe(Effect.mapError(() => "Put workspace error"));
7781

apps/server/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ POSTGRES_PASSWORD=postgres
22
POSTGRES_USERNAME=postgres
33
POSTGRES_DATABASE=postgres
44
POSTGRES_HOST=localhost
5-
POSTGRES_PORT=5435
5+
POSTGRES_PORT=5435
6+
JWT_SECRET=

apps/server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"author": "",
1313
"license": "ISC",
1414
"devDependencies": {
15+
"@types/jsonwebtoken": "^9.0.9",
1516
"@types/node": "^22.5.1",
1617
"drizzle-kit": "^0.30.4",
1718
"tsx": "^4.19.2"
@@ -24,6 +25,7 @@
2425
"@local/sync": "workspace:*",
2526
"drizzle-orm": "^0.39.3",
2627
"effect": "^3.13.2",
28+
"jsonwebtoken": "^9.0.2",
2729
"pg": "^8.13.3"
2830
}
2931
}

apps/server/src/group/sync-auth.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,45 @@
11
import { HttpApiBuilder } from "@effect/platform";
2-
import { SyncApi } from "@local/sync";
3-
import { Effect } from "effect";
2+
import { SyncApi, type Scope } from "@local/sync";
3+
import { Config, Effect, Redacted } from "effect";
4+
import * as jwt from "jsonwebtoken";
5+
6+
interface TokenPayload {
7+
iat: number; // Issued at (Unix timestamp)
8+
exp?: number; // Expiration (optional for master tokens)
9+
sub: string; // Client ID
10+
workspaceId: string; // Workshop ID
11+
scope: typeof Scope.Type; // Permission scope
12+
isMaster: boolean; // Master token flag
13+
}
414

515
export const SyncAuthGroupLive = HttpApiBuilder.group(
616
SyncApi,
717
"syncAuth",
818
(handlers) =>
919
Effect.gen(function* () {
20+
const secretKey = yield* Config.redacted("JWT_SECRET");
1021
return handlers
1122
.handle("generateToken", ({ payload }) =>
12-
Effect.fail("Not implemented")
23+
Effect.gen(function* () {
24+
const tokenPayload = {
25+
iat: Math.floor(Date.now() / 1000), // Current timestamp in seconds
26+
sub: payload.clientId,
27+
workspaceId: payload.workspaceId,
28+
scope: "read_write", // TODO
29+
isMaster: true, // TODO
30+
} satisfies TokenPayload;
31+
32+
const token = jwt.sign(tokenPayload, Redacted.value(secretKey), {
33+
algorithm: "HS256",
34+
});
35+
36+
return {
37+
token,
38+
workspaceId: payload.workspaceId,
39+
snapshot: payload.snapshot,
40+
createdAt: new Date(),
41+
};
42+
})
1343
)
1444
.handle("issueToken", ({ path: { workspaceId } }) =>
1545
Effect.fail("Not implemented")

apps/server/src/group/sync-data.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HttpApiBuilder } from "@effect/platform";
22
import { SyncApi } from "@local/sync";
3-
import { LoroSchemaTransform } from "@local/sync/loro";
3+
import { SnapshotToLoroDoc } from "@local/sync/loro";
44
import { and, eq } from "drizzle-orm";
55
import { Array, Effect, Layer, Schema } from "effect";
66
import { workspaceTable } from "../db/schema";
@@ -17,7 +17,7 @@ export const SyncDataGroupLive = HttpApiBuilder.group(
1717
"push",
1818
({ payload: { clientId, snapshot }, path: { workspaceId } }) =>
1919
Effect.gen(function* () {
20-
const doc = yield* Schema.encode(LoroSchemaTransform)(snapshot);
20+
const doc = yield* Schema.decode(SnapshotToLoroDoc)(snapshot);
2121

2222
const workspace = yield* query({
2323
Request: Schema.Struct({
@@ -39,26 +39,29 @@ export const SyncDataGroupLive = HttpApiBuilder.group(
3939

4040
doc.import(workspace.snapshot); // 🪄
4141

42-
const newSnapshot =
43-
yield* Schema.decode(LoroSchemaTransform)(doc);
42+
const newSnapshot = yield* Schema.encode(SnapshotToLoroDoc)(doc);
4443

45-
yield* query({
44+
const newWorkspace = yield* query({
4645
Request: Schema.Struct({
47-
newSnapshot: Schema.Uint8Array,
46+
newSnapshot: Schema.Uint8ArrayFromSelf,
4847
}),
4948
execute: (db, { newSnapshot: snapshot }) =>
50-
db.insert(workspaceTable).values({
51-
snapshot,
52-
clientId: workspace.clientId,
53-
workspaceId: workspace.workspaceId,
54-
ownerClientId: workspace.ownerClientId,
55-
// createdAt
56-
}),
57-
})({ newSnapshot });
49+
db
50+
.insert(workspaceTable)
51+
.values({
52+
snapshot,
53+
clientId: workspace.clientId,
54+
workspaceId: workspace.workspaceId,
55+
ownerClientId: workspace.ownerClientId,
56+
})
57+
.returning(),
58+
})({ newSnapshot }).pipe(Effect.flatMap(Array.head));
5859

59-
return yield* Effect.fail({
60-
message: "Not (fully) implemented" as const,
61-
});
60+
return {
61+
workspaceId: newWorkspace.workspaceId,
62+
createdAt: newWorkspace.createdAt,
63+
snapshot: newWorkspace.snapshot,
64+
};
6265
}).pipe(Effect.mapError((error) => error.message))
6366
)
6467
.handle("pull", ({ path: { workspaceId } }) =>

packages/sync/src/loro.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,41 @@ const SnapshotSchemaTransform = Schema.instanceOf(LoroDoc<LoroSchema>).pipe(
4343
);
4444

4545
export const Snapshot = Schema.Uint8Array;
46-
export const LoroSchemaTransform = Schema.instanceOf(LoroDoc<LoroSchema>).pipe(
47-
Schema.transformOrFail(Snapshot, {
46+
export const SnapshotToLoroDoc = Schema.Uint8ArrayFromSelf.pipe(
47+
Schema.transformOrFail(Schema.instanceOf(LoroDoc<LoroSchema>), {
4848
decode: (from, _, ast) =>
49-
Schema.encode(Snapshot)(from.export({ mode: "snapshot" })).pipe(
49+
Effect.gen(function* () {
50+
const doc = new LoroDoc<LoroSchema>();
51+
doc.import(from);
52+
53+
// TODO: This?
54+
yield* Schema.decodeUnknown(SnapshotSchema)(doc.toJSON());
55+
56+
return doc;
57+
}).pipe(
5058
Effect.mapError(
5159
(error) => new ParseResult.Type(ast, from, error.message)
5260
)
5361
),
5462

5563
encode: (to, _, ast) =>
56-
Schema.decode(Snapshot)(to).pipe(
57-
Effect.flatMap((data) =>
58-
Effect.gen(function* () {
59-
const doc = new LoroDoc<LoroSchema>();
60-
doc.import(data);
61-
yield* Schema.decodeUnknown(SnapshotSchema)(doc.toJSON());
62-
return doc;
63-
})
64-
),
64+
Schema.encode(Snapshot)(to.export({ mode: "snapshot" })).pipe(
65+
Effect.flatMap(Schema.decode(Snapshot)),
6566
Effect.mapError((error) => new ParseResult.Type(ast, to, error.message))
6667
),
6768
})
6869
);
70+
71+
// const swap = <A, I, R>(
72+
// schema: Schema.Schema<A, I, R>
73+
// ): Schema.Schema<I, A, R> =>
74+
// Schema.transformOrFail(
75+
// Schema.typeSchema(schema),
76+
// Schema.encodedSchema(schema),
77+
// {
78+
// decode: ParseResult.encode(schema),
79+
// encode: ParseResult.decode(schema),
80+
// }
81+
// );
82+
83+
// export const LoroDocSchema = Schema.compose( LoroSchemaTransform, Snapshot);

packages/sync/src/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
HttpApiSchema,
66
} from "@effect/platform";
77
import { Schema } from "effect";
8-
import { LoroSchemaTransform } from "./loro";
8+
import { Snapshot } from "./loro";
99

1010
export const ClientId = Schema.UUID;
1111
export const WorkspaceId = Schema.UUID;
@@ -23,7 +23,7 @@ export class WorkspaceTable extends Schema.Class<WorkspaceTable>(
2323
ownerClientId: ClientId,
2424
createdAt: Schema.DateFromString,
2525
clientId: ClientId,
26-
snapshot: LoroSchemaTransform,
26+
snapshot: Snapshot,
2727
}) {}
2828

2929
export class TokenTable extends Schema.Class<TokenTable>("TokenTable")({

0 commit comments

Comments
 (0)